From 042a52550b83a0c91bf7f6230d042bc8727a390e Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 28 Jan 2026 07:10:25 -0800 Subject: [PATCH] =?UTF-8?q?feat(c2c):=20=E5=AE=9E=E7=8E=B0C2C=20Bot?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=BA=A4=E6=98=93=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建独立的 mining-blockchain-service 服务 (基于 blockchain-service) - 添加 dUSDT 转账接口供 C2C Bot 调用 - 实现 C2cBotService 自动购买卖单 - 实现 C2cBotScheduler 每10秒扫描待处理卖单 - 添加 BlockchainClient 和 IdentityClient 客户端 - 更新 C2cOrder 模型添加 Bot 购买相关字段 - 使用 MPC 热钱包签名交易 Co-Authored-By: Claude Opus 4.5 --- .../mining-blockchain-service/.dockerignore | 13 + .../mining-blockchain-service/.env.example | 77 + .../mining-blockchain-service/.eslintrc.js | 25 + .../mining-blockchain-service/.gitignore | 32 + .../mining-blockchain-service/.prettierrc | 7 + .../DEVELOPMENT_GUIDE.md | 2790 ++++ .../mining-blockchain-service/Dockerfile | 82 + .../contracts/TestUSDT.sol | 59 + .../contracts/TestUSDT_Flat.sol | 142 + .../contracts/eUSDT/EnergyUSDT.sol | 78 + .../contracts/eUSDT/README.md | 81 + .../contracts/eUSDT/compile.mjs | 51 + .../contracts/eUSDT/deploy.mjs | 86 + .../contracts/eUSDT/deployment.json | 14 + .../contracts/eUSDT/package-lock.json | 222 + .../contracts/eUSDT/package.json | 14 + .../contracts/fUSDT/FutureUSDT.sol | 78 + .../contracts/fUSDT/README.md | 81 + .../contracts/fUSDT/compile.mjs | 51 + .../contracts/fUSDT/deploy.mjs | 86 + .../contracts/fUSDT/deployment.json | 14 + .../contracts/fUSDT/package-lock.json | 222 + .../contracts/fUSDT/package.json | 14 + .../mining-blockchain-service/deploy.sh | 158 + .../docker-compose.yml | 53 + .../mining-blockchain-service/nest-cli.json | 17 + .../package-lock.json | 10587 ++++++++++++++++ .../mining-blockchain-service/package.json | 105 + .../20241207000000_init/migration.sql | 141 + .../migration.sql | 65 + .../migration.sql | 30 + .../prisma/schema.prisma | 260 + .../scripts/README.md | 235 + .../scripts/deploy-kava-simple.ts | 134 + .../scripts/deploy-test-usdt-kava.ts | 106 + .../scripts/deploy-test-usdt.ts | 107 + .../scripts/generate-wallet.ts | 26 + .../scripts/health-check.sh | 93 + .../scripts/quick-test.sh | 119 + .../scripts/rebuild-kafka.sh | 74 + .../scripts/start-all.sh | 66 + .../scripts/stop-service.sh | 44 + .../src/api/api.module.ts | 14 + .../src/api/controllers/balance.controller.ts | 36 + .../controllers/deposit-repair.controller.ts | 72 + .../src/api/controllers/deposit.controller.ts | 102 + .../src/api/controllers/health.controller.ts | 59 + .../src/api/controllers/index.ts | 5 + .../api/controllers/internal.controller.ts | 113 + .../api/controllers/transfer.controller.ts | 114 + .../src/api/dto/index.ts | 2 + .../src/api/dto/request/derive-address.dto.ts | 19 + .../src/api/dto/request/index.ts | 6 + .../dto/request/mark-mnemonic-backup.dto.ts | 8 + .../src/api/dto/request/query-balance.dto.ts | 34 + .../api/dto/request/revoke-mnemonic.dto.ts | 15 + .../dto/request/verify-mnemonic-hash.dto.ts | 18 + .../api/dto/request/verify-mnemonic.dto.ts | 22 + .../src/api/dto/response/address.dto.ts | 20 + .../src/api/dto/response/balance.dto.ts | 26 + .../src/api/dto/response/index.ts | 2 + .../src/app.module.ts | 17 + .../src/application/application.module.ts | 14 + .../src/application/event-handlers/index.ts | 2 + .../mpc-keygen-completed.handler.ts | 74 + .../system-withdrawal-requested.handler.ts | 140 + .../withdrawal-requested.handler.ts | 165 + .../hot-wallet-balance.scheduler.ts | 122 + .../src/application/schedulers/index.ts | 1 + .../services/address-derivation.service.ts | 183 + .../services/balance-query.service.ts | 93 + .../services/deposit-detection.service.ts | 253 + .../services/deposit-repair.service.ts | 195 + .../src/application/services/index.ts | 7 + .../services/mnemonic-verification.service.ts | 173 + .../mpc-transfer-initializer.service.ts | 26 + .../services/outbox-publisher.service.ts | 148 + .../src/config/app.config.ts | 7 + .../src/config/blockchain.config.ts | 65 + .../src/config/database.config.ts | 5 + .../src/config/index.ts | 5 + .../src/config/kafka.config.ts | 7 + .../src/config/redis.config.ts | 8 + .../domain/aggregates/aggregate-root.base.ts | 44 + .../deposit-transaction.aggregate.ts | 212 + .../aggregates/deposit-transaction/index.ts | 1 + .../src/domain/aggregates/index.ts | 4 + .../aggregates/monitored-address/index.ts | 1 + .../monitored-address.aggregate.ts | 84 + .../aggregates/transaction-request/index.ts | 1 + .../transaction-request.aggregate.ts | 185 + .../src/domain/domain.module.ts | 9 + .../src/domain/enums/chain-type.enum.ts | 8 + .../src/domain/enums/deposit-status.enum.ts | 13 + .../src/domain/enums/index.ts | 3 + .../domain/enums/transaction-status.enum.ts | 15 + .../domain/events/deposit-confirmed.event.ts | 30 + .../domain/events/deposit-detected.event.ts | 33 + .../src/domain/events/domain-event.base.ts | 17 + .../src/domain/events/index.ts | 5 + .../events/transaction-broadcasted.event.ts | 29 + .../events/wallet-address-created.event.ts | 32 + .../block-checkpoint.repository.interface.ts | 43 + ...eposit-transaction.repository.interface.ts | 47 + .../src/domain/repositories/index.ts | 5 + .../monitored-address.repository.interface.ts | 44 + .../outbox-event.repository.interface.ts | 99 + ...ransaction-request.repository.interface.ts | 41 + .../domain/services/chain-config.service.ts | 117 + .../services/confirmation-policy.service.ts | 42 + .../domain/services/erc20-transfer.service.ts | 327 + .../src/domain/services/index.ts | 2 + .../domain/value-objects/block-number.vo.ts | 61 + .../src/domain/value-objects/chain-type.vo.ts | 48 + .../domain/value-objects/cosmos-address.vo.ts | 50 + .../domain/value-objects/evm-address.vo.ts | 41 + .../src/domain/value-objects/index.ts | 6 + .../domain/value-objects/token-amount.vo.ts | 99 + .../src/domain/value-objects/tx-hash.vo.ts | 42 + .../blockchain/address-derivation.adapter.ts | 188 + .../blockchain/block-scanner.service.ts | 124 + .../blockchain/evm-provider.adapter.ts | 198 + .../src/infrastructure/blockchain/index.ts | 5 + .../blockchain/mnemonic-derivation.adapter.ts | 148 + .../blockchain/recovery-mnemonic.adapter.ts | 176 + .../infrastructure/infrastructure.module.ts | 88 + .../kafka/deposit-ack-consumer.service.ts | 151 + .../kafka/event-consumer.controller.ts | 36 + .../kafka/event-publisher.service.ts | 111 + .../src/infrastructure/kafka/index.ts | 5 + .../kafka/mpc-event-consumer.service.ts | 247 + .../withdrawal-event-consumer.service.ts | 186 + .../src/infrastructure/mpc/index.ts | 1 + .../infrastructure/mpc/mpc-signing.client.ts | 197 + .../mappers/deposit-transaction.mapper.ts | 79 + .../persistence/mappers/index.ts | 3 + .../mappers/monitored-address.mapper.ts | 46 + .../mappers/transaction-request.mapper.ts | 57 + .../persistence/prisma/prisma.service.ts | 17 + .../block-checkpoint.repository.impl.ts | 90 + .../deposit-transaction.repository.impl.ts | 97 + .../persistence/repositories/index.ts | 5 + .../monitored-address.repository.impl.ts | 90 + .../outbox-event.repository.impl.ts | 219 + .../transaction-request.repository.impl.ts | 94 + .../redis/address-cache.service.ts | 92 + .../src/infrastructure/redis/index.ts | 2 + .../src/infrastructure/redis/redis.service.ts | 67 + .../mining-blockchain-service/src/main.ts | 72 + .../src/shared/decorators/index.ts | 1 + .../src/shared/decorators/public.decorator.ts | 4 + .../shared/exceptions/blockchain.exception.ts | 39 + .../src/shared/exceptions/domain.exception.ts | 26 + .../src/shared/exceptions/index.ts | 2 + .../shared/filters/global-exception.filter.ts | 45 + .../src/shared/filters/index.ts | 1 + .../src/shared/guards/jwt-auth.guard.ts | 22 + .../src/shared/index.ts | 3 + .../src/shared/strategies/jwt.strategy.ts | 32 + .../mining-blockchain-service/tsconfig.json | 24 + .../trading-service/package-lock.json | 123 + backend/services/trading-service/package.json | 1 + .../trading-service/prisma/schema.prisma | 9 + .../src/application/application.module.ts | 6 +- .../schedulers/c2c-bot.scheduler.ts | 117 + .../application/services/c2c-bot.service.ts | 122 + .../blockchain/blockchain.client.ts | 111 + .../identity/identity.client.ts | 71 + .../infrastructure/infrastructure.module.ts | 8 + .../repositories/c2c-order.repository.ts | 49 + .../trading-account.repository.ts | 24 + 171 files changed, 24706 insertions(+), 1 deletion(-) create mode 100644 backend/services/mining-blockchain-service/.dockerignore create mode 100644 backend/services/mining-blockchain-service/.env.example create mode 100644 backend/services/mining-blockchain-service/.eslintrc.js create mode 100644 backend/services/mining-blockchain-service/.gitignore create mode 100644 backend/services/mining-blockchain-service/.prettierrc create mode 100644 backend/services/mining-blockchain-service/DEVELOPMENT_GUIDE.md create mode 100644 backend/services/mining-blockchain-service/Dockerfile create mode 100644 backend/services/mining-blockchain-service/contracts/TestUSDT.sol create mode 100644 backend/services/mining-blockchain-service/contracts/TestUSDT_Flat.sol create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/EnergyUSDT.sol create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/README.md create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/compile.mjs create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/deploy.mjs create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/deployment.json create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/package-lock.json create mode 100644 backend/services/mining-blockchain-service/contracts/eUSDT/package.json create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/FutureUSDT.sol create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/README.md create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/compile.mjs create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/deploy.mjs create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/deployment.json create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/package-lock.json create mode 100644 backend/services/mining-blockchain-service/contracts/fUSDT/package.json create mode 100644 backend/services/mining-blockchain-service/deploy.sh create mode 100644 backend/services/mining-blockchain-service/docker-compose.yml create mode 100644 backend/services/mining-blockchain-service/nest-cli.json create mode 100644 backend/services/mining-blockchain-service/package-lock.json create mode 100644 backend/services/mining-blockchain-service/package.json create mode 100644 backend/services/mining-blockchain-service/prisma/migrations/20241207000000_init/migration.sql create mode 100644 backend/services/mining-blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql create mode 100644 backend/services/mining-blockchain-service/prisma/migrations/20241209000000_add_outbox_events/migration.sql create mode 100644 backend/services/mining-blockchain-service/prisma/schema.prisma create mode 100644 backend/services/mining-blockchain-service/scripts/README.md create mode 100644 backend/services/mining-blockchain-service/scripts/deploy-kava-simple.ts create mode 100644 backend/services/mining-blockchain-service/scripts/deploy-test-usdt-kava.ts create mode 100644 backend/services/mining-blockchain-service/scripts/deploy-test-usdt.ts create mode 100644 backend/services/mining-blockchain-service/scripts/generate-wallet.ts create mode 100644 backend/services/mining-blockchain-service/scripts/health-check.sh create mode 100644 backend/services/mining-blockchain-service/scripts/quick-test.sh create mode 100644 backend/services/mining-blockchain-service/scripts/rebuild-kafka.sh create mode 100644 backend/services/mining-blockchain-service/scripts/start-all.sh create mode 100644 backend/services/mining-blockchain-service/scripts/stop-service.sh create mode 100644 backend/services/mining-blockchain-service/src/api/api.module.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/balance.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/deposit-repair.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/deposit.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/health.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/index.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/internal.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/index.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/derive-address.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/index.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/query-balance.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/response/address.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/response/balance.dto.ts create mode 100644 backend/services/mining-blockchain-service/src/api/dto/response/index.ts create mode 100644 backend/services/mining-blockchain-service/src/app.module.ts create mode 100644 backend/services/mining-blockchain-service/src/application/application.module.ts create mode 100644 backend/services/mining-blockchain-service/src/application/event-handlers/index.ts create mode 100644 backend/services/mining-blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts create mode 100644 backend/services/mining-blockchain-service/src/application/event-handlers/system-withdrawal-requested.handler.ts create mode 100644 backend/services/mining-blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts create mode 100644 backend/services/mining-blockchain-service/src/application/schedulers/hot-wallet-balance.scheduler.ts create mode 100644 backend/services/mining-blockchain-service/src/application/schedulers/index.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/address-derivation.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/balance-query.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/deposit-detection.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/deposit-repair.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/index.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/mnemonic-verification.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/mpc-transfer-initializer.service.ts create mode 100644 backend/services/mining-blockchain-service/src/application/services/outbox-publisher.service.ts create mode 100644 backend/services/mining-blockchain-service/src/config/app.config.ts create mode 100644 backend/services/mining-blockchain-service/src/config/blockchain.config.ts create mode 100644 backend/services/mining-blockchain-service/src/config/database.config.ts create mode 100644 backend/services/mining-blockchain-service/src/config/index.ts create mode 100644 backend/services/mining-blockchain-service/src/config/kafka.config.ts create mode 100644 backend/services/mining-blockchain-service/src/config/redis.config.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/aggregate-root.base.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/transaction-request.aggregate.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/domain.module.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/enums/chain-type.enum.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/enums/deposit-status.enum.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/enums/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/enums/transaction-status.enum.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/deposit-confirmed.event.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/deposit-detected.event.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/domain-event.base.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/transaction-broadcasted.event.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/events/wallet-address-created.event.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/block-checkpoint.repository.interface.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/deposit-transaction.repository.interface.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/monitored-address.repository.interface.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/outbox-event.repository.interface.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/repositories/transaction-request.repository.interface.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/services/confirmation-policy.service.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/services/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/block-number.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/chain-type.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/cosmos-address.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/evm-address.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/index.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/token-amount.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/domain/value-objects/tx-hash.vo.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/block-scanner.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/evm-provider.adapter.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/mnemonic-derivation.adapter.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/deposit-ack-consumer.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/event-consumer.controller.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/mpc/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/transaction-request.mapper.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/prisma/prisma.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/block-checkpoint.repository.impl.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/deposit-transaction.repository.impl.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/monitored-address.repository.impl.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/outbox-event.repository.impl.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/transaction-request.repository.impl.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/redis/address-cache.service.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/redis/index.ts create mode 100644 backend/services/mining-blockchain-service/src/infrastructure/redis/redis.service.ts create mode 100644 backend/services/mining-blockchain-service/src/main.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/decorators/index.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/decorators/public.decorator.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/exceptions/blockchain.exception.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/exceptions/domain.exception.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/exceptions/index.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/filters/global-exception.filter.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/filters/index.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/guards/jwt-auth.guard.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/index.ts create mode 100644 backend/services/mining-blockchain-service/src/shared/strategies/jwt.strategy.ts create mode 100644 backend/services/mining-blockchain-service/tsconfig.json create mode 100644 backend/services/trading-service/src/application/schedulers/c2c-bot.scheduler.ts create mode 100644 backend/services/trading-service/src/application/services/c2c-bot.service.ts create mode 100644 backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts create mode 100644 backend/services/trading-service/src/infrastructure/identity/identity.client.ts diff --git a/backend/services/mining-blockchain-service/.dockerignore b/backend/services/mining-blockchain-service/.dockerignore new file mode 100644 index 00000000..4ee408dd --- /dev/null +++ b/backend/services/mining-blockchain-service/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +.git +.gitignore +.env +.env.local +*.md +.vscode +.idea +coverage +test +*.log +npm-debug.log diff --git a/backend/services/mining-blockchain-service/.env.example b/backend/services/mining-blockchain-service/.env.example new file mode 100644 index 00000000..e6bfe6fc --- /dev/null +++ b/backend/services/mining-blockchain-service/.env.example @@ -0,0 +1,77 @@ +# ============================================================================= +# Mining Blockchain Service - Production Environment Configuration +# ============================================================================= +# +# Role: dUSDT (绿积分) transfer for C2C Bot +# +# Responsibilities: +# - Transfer dUSDT from MPC hot wallet to user's Kava address +# - Query hot wallet balance +# +# Setup: +# 1. Copy to .env: cp .env.example .env +# 2. In Docker Compose mode, most values are overridden by docker-compose.yml +# ============================================================================= + +# ============================================================================= +# Application +# ============================================================================= +NODE_ENV=production +PORT=3020 +SERVICE_NAME=mining-blockchain-service +API_PREFIX=api/v1 + +# ============================================================================= +# Database (PostgreSQL) +# ============================================================================= +DATABASE_URL=postgresql://rwa_user:your_password@localhost:5432/rwa_mining_blockchain?schema=public + +# ============================================================================= +# Redis +# ============================================================================= +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=15 +REDIS_PASSWORD= + +# ============================================================================= +# Kafka (用于 MPC 签名通信) +# ============================================================================= +KAFKA_BROKERS=localhost:9092 +KAFKA_CLIENT_ID=mining-blockchain-service +KAFKA_GROUP_ID=mining-blockchain-service-group + +# ============================================================================= +# Blockchain - KAVA (EVM-compatible Cosmos chain) +# ============================================================================= +# Official KAVA EVM RPC endpoint +KAVA_RPC_URL=https://evm.kava.io +KAVA_CHAIN_ID=2222 +# dUSDT (绿积分) 合约地址 - Durian USDT, 精度6位 +# 合约链接: https://kavascan.com/address/0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3 +KAVA_USDT_CONTRACT=0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3 + +# ============================================================================= +# dUSDT Transfer Configuration +# ============================================================================= +# 等待交易确认的最大时间(秒) +TX_CONFIRMATION_TIMEOUT=120 + +# ============================================================================= +# MPC Hot Wallet (C2C Bot 热钱包) +# ============================================================================= +# MPC 服务地址 +MPC_SERVICE_URL=http://localhost:3013 + +# C2C Bot 热钱包用户名(MPC 系统中的标识,需要预先通过 keygen 创建) +HOT_WALLET_USERNAME=c2c-bot-wallet + +# C2C Bot 热钱包地址(从 MPC 公钥派生的 EVM 地址) +# 在 MPC keygen 完成后,从公钥计算得出 +HOT_WALLET_ADDRESS= + +# ============================================================================= +# Logging +# ============================================================================= +# Options: debug, info, warn, error +LOG_LEVEL=info diff --git a/backend/services/mining-blockchain-service/.eslintrc.js b/backend/services/mining-blockchain-service/.eslintrc.js new file mode 100644 index 00000000..fe00111e --- /dev/null +++ b/backend/services/mining-blockchain-service/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + }, +}; diff --git a/backend/services/mining-blockchain-service/.gitignore b/backend/services/mining-blockchain-service/.gitignore new file mode 100644 index 00000000..4720b898 --- /dev/null +++ b/backend/services/mining-blockchain-service/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ + +# Build +dist/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* + +# Test +coverage/ + +# Prisma +prisma/*.db +prisma/*.db-journal diff --git a/backend/services/mining-blockchain-service/.prettierrc b/backend/services/mining-blockchain-service/.prettierrc new file mode 100644 index 00000000..243b2659 --- /dev/null +++ b/backend/services/mining-blockchain-service/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "printWidth": 100 +} diff --git a/backend/services/mining-blockchain-service/DEVELOPMENT_GUIDE.md b/backend/services/mining-blockchain-service/DEVELOPMENT_GUIDE.md new file mode 100644 index 00000000..2388e215 --- /dev/null +++ b/backend/services/mining-blockchain-service/DEVELOPMENT_GUIDE.md @@ -0,0 +1,2790 @@ +# Blockchain Service 开发指南 + +## 1. 服务概述 + +### 1.1 服务定位 + +blockchain-service 是 RWA 榴莲皇后平台的区块链基础设施服务,负责: + +- **公钥→地址派生**:从 MPC 公钥派生多链钱包地址 (EVM/Cosmos) +- **链上事件监听**:监听 ERC20 Transfer 事件,检测用户充值 +- **充值入账触发**:检测到充值后通知 wallet-service 入账 +- **余额查询**:查询链上 USDT/原生代币余额 +- **交易广播**:提交签名后的交易到链上 +- **地址管理**:管理平台充值地址池 + +### 1.4 与其他服务的关系 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ identity-service│ │ mpc-service │ │blockchain-service│ +├─────────────────┤ ├─────────────────┤ ├─────────────────┤ +│ - 用户账户 │ │ - 密钥分片生成 │ │ - 公钥→地址派生 │ +│ - 设备绑定 │ │ - 签名协调 │ │ - 充值检测 │ +│ - KYC验证 │ │ - 分片存储 │ │ - 交易广播 │ +│ - 身份认证 │ │ - 阈值策略 │ │ - 余额查询 │ +└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + │ │ │ + │ MpcKeygenRequested │ KeygenCompleted │ + │──────────────────────>│ (with publicKey) │ + │ │──────────────────────>│ + │ │ │ derive address + │ WalletAddressCreated │ + │<──────────────────────────────────────────────│ + │ (存储 userId ↔ walletAddress 关联) │ + │ │ │ + │ wallet-service │ + │ │ │ + │ WalletAddressCreated │ + │<──────────────────────────────────────────────│ + │ (存储钱包地址、管理余额) │ +``` + +**职责边界原则**: +- **identity-service**:只关心用户身份,不处理区块链技术细节 +- **mpc-service**:只关心 MPC 协议,生成公钥后发布事件 +- **blockchain-service**:封装所有区块链技术,包括地址派生、链交互 +- **wallet-service**:管理用户钱包的业务逻辑,不直接与链交互 + +### 1.2 技术栈 + +| 组件 | 技术选型 | +|------|----------| +| 框架 | NestJS 10.x | +| 语言 | TypeScript 5.x | +| 数据库 | PostgreSQL 15 + Prisma | +| 消息队列 | Kafka | +| 缓存 | Redis | +| 区块链 | ethers.js 6.x | +| 容器化 | Docker | + +### 1.3 端口分配 + +- HTTP API: `3012` +- 数据库: `rwa_blockchain` (共享 PostgreSQL) +- Redis DB: `11` + +--- + +## 2. 架构设计 + +### 2.1 六边形架构 (Hexagonal Architecture) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ blockchain-service │ +├─────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ API Layer │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Health Ctrl │ │ Balance Ctrl │ │ Internal Ctrl│ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Application Layer │ │ +│ │ ┌──────────────────┐ ┌──────────────────┐ │ │ +│ │ │ DepositDetection │ │ BalanceQuery │ │ │ +│ │ │ Service │ │ Service │ │ │ +│ │ └──────────────────┘ └──────────────────┘ │ │ +│ │ ┌──────────────────┐ ┌──────────────────┐ │ │ +│ │ │ TransactionBroad │ │ AddressRegistry │ │ │ +│ │ │ castService │ │ Service │ │ │ +│ │ └──────────────────┘ └──────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Domain Layer │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ Aggregates │ │ │ +│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │ │ +│ │ │ │DepositTx │ │MonitoredAddr│ │TransactionRequest │ │ │ │ +│ │ │ └────────────┘ └────────────┘ └────────────────────┘ │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ Domain Events │ │ │ +│ │ │ DepositDetected, TransactionBroadcasted, BlockScanned │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ Repository Interfaces (Ports) │ │ │ +│ │ │ IDepositTxRepository, IMonitoredAddressRepository │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Infrastructure Layer │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Prisma Repos │ │ Kafka │ │ Redis Cache │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ EVM Provider │ │ Event │ │ Block │ │ │ +│ │ │ Adapter │ │ Listener │ │ Scanner │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 目录结构 + +``` +blockchain-service/ +├── prisma/ +│ └── schema.prisma +├── src/ +│ ├── modules/ # NestJS 模块定义 (与其他服务保持一致) +│ │ ├── api.module.ts # API 层模块 +│ │ ├── application.module.ts # 应用层模块 +│ │ ├── domain.module.ts # 领域层模块 +│ │ └── infrastructure.module.ts # 基础设施层模块 +│ │ +│ ├── api/ # 入站适配器 (Driving Adapters) +│ │ ├── controllers/ +│ │ │ ├── health.controller.ts +│ │ │ ├── balance.controller.ts +│ │ │ └── internal.controller.ts +│ │ └── dto/ +│ │ ├── request/ +│ │ │ └── query-balance.dto.ts +│ │ └── response/ +│ │ └── balance.dto.ts +│ │ +│ ├── application/ # 应用层 (Use Cases) +│ │ ├── services/ +│ │ │ ├── deposit-detection.service.ts +│ │ │ ├── balance-query.service.ts +│ │ │ ├── transaction-broadcast.service.ts +│ │ │ ├── address-registry.service.ts +│ │ │ └── address-derivation.service.ts # 公钥→地址派生 +│ │ ├── commands/ +│ │ │ ├── register-address/ +│ │ │ │ ├── register-address.command.ts +│ │ │ │ └── register-address.handler.ts +│ │ │ └── broadcast-transaction/ +│ │ │ ├── broadcast-transaction.command.ts +│ │ │ └── broadcast-transaction.handler.ts +│ │ └── queries/ +│ │ ├── get-balance/ +│ │ │ ├── get-balance.query.ts +│ │ │ └── get-balance.handler.ts +│ │ └── get-deposit-history/ +│ │ ├── get-deposit-history.query.ts +│ │ └── get-deposit-history.handler.ts +│ │ +│ ├── domain/ # 领域层 (核心业务) +│ │ ├── aggregates/ +│ │ │ ├── aggregate-root.base.ts # 聚合根基类 (统一事件管理) +│ │ │ ├── deposit-transaction/ +│ │ │ │ ├── deposit-transaction.aggregate.ts +│ │ │ │ ├── deposit-transaction.factory.ts +│ │ │ │ └── index.ts +│ │ │ ├── monitored-address/ +│ │ │ │ ├── monitored-address.aggregate.ts +│ │ │ │ └── index.ts +│ │ │ └── transaction-request/ +│ │ │ ├── transaction-request.aggregate.ts +│ │ │ └── index.ts +│ │ ├── entities/ +│ │ │ └── block-checkpoint.entity.ts +│ │ ├── events/ +│ │ │ ├── domain-event.base.ts +│ │ │ ├── deposit-detected.event.ts +│ │ │ ├── deposit-confirmed.event.ts +│ │ │ ├── transaction-broadcasted.event.ts +│ │ │ ├── wallet-address-created.event.ts # 地址派生完成事件 +│ │ │ └── index.ts +│ │ ├── repositories/ +│ │ │ ├── deposit-transaction.repository.interface.ts +│ │ │ ├── monitored-address.repository.interface.ts +│ │ │ ├── block-checkpoint.repository.interface.ts +│ │ │ └── index.ts +│ │ ├── services/ +│ │ │ ├── confirmation-policy.service.ts +│ │ │ └── chain-config.service.ts +│ │ ├── value-objects/ +│ │ │ ├── chain-type.vo.ts +│ │ │ ├── tx-hash.vo.ts +│ │ │ ├── evm-address.vo.ts +│ │ │ ├── token-amount.vo.ts +│ │ │ ├── block-number.vo.ts +│ │ │ └── index.ts +│ │ └── enums/ +│ │ ├── deposit-status.enum.ts +│ │ ├── chain-type.enum.ts +│ │ └── index.ts +│ │ +│ ├── infrastructure/ # 出站适配器 (Driven Adapters) +│ │ ├── blockchain/ +│ │ │ ├── evm-provider.adapter.ts +│ │ │ ├── event-listener.service.ts +│ │ │ ├── block-scanner.service.ts +│ │ │ ├── transaction-sender.service.ts +│ │ │ ├── address-derivation.adapter.ts # 地址派生适配器 +│ │ │ └── blockchain.module.ts +│ │ ├── persistence/ +│ │ │ ├── prisma/ +│ │ │ │ └── prisma.service.ts +│ │ │ ├── repositories/ +│ │ │ │ ├── deposit-transaction.repository.impl.ts +│ │ │ │ ├── monitored-address.repository.impl.ts +│ │ │ │ └── block-checkpoint.repository.impl.ts +│ │ │ └── mappers/ +│ │ │ ├── deposit-transaction.mapper.ts +│ │ │ ├── monitored-address.mapper.ts +│ │ │ └── transaction-request.mapper.ts +│ │ ├── kafka/ +│ │ │ ├── event-publisher.service.ts +│ │ │ ├── event-consumer.controller.ts +│ │ │ └── kafka.module.ts +│ │ ├── redis/ +│ │ │ ├── redis.service.ts +│ │ │ ├── address-cache.service.ts +│ │ │ └── redis.module.ts +│ │ ├── external/ +│ │ │ ├── wallet-service/ +│ │ │ │ └── wallet-client.service.ts +│ │ │ └── identity-service/ +│ │ │ └── identity-client.service.ts +│ │ └── infrastructure.module.ts +│ │ +│ ├── config/ +│ │ ├── app.config.ts +│ │ ├── database.config.ts +│ │ ├── kafka.config.ts +│ │ ├── redis.config.ts +│ │ ├── chain.config.ts +│ │ └── index.ts +│ │ +│ ├── shared/ +│ │ ├── decorators/ +│ │ │ ├── public.decorator.ts +│ │ │ └── index.ts +│ │ ├── exceptions/ +│ │ │ ├── domain.exception.ts +│ │ │ ├── blockchain.exception.ts +│ │ │ └── index.ts +│ │ ├── filters/ +│ │ │ ├── global-exception.filter.ts +│ │ │ └── domain-exception.filter.ts +│ │ └── interceptors/ +│ │ └── transform.interceptor.ts +│ │ +│ ├── app.module.ts +│ └── main.ts +│ +├── test/ +│ ├── unit/ +│ ├── integration/ +│ └── e2e/ +│ +├── .env.example +├── Dockerfile +├── docker-compose.yml +├── package.json +├── tsconfig.json +├── nest-cli.json +└── DEVELOPMENT_GUIDE.md +``` + +### 2.3 模块化设计 (与其他服务保持一致) + +为了与 identity-service、wallet-service、leaderboard-service 等服务保持架构一致性,采用分层模块化设计: + +```typescript +// src/modules/infrastructure.module.ts +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { RedisService } from '@/infrastructure/redis/redis.service'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { WalletClientService } from '@/infrastructure/external/wallet-service/wallet-client.service'; +import { IdentityClientService } from '@/infrastructure/external/identity-service/identity-client.service'; + +@Global() +@Module({ + providers: [ + PrismaService, + RedisService, + EvmProviderAdapter, + EventPublisherService, + WalletClientService, + IdentityClientService, + ], + exports: [ + PrismaService, + RedisService, + EvmProviderAdapter, + EventPublisherService, + WalletClientService, + IdentityClientService, + ], +}) +export class InfrastructureModule {} +``` + +```typescript +// src/modules/domain.module.ts +import { Module } from '@nestjs/common'; +import { InfrastructureModule } from './infrastructure.module'; +import { + DEPOSIT_TRANSACTION_REPOSITORY, + MONITORED_ADDRESS_REPOSITORY, + BLOCK_CHECKPOINT_REPOSITORY, +} from '@/domain/repositories'; +import { DepositTransactionRepositoryImpl } from '@/infrastructure/persistence/repositories/deposit-transaction.repository.impl'; +import { MonitoredAddressRepositoryImpl } from '@/infrastructure/persistence/repositories/monitored-address.repository.impl'; +import { BlockCheckpointRepositoryImpl } from '@/infrastructure/persistence/repositories/block-checkpoint.repository.impl'; +import { ConfirmationPolicyService } from '@/domain/services/confirmation-policy.service'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; + +@Module({ + imports: [InfrastructureModule], + providers: [ + // 仓储实现 (依赖倒置) + { + provide: DEPOSIT_TRANSACTION_REPOSITORY, + useClass: DepositTransactionRepositoryImpl, + }, + { + provide: MONITORED_ADDRESS_REPOSITORY, + useClass: MonitoredAddressRepositoryImpl, + }, + { + provide: BLOCK_CHECKPOINT_REPOSITORY, + useClass: BlockCheckpointRepositoryImpl, + }, + // 领域服务 + ConfirmationPolicyService, + ChainConfigService, + ], + exports: [ + DEPOSIT_TRANSACTION_REPOSITORY, + MONITORED_ADDRESS_REPOSITORY, + BLOCK_CHECKPOINT_REPOSITORY, + ConfirmationPolicyService, + ChainConfigService, + ], +}) +export class DomainModule {} +``` + +```typescript +// src/modules/application.module.ts +import { Module } from '@nestjs/common'; +import { DomainModule } from './domain.module'; +import { InfrastructureModule } from './infrastructure.module'; +import { DepositDetectionService } from '@/application/services/deposit-detection.service'; +import { BalanceQueryService } from '@/application/services/balance-query.service'; +import { TransactionBroadcastService } from '@/application/services/transaction-broadcast.service'; +import { AddressRegistryService } from '@/application/services/address-registry.service'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { EventListenerService } from '@/infrastructure/blockchain/event-listener.service'; +import { BlockScannerService } from '@/infrastructure/blockchain/block-scanner.service'; + +@Module({ + imports: [DomainModule, InfrastructureModule], + providers: [ + // 应用服务 + DepositDetectionService, + BalanceQueryService, + TransactionBroadcastService, + AddressRegistryService, + // 基础设施服务 (需要应用层依赖) + AddressCacheService, + EventListenerService, + BlockScannerService, + ], + exports: [ + DepositDetectionService, + BalanceQueryService, + TransactionBroadcastService, + AddressRegistryService, + AddressCacheService, + ], +}) +export class ApplicationModule {} +``` + +```typescript +// src/modules/api.module.ts +import { Module } from '@nestjs/common'; +import { ApplicationModule } from './application.module'; +import { HealthController } from '@/api/controllers/health.controller'; +import { BalanceController } from '@/api/controllers/balance.controller'; +import { InternalController } from '@/api/controllers/internal.controller'; +import { EventConsumerController } from '@/infrastructure/kafka/event-consumer.controller'; + +@Module({ + imports: [ApplicationModule], + controllers: [ + HealthController, + BalanceController, + InternalController, + EventConsumerController, + ], +}) +export class ApiModule {} +``` + +```typescript +// src/app.module.ts +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { ApiModule } from '@/modules/api.module'; +import appConfig from '@/config/app.config'; +import databaseConfig from '@/config/database.config'; +import kafkaConfig from '@/config/kafka.config'; +import redisConfig from '@/config/redis.config'; +import chainConfig from '@/config/chain.config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig, kafkaConfig, redisConfig, chainConfig], + }), + ScheduleModule.forRoot(), + ApiModule, + ], +}) +export class AppModule {} +``` + +**模块依赖关系图:** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AppModule │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ ApiModule │ │ +│ │ Controllers: Health, Balance, Internal, EventConsumer │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ imports │ +│ ┌───────────────────────▼───────────────────────────────┐ │ +│ │ ApplicationModule │ │ +│ │ Services: DepositDetection, BalanceQuery, ... │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ imports │ +│ ┌───────────────────────▼───────────────────────────────┐ │ +│ │ DomainModule │ │ +│ │ Repositories (interfaces → impl), Domain Services │ │ +│ └───────────────────────┬───────────────────────────────┘ │ +│ │ imports │ +│ ┌───────────────────────▼───────────────────────────────┐ │ +│ │ InfrastructureModule (@Global) │ │ +│ │ Prisma, Redis, Kafka, EVM Provider, External Clients │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 数据模型设计 + +### 3.1 Prisma Schema + +```prisma +// prisma/schema.prisma + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================ +// 监控地址表 +// 存储需要监听充值的地址 +// ============================================ +model MonitoredAddress { + id BigInt @id @default(autoincrement()) @map("address_id") + + chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC + address String @db.VarChar(42) // 0x地址 + + userId BigInt @map("user_id") // 关联用户ID + + isActive Boolean @default(true) @map("is_active") // 是否激活监听 + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + deposits DepositTransaction[] + + @@unique([chainType, address], name: "uk_chain_address") + @@index([userId], name: "idx_user") + @@index([chainType, isActive], name: "idx_chain_active") + @@map("monitored_addresses") +} + +// ============================================ +// 充值交易表 (Append-Only) +// 记录检测到的所有充值交易 +// ============================================ +model DepositTransaction { + id BigInt @id @default(autoincrement()) @map("deposit_id") + + chainType String @map("chain_type") @db.VarChar(20) + txHash String @unique @map("tx_hash") @db.VarChar(66) + + fromAddress String @map("from_address") @db.VarChar(42) + toAddress String @map("to_address") @db.VarChar(42) + + tokenContract String @map("token_contract") @db.VarChar(42) // USDT合约地址 + amount Decimal @db.Decimal(36, 18) // 原始金额 + amountFormatted Decimal @map("amount_formatted") @db.Decimal(20, 8) // 格式化金额 + + blockNumber BigInt @map("block_number") + blockTimestamp DateTime @map("block_timestamp") + logIndex Int @map("log_index") + + // 确认状态 + confirmations Int @default(0) + status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED + + // 关联 + addressId BigInt @map("address_id") + userId BigInt @map("user_id") + + // 通知状态 + notifiedAt DateTime? @map("notified_at") + notifyAttempts Int @default(0) @map("notify_attempts") + lastNotifyError String? @map("last_notify_error") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + monitoredAddress MonitoredAddress @relation(fields: [addressId], references: [id]) + + @@index([chainType, status], name: "idx_chain_status") + @@index([userId], name: "idx_deposit_user") + @@index([blockNumber], name: "idx_block") + @@index([status, notifiedAt], name: "idx_pending_notify") + @@map("deposit_transactions") +} + +// ============================================ +// 区块扫描检查点 (每条链一条记录) +// 记录扫描进度,用于断点续扫 +// ============================================ +model BlockCheckpoint { + id BigInt @id @default(autoincrement()) @map("checkpoint_id") + + chainType String @unique @map("chain_type") @db.VarChar(20) + + lastScannedBlock BigInt @map("last_scanned_block") + lastScannedAt DateTime @map("last_scanned_at") + + // 健康状态 + isHealthy Boolean @default(true) @map("is_healthy") + lastError String? @map("last_error") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("block_checkpoints") +} + +// ============================================ +// 交易广播请求表 +// 记录待广播和已广播的交易 +// ============================================ +model TransactionRequest { + id BigInt @id @default(autoincrement()) @map("request_id") + + chainType String @map("chain_type") @db.VarChar(20) + + // 请求来源 + sourceService String @map("source_service") @db.VarChar(50) + sourceOrderId String @map("source_order_id") @db.VarChar(100) + + // 交易数据 + fromAddress String @map("from_address") @db.VarChar(42) + toAddress String @map("to_address") @db.VarChar(42) + value Decimal @db.Decimal(36, 18) + data String? @db.Text // 合约调用数据 + + // 签名数据 (由 MPC 服务提供) + signedTx String? @map("signed_tx") @db.Text + + // 广播结果 + txHash String? @map("tx_hash") @db.VarChar(66) + status String @default("PENDING") @db.VarChar(20) // PENDING, SIGNED, BROADCASTED, CONFIRMED, FAILED + + // Gas 信息 + gasLimit BigInt? @map("gas_limit") + gasPrice Decimal? @map("gas_price") @db.Decimal(36, 18) + nonce Int? + + // 错误信息 + errorMessage String? @map("error_message") @db.Text + retryCount Int @default(0) @map("retry_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([sourceService, sourceOrderId], name: "uk_source_order") + @@index([chainType, status], name: "idx_tx_chain_status") + @@index([txHash], name: "idx_tx_hash") + @@map("transaction_requests") +} + +// ============================================ +// 区块链事件日志 (Append-Only 审计) +// ============================================ +model BlockchainEvent { + id BigInt @id @default(autoincrement()) @map("event_id") + + eventType String @map("event_type") @db.VarChar(50) + + aggregateId String @map("aggregate_id") @db.VarChar(100) + aggregateType String @map("aggregate_type") @db.VarChar(50) + + eventData Json @map("event_data") + + chainType String? @map("chain_type") @db.VarChar(20) + txHash String? @map("tx_hash") @db.VarChar(66) + + occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6) + + @@index([aggregateType, aggregateId], name: "idx_event_aggregate") + @@index([eventType], name: "idx_event_type") + @@index([chainType], name: "idx_event_chain") + @@index([occurredAt], name: "idx_event_occurred") + @@map("blockchain_events") +} +``` + +--- + +## 4. 领域层设计 + +### 4.1 聚合根基类 (与其他服务保持一致) + +为了与 authorization-service、wallet-service 等服务保持架构一致性,抽取统一的聚合根基类: + +```typescript +// src/domain/aggregates/aggregate-root.base.ts + +import { DomainEvent } from '@/domain/events/domain-event.base'; + +/** + * 聚合根基类 + * + * 所有聚合根都应继承此基类,统一管理领域事件的收集和清理。 + * 参考: authorization-service/src/domain/aggregates/aggregate-root.base.ts + */ +export abstract class AggregateRoot { + private readonly _domainEvents: DomainEvent[] = []; + + /** + * 聚合根唯一标识 + */ + abstract get id(): TId | undefined; + + /** + * 获取所有待发布的领域事件 + */ + get domainEvents(): ReadonlyArray { + return [...this._domainEvents]; + } + + /** + * 添加领域事件 + * @param event 领域事件 + */ + protected addDomainEvent(event: DomainEvent): void { + this._domainEvents.push(event); + } + + /** + * 清空领域事件(在事件发布后调用) + */ + clearDomainEvents(): void { + this._domainEvents.length = 0; + } + + /** + * 检查是否有待发布的领域事件 + */ + hasDomainEvents(): boolean { + return this._domainEvents.length > 0; + } +} +``` + +### 4.2 聚合根:DepositTransaction + +```typescript +// src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts + +import { AggregateRoot } from '../aggregate-root.base'; +import { DomainEvent, DepositDetectedEvent, DepositConfirmedEvent } from '@/domain/events'; +import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +export interface DepositTransactionProps { + id?: bigint; + chainType: ChainType; + txHash: TxHash; + fromAddress: EvmAddress; + toAddress: EvmAddress; + tokenContract: EvmAddress; + amount: TokenAmount; + blockNumber: BlockNumber; + blockTimestamp: Date; + logIndex: number; + confirmations: number; + status: DepositStatus; + addressId: bigint; + userId: bigint; + notifiedAt?: Date; + notifyAttempts: number; + lastNotifyError?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export class DepositTransaction extends AggregateRoot { + private constructor(private props: DepositTransactionProps) { + super(); + } + + // Getters + get id(): bigint | undefined { return this.props.id; } + get chainType(): ChainType { return this.props.chainType; } + get txHash(): TxHash { return this.props.txHash; } + get fromAddress(): EvmAddress { return this.props.fromAddress; } + get toAddress(): EvmAddress { return this.props.toAddress; } + get amount(): TokenAmount { return this.props.amount; } + get blockNumber(): BlockNumber { return this.props.blockNumber; } + get confirmations(): number { return this.props.confirmations; } + get status(): DepositStatus { return this.props.status; } + get userId(): bigint { return this.props.userId; } + get isConfirmed(): boolean { return this.props.status === DepositStatus.CONFIRMED; } + get isNotified(): boolean { return this.props.status === DepositStatus.NOTIFIED; } + get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } + + /** + * 创建新的充值交易(检测到时) + */ + static create(params: { + chainType: ChainType; + txHash: TxHash; + fromAddress: EvmAddress; + toAddress: EvmAddress; + tokenContract: EvmAddress; + amount: TokenAmount; + blockNumber: BlockNumber; + blockTimestamp: Date; + logIndex: number; + addressId: bigint; + userId: bigint; + }): DepositTransaction { + const deposit = new DepositTransaction({ + ...params, + confirmations: 1, + status: DepositStatus.DETECTED, + notifyAttempts: 0, + }); + + deposit.addDomainEvent(new DepositDetectedEvent({ + chainType: params.chainType.value, + txHash: params.txHash.value, + fromAddress: params.fromAddress.value, + toAddress: params.toAddress.value, + amount: params.amount.formatted, + userId: params.userId.toString(), + blockNumber: params.blockNumber.value, + })); + + return deposit; + } + + /** + * 从数据库重建 + */ + static reconstruct(props: DepositTransactionProps): DepositTransaction { + return new DepositTransaction(props); + } + + /** + * 更新确认数 + */ + updateConfirmations(newConfirmations: number, requiredConfirmations: number): void { + this.props.confirmations = newConfirmations; + + if (newConfirmations >= requiredConfirmations && this.props.status === DepositStatus.DETECTED) { + this.props.status = DepositStatus.CONFIRMING; + } + + if (newConfirmations >= requiredConfirmations && this.props.status === DepositStatus.CONFIRMING) { + this.confirm(); + } + } + + /** + * 确认充值 + */ + private confirm(): void { + if (this.props.status === DepositStatus.CONFIRMED) { + return; + } + + this.props.status = DepositStatus.CONFIRMED; + + this.addDomainEvent(new DepositConfirmedEvent({ + depositId: this.props.id!.toString(), + chainType: this.props.chainType.value, + txHash: this.props.txHash.value, + amount: this.props.amount.formatted, + userId: this.props.userId.toString(), + confirmations: this.props.confirmations, + })); + } + + /** + * 标记已通知 + */ + markNotified(): void { + this.props.status = DepositStatus.NOTIFIED; + this.props.notifiedAt = new Date(); + } + + /** + * 记录通知失败 + */ + recordNotifyFailure(error: string): void { + this.props.notifyAttempts++; + this.props.lastNotifyError = error; + } + + // 注意:addDomainEvent 和 clearDomainEvents 方法已在 AggregateRoot 基类中定义 + + toProps(): DepositTransactionProps { + return { ...this.props }; + } +} +``` + +### 4.3 值对象 + +#### 4.3.1 EVM 地址值对象 + +```typescript +// src/domain/value-objects/evm-address.vo.ts + +import { ethers } from 'ethers'; + +export class EvmAddress { + private constructor(private readonly _value: string) {} + + get value(): string { + return this._value; + } + + get checksummed(): string { + return ethers.getAddress(this._value); + } + + static create(address: string): EvmAddress { + if (!ethers.isAddress(address)) { + throw new Error(`Invalid EVM address: ${address}`); + } + return new EvmAddress(ethers.getAddress(address)); // 标准化为 checksum 格式 + } + + equals(other: EvmAddress): boolean { + return this._value.toLowerCase() === other._value.toLowerCase(); + } + + toString(): string { + return this._value; + } +} +``` + +#### 4.3.2 代币金额值对象 + +```typescript +// src/domain/value-objects/token-amount.vo.ts + +import Decimal from 'decimal.js'; + +export class TokenAmount { + private constructor( + private readonly _raw: Decimal, // 链上原始值 (wei) + private readonly _decimals: number, // 代币精度 + ) {} + + get raw(): Decimal { + return this._raw; + } + + get decimals(): number { + return this._decimals; + } + + get formatted(): string { + return this._raw.div(new Decimal(10).pow(this._decimals)).toFixed(8); + } + + get value(): number { + return parseFloat(this.formatted); + } + + static create(raw: string | bigint, decimals: number): TokenAmount { + return new TokenAmount(new Decimal(raw.toString()), decimals); + } + + static fromFormatted(amount: string, decimals: number): TokenAmount { + const raw = new Decimal(amount).mul(new Decimal(10).pow(decimals)); + return new TokenAmount(raw, decimals); + } + + isZero(): boolean { + return this._raw.isZero(); + } + + greaterThan(other: TokenAmount): boolean { + return this._raw.greaterThan(other._raw); + } +} +``` + +#### 4.3.3 链类型值对象 + +```typescript +// src/domain/value-objects/chain-type.vo.ts + +export enum ChainTypeEnum { + KAVA = 'KAVA', + BSC = 'BSC', +} + +export class ChainType { + private constructor(private readonly _value: ChainTypeEnum) {} + + get value(): ChainTypeEnum { + return this._value; + } + + get name(): string { + switch (this._value) { + case ChainTypeEnum.KAVA: return 'KAVA EVM'; + case ChainTypeEnum.BSC: return 'BSC'; + } + } + + get requiredConfirmations(): number { + switch (this._value) { + case ChainTypeEnum.KAVA: return 12; + case ChainTypeEnum.BSC: return 15; + } + } + + get blockTime(): number { + switch (this._value) { + case ChainTypeEnum.KAVA: return 6; // 约 6 秒 + case ChainTypeEnum.BSC: return 3; // 约 3 秒 + } + } + + static create(value: string): ChainType { + if (!Object.values(ChainTypeEnum).includes(value as ChainTypeEnum)) { + throw new Error(`Invalid chain type: ${value}`); + } + return new ChainType(value as ChainTypeEnum); + } + + static KAVA(): ChainType { + return new ChainType(ChainTypeEnum.KAVA); + } + + static BSC(): ChainType { + return new ChainType(ChainTypeEnum.BSC); + } + + equals(other: ChainType): boolean { + return this._value === other._value; + } +} +``` + +#### 4.3.4 区块号值对象 + +```typescript +// src/domain/value-objects/block-number.vo.ts + +/** + * 区块号值对象 + * + * 封装区块号,确保类型安全和业务规则验证 + */ +export class BlockNumber { + private constructor(private readonly _value: bigint) {} + + get value(): bigint { + return this._value; + } + + /** + * 创建区块号值对象 + * @param value 区块号(支持 number 或 bigint) + * @throws Error 如果区块号为负数 + */ + static create(value: number | bigint): BlockNumber { + const bn = BigInt(value); + if (bn < 0n) { + throw new Error(`Invalid block number: ${value}. Block number cannot be negative.`); + } + return new BlockNumber(bn); + } + + /** + * 计算两个区块之间的差值 + */ + diff(other: BlockNumber): bigint { + return this._value - other._value; + } + + /** + * 检查是否大于另一个区块号 + */ + greaterThan(other: BlockNumber): boolean { + return this._value > other._value; + } + + /** + * 检查是否小于另一个区块号 + */ + lessThan(other: BlockNumber): boolean { + return this._value < other._value; + } + + /** + * 加上指定数量的区块 + */ + add(blocks: number | bigint): BlockNumber { + return new BlockNumber(this._value + BigInt(blocks)); + } + + /** + * 减去指定数量的区块 + */ + subtract(blocks: number | bigint): BlockNumber { + const result = this._value - BigInt(blocks); + if (result < 0n) { + throw new Error('Block number cannot be negative after subtraction'); + } + return new BlockNumber(result); + } + + equals(other: BlockNumber): boolean { + return this._value === other._value; + } + + toString(): string { + return this._value.toString(); + } + + toNumber(): number { + return Number(this._value); + } +} +``` + +#### 4.3.5 交易哈希值对象 + +```typescript +// src/domain/value-objects/tx-hash.vo.ts + +/** + * 交易哈希值对象 + * + * 封装 EVM 交易哈希,确保格式正确(0x + 64位十六进制字符) + */ +export class TxHash { + private static readonly PATTERN = /^0x[a-fA-F0-9]{64}$/; + + private constructor(private readonly _value: string) {} + + get value(): string { + return this._value; + } + + /** + * 获取标准化(小写)的哈希值 + */ + get normalized(): string { + return this._value.toLowerCase(); + } + + /** + * 创建交易哈希值对象 + * @param hash 交易哈希字符串 + * @throws Error 如果哈希格式无效 + */ + static create(hash: string): TxHash { + if (!this.isValid(hash)) { + throw new Error(`Invalid transaction hash: ${hash}. Expected format: 0x + 64 hex characters.`); + } + // 统一存储为小写格式 + return new TxHash(hash.toLowerCase()); + } + + /** + * 验证交易哈希格式是否有效 + */ + static isValid(hash: string): boolean { + return this.PATTERN.test(hash); + } + + /** + * 获取缩短显示格式 (0x1234...abcd) + */ + toShortString(prefixLength = 6, suffixLength = 4): string { + return `${this._value.slice(0, prefixLength + 2)}...${this._value.slice(-suffixLength)}`; + } + + equals(other: TxHash): boolean { + return this._value === other._value; + } + + toString(): string { + return this._value; + } +} +``` + +#### 4.3.6 值对象导出索引 + +```typescript +// src/domain/value-objects/index.ts + +export { ChainType, ChainTypeEnum } from './chain-type.vo'; +export { EvmAddress } from './evm-address.vo'; +export { TokenAmount } from './token-amount.vo'; +export { BlockNumber } from './block-number.vo'; +export { TxHash } from './tx-hash.vo'; +``` + +### 4.4 领域事件 + +### 4.6 WalletAddressCreated 事件 + +```typescript +// src/domain/events/wallet-address-created.event.ts + +import { DomainEvent } from './domain-event.base'; + +export interface WalletAddressCreatedPayload { + userId: string; + username: string; + publicKey: string; // 压缩公钥 (33 bytes hex) + addresses: Array<{ + chainType: string; // BSC, KAVA, DST + address: string; // 派生的地址 + addressType: string; // EVM 或 COSMOS + }>; + mpcSessionId: string; // MPC 会话 ID + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; +} + +export class WalletAddressCreatedEvent extends DomainEvent { + constructor(public readonly payload: WalletAddressCreatedPayload) { + super(); + } + + get eventType(): string { + return 'WalletAddressCreated'; + } + + get aggregateId(): string { + return this.payload.userId; + } + + get aggregateType(): string { + return 'WalletAddress'; + } +} +``` + +--- + +```typescript +// src/domain/events/deposit-detected.event.ts + +import { DomainEvent } from './domain-event.base'; + +export interface DepositDetectedPayload { + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + amount: string; + userId: string; + blockNumber: number; +} + +export class DepositDetectedEvent extends DomainEvent { + constructor(public readonly payload: DepositDetectedPayload) { + super(); + } + + get eventType(): string { + return 'DepositDetected'; + } + + get aggregateId(): string { + return this.payload.txHash; + } + + get aggregateType(): string { + return 'DepositTransaction'; + } +} +``` + +```typescript +// src/domain/events/deposit-confirmed.event.ts + +import { DomainEvent } from './domain-event.base'; + +export interface DepositConfirmedPayload { + depositId: string; + chainType: string; + txHash: string; + amount: string; + userId: string; + confirmations: number; +} + +export class DepositConfirmedEvent extends DomainEvent { + constructor(public readonly payload: DepositConfirmedPayload) { + super(); + } + + get eventType(): string { + return 'DepositConfirmed'; + } + + get aggregateId(): string { + return this.payload.depositId; + } + + get aggregateType(): string { + return 'DepositTransaction'; + } +} +``` + +### 4.5 仓储接口 (Ports) + +```typescript +// src/domain/repositories/deposit-transaction.repository.interface.ts + +import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +export interface IDepositTransactionRepository { + save(deposit: DepositTransaction): Promise; + findById(id: bigint): Promise; + findByTxHash(txHash: TxHash): Promise; + existsByTxHash(txHash: TxHash): Promise; + findByStatus(status: DepositStatus): Promise; + findPendingNotification(limit?: number): Promise; + findByUserIdAndChain(userId: bigint, chainType: ChainType): Promise; + findUnconfirmed(chainType: ChainType, minBlockNumber: bigint): Promise; +} + +export const DEPOSIT_TRANSACTION_REPOSITORY = Symbol('DEPOSIT_TRANSACTION_REPOSITORY'); +``` + +```typescript +// src/domain/repositories/monitored-address.repository.interface.ts + +import { ChainType, EvmAddress } from '@/domain/value-objects'; + +export interface MonitoredAddressData { + id: bigint; + chainType: ChainType; + address: EvmAddress; + userId: bigint; + isActive: boolean; + createdAt: Date; +} + +export interface IMonitoredAddressRepository { + save(data: Omit): Promise; + findByAddress(chainType: ChainType, address: EvmAddress): Promise; + findByUserId(userId: bigint): Promise; + findActiveByChain(chainType: ChainType): Promise; + getAllActiveAddresses(): Promise>; // 返回所有激活地址的 Set + deactivate(id: bigint): Promise; +} + +export const MONITORED_ADDRESS_REPOSITORY = Symbol('MONITORED_ADDRESS_REPOSITORY'); +``` + +--- + +## 5. 基础设施层设计 + +### 5.1 EVM Provider 适配器 + +```typescript +// src/infrastructure/blockchain/evm-provider.adapter.ts + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ethers, JsonRpcProvider, WebSocketProvider, Contract } from 'ethers'; +import { ChainTypeEnum } from '@/domain/value-objects'; + +interface ChainConfig { + rpcUrl: string; + wsUrl?: string; + usdtContract: string; + decimals: number; + chainId: number; +} + +const ERC20_ABI = [ + 'event Transfer(address indexed from, address indexed to, uint256 value)', + 'function balanceOf(address owner) view returns (uint256)', + 'function decimals() view returns (uint8)', +]; + +@Injectable() +export class EvmProviderAdapter implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(EvmProviderAdapter.name); + + private rpcProviders: Map = new Map(); + private wsProviders: Map = new Map(); + private contracts: Map = new Map(); + + private readonly chainConfigs: Record = { + [ChainTypeEnum.KAVA]: { + rpcUrl: this.configService.get('KAVA_RPC_URL', 'https://evm.kava.io'), + wsUrl: this.configService.get('KAVA_WS_URL'), + usdtContract: '0x919C1c267BC06a7039e03fcc2eF738525769109c', + decimals: 6, + chainId: 2222, + }, + [ChainTypeEnum.BSC]: { + rpcUrl: this.configService.get('BSC_RPC_URL', 'https://bsc-dataseed.binance.org'), + wsUrl: this.configService.get('BSC_WS_URL'), + usdtContract: '0x55d398326f99059fF775485246999027B3197955', + decimals: 18, + chainId: 56, + }, + }; + + constructor(private readonly configService: ConfigService) {} + + async onModuleInit(): Promise { + await this.initializeProviders(); + } + + async onModuleDestroy(): Promise { + await this.closeProviders(); + } + + private async initializeProviders(): Promise { + for (const [chainType, config] of Object.entries(this.chainConfigs)) { + try { + // RPC Provider (用于查询) + const rpcProvider = new JsonRpcProvider(config.rpcUrl, config.chainId, { + staticNetwork: true, + }); + this.rpcProviders.set(chainType as ChainTypeEnum, rpcProvider); + + // WebSocket Provider (用于事件监听) + if (config.wsUrl) { + const wsProvider = new WebSocketProvider(config.wsUrl, config.chainId); + this.wsProviders.set(chainType as ChainTypeEnum, wsProvider); + } + + // USDT Contract + const provider = config.wsUrl + ? this.wsProviders.get(chainType as ChainTypeEnum)! + : rpcProvider; + const contract = new Contract(config.usdtContract, ERC20_ABI, provider); + this.contracts.set(chainType as ChainTypeEnum, contract); + + this.logger.log(`Initialized ${chainType} provider`); + } catch (error) { + this.logger.error(`Failed to initialize ${chainType} provider: ${error.message}`); + } + } + } + + private async closeProviders(): Promise { + for (const wsProvider of this.wsProviders.values()) { + await wsProvider.destroy(); + } + } + + getRpcProvider(chainType: ChainTypeEnum): JsonRpcProvider { + const provider = this.rpcProviders.get(chainType); + if (!provider) { + throw new Error(`No RPC provider for chain: ${chainType}`); + } + return provider; + } + + getWsProvider(chainType: ChainTypeEnum): WebSocketProvider | undefined { + return this.wsProviders.get(chainType); + } + + getUsdtContract(chainType: ChainTypeEnum): Contract { + const contract = this.contracts.get(chainType); + if (!contract) { + throw new Error(`No USDT contract for chain: ${chainType}`); + } + return contract; + } + + getChainConfig(chainType: ChainTypeEnum): ChainConfig { + return this.chainConfigs[chainType]; + } + + async getCurrentBlockNumber(chainType: ChainTypeEnum): Promise { + const provider = this.getRpcProvider(chainType); + const blockNumber = await provider.getBlockNumber(); + return BigInt(blockNumber); + } + + async getBalance(chainType: ChainTypeEnum, address: string): Promise { + const contract = this.getUsdtContract(chainType); + const config = this.getChainConfig(chainType); + const balance = await contract.balanceOf(address); + return ethers.formatUnits(balance, config.decimals); + } +} +``` + +### 5.2 事件监听器服务 + +```typescript +// src/infrastructure/blockchain/event-listener.service.ts + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { EvmProviderAdapter } from './evm-provider.adapter'; +import { DepositDetectionService } from '@/application/services/deposit-detection.service'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { ChainTypeEnum } from '@/domain/value-objects'; +import { Contract, EventLog } from 'ethers'; + +@Injectable() +export class EventListenerService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(EventListenerService.name); + private isListening = false; + private listeners: Map = new Map(); + + constructor( + private readonly evmProvider: EvmProviderAdapter, + private readonly depositService: DepositDetectionService, + private readonly addressCache: AddressCacheService, + ) {} + + async onModuleInit(): Promise { + await this.startListening(); + } + + async onModuleDestroy(): Promise { + await this.stopListening(); + } + + async startListening(): Promise { + if (this.isListening) return; + + for (const chainType of Object.values(ChainTypeEnum)) { + await this.subscribeToChain(chainType); + } + + this.isListening = true; + this.logger.log('Event listeners started'); + } + + private async subscribeToChain(chainType: ChainTypeEnum): Promise { + const wsProvider = this.evmProvider.getWsProvider(chainType); + if (!wsProvider) { + this.logger.warn(`No WebSocket provider for ${chainType}, using polling`); + return; + } + + const contract = this.evmProvider.getUsdtContract(chainType); + + // 监听所有 Transfer 事件 + const filter = contract.filters.Transfer(); + + const listener = async (from: string, to: string, value: bigint, event: EventLog) => { + try { + // 检查目标地址是否是平台监控地址 + const isMonitored = await this.addressCache.isMonitoredAddress(chainType, to); + if (!isMonitored) return; + + this.logger.log(`Detected deposit: ${chainType} ${event.transactionHash}`); + + await this.depositService.handleTransferEvent({ + chainType, + txHash: event.transactionHash, + fromAddress: from, + toAddress: to, + amount: value.toString(), + blockNumber: event.blockNumber, + logIndex: event.index, + }); + } catch (error) { + this.logger.error(`Error processing Transfer event: ${error.message}`); + } + }; + + contract.on(filter, listener); + this.listeners.set(chainType, { contract, filter, listener }); + + this.logger.log(`Subscribed to ${chainType} Transfer events`); + } + + async stopListening(): Promise { + for (const [chainType, { contract, filter, listener }] of this.listeners) { + contract.off(filter, listener); + this.logger.log(`Unsubscribed from ${chainType}`); + } + this.listeners.clear(); + this.isListening = false; + } +} +``` + +### 5.3 地址派生适配器 + +```typescript +// src/infrastructure/blockchain/address-derivation.adapter.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { createHash } from 'crypto'; +import { bech32 } from 'bech32'; +import { ethers } from 'ethers'; +import * as secp256k1 from 'secp256k1'; + +export enum AddressType { + EVM = 'EVM', + COSMOS = 'COSMOS', +} + +export interface DerivedAddress { + chainType: string; + address: string; + addressType: AddressType; +} + +/** + * 地址派生适配器 + * + * 从压缩的 secp256k1 公钥派生多链钱包地址 + * - EVM 链 (BSC, KAVA EVM): 使用 keccak256(uncompressed_pubkey[1:])[-20:] + * - Cosmos 链 (KAVA, DST): 使用 bech32(ripemd160(sha256(compressed_pubkey))) + */ +@Injectable() +export class AddressDerivationAdapter { + private readonly logger = new Logger(AddressDerivationAdapter.name); + + /** + * 从压缩公钥派生所有支持链的地址 + * @param compressedPubKeyHex 33 字节压缩公钥 (hex 格式,不含 0x 前缀) + */ + deriveAllAddresses(compressedPubKeyHex: string): DerivedAddress[] { + const pubKeyBytes = Buffer.from(compressedPubKeyHex, 'hex'); + + if (pubKeyBytes.length !== 33) { + throw new Error(`Invalid compressed public key length: ${pubKeyBytes.length}, expected 33`); + } + + const addresses: DerivedAddress[] = []; + + // EVM 地址 (BSC, KAVA EVM) + const evmAddress = this.deriveEvmAddress(pubKeyBytes); + addresses.push({ chainType: 'BSC', address: evmAddress, addressType: AddressType.EVM }); + addresses.push({ chainType: 'KAVA', address: evmAddress, addressType: AddressType.EVM }); + + // Cosmos 地址 (KAVA Native 使用 kava 前缀,DST 使用 dst 前缀) + const kavaCosmosAddress = this.deriveCosmosAddress(pubKeyBytes, 'kava'); + const dstAddress = this.deriveCosmosAddress(pubKeyBytes, 'dst'); + addresses.push({ chainType: 'KAVA_COSMOS', address: kavaCosmosAddress, addressType: AddressType.COSMOS }); + addresses.push({ chainType: 'DST', address: dstAddress, addressType: AddressType.COSMOS }); + + this.logger.log(`Derived ${addresses.length} addresses from public key`); + return addresses; + } + + /** + * 派生 EVM 地址 + * 1. 解压公钥 (33 bytes → 65 bytes) + * 2. 取非压缩公钥去掉前缀 (64 bytes) + * 3. keccak256 哈希后取后 20 bytes + */ + private deriveEvmAddress(compressedPubKey: Buffer): string { + // 使用 secp256k1 解压公钥 + const uncompressedPubKey = Buffer.from( + secp256k1.publicKeyConvert(compressedPubKey, false) + ); + + // 去掉 0x04 前缀,取 64 bytes + const pubKeyWithoutPrefix = uncompressedPubKey.slice(1); + + // keccak256 哈希后取后 20 bytes + const hash = ethers.keccak256(pubKeyWithoutPrefix); + const address = '0x' + hash.slice(-40); + + return ethers.getAddress(address); // 返回 checksum 格式 + } + + /** + * 派生 Cosmos 地址 + * 1. SHA256(compressed_pubkey) + * 2. RIPEMD160(sha256_result) + * 3. bech32 编码 + */ + private deriveCosmosAddress(compressedPubKey: Buffer, prefix: string): string { + // SHA256 + const sha256Hash = createHash('sha256').update(compressedPubKey).digest(); + + // RIPEMD160 + const ripemd160Hash = createHash('ripemd160').update(sha256Hash).digest(); + + // Bech32 编码 + const words = bech32.toWords(ripemd160Hash); + return bech32.encode(prefix, words); + } + + /** + * 验证地址格式 + */ + validateAddress(chainType: string, address: string): boolean { + switch (chainType) { + case 'BSC': + case 'KAVA': + return ethers.isAddress(address); + case 'KAVA_COSMOS': + return /^kava1[a-z0-9]{38}$/.test(address); + case 'DST': + return /^dst1[a-z0-9]{38}$/.test(address); + default: + return false; + } + } +} +``` + +### 5.4 区块扫描器(补扫服务) + +```typescript +// src/infrastructure/blockchain/block-scanner.service.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { EvmProviderAdapter } from './evm-provider.adapter'; +import { DepositDetectionService } from '@/application/services/deposit-detection.service'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { BlockCheckpointRepository } from '@/infrastructure/persistence/repositories/block-checkpoint.repository.impl'; +import { ChainTypeEnum } from '@/domain/value-objects'; +import { ethers } from 'ethers'; + +const BATCH_SIZE = 1000; // 每次扫描的区块数 + +@Injectable() +export class BlockScannerService { + private readonly logger = new Logger(BlockScannerService.name); + private isScanning: Map = new Map(); + + constructor( + private readonly evmProvider: EvmProviderAdapter, + private readonly depositService: DepositDetectionService, + private readonly addressCache: AddressCacheService, + private readonly checkpointRepo: BlockCheckpointRepository, + ) {} + + /** + * 定时补扫任务 (每 5 分钟) + */ + @Cron(CronExpression.EVERY_5_MINUTES) + async scheduledScan(): Promise { + for (const chainType of Object.values(ChainTypeEnum)) { + await this.scanChain(chainType); + } + } + + /** + * 扫描指定链的区块 + */ + async scanChain(chainType: ChainTypeEnum): Promise { + if (this.isScanning.get(chainType)) { + this.logger.debug(`${chainType} scan already in progress`); + return; + } + + this.isScanning.set(chainType, true); + + try { + const checkpoint = await this.checkpointRepo.findByChain(chainType); + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + + let fromBlock = checkpoint + ? checkpoint.lastScannedBlock + 1n + : currentBlock - 1000n; // 首次启动扫描最近 1000 块 + + while (fromBlock <= currentBlock) { + const toBlock = fromBlock + BigInt(BATCH_SIZE) - 1n > currentBlock + ? currentBlock + : fromBlock + BigInt(BATCH_SIZE) - 1n; + + await this.scanBlockRange(chainType, fromBlock, toBlock); + + // 更新检查点 + await this.checkpointRepo.updateCheckpoint(chainType, toBlock); + + fromBlock = toBlock + 1n; + } + + this.logger.debug(`${chainType} scan completed up to block ${currentBlock}`); + } catch (error) { + this.logger.error(`${chainType} scan failed: ${error.message}`); + await this.checkpointRepo.markUnhealthy(chainType, error.message); + } finally { + this.isScanning.set(chainType, false); + } + } + + /** + * 扫描指定区块范围 + */ + private async scanBlockRange( + chainType: ChainTypeEnum, + fromBlock: bigint, + toBlock: bigint, + ): Promise { + const contract = this.evmProvider.getUsdtContract(chainType); + const monitoredAddresses = await this.addressCache.getMonitoredAddresses(chainType); + + if (monitoredAddresses.size === 0) { + return; + } + + // 查询 Transfer 事件 + const filter = contract.filters.Transfer(null, [...monitoredAddresses]); + const events = await contract.queryFilter(filter, Number(fromBlock), Number(toBlock)); + + for (const event of events) { + if (!('args' in event)) continue; + + const [from, to, value] = event.args; + + try { + await this.depositService.handleTransferEvent({ + chainType, + txHash: event.transactionHash, + fromAddress: from, + toAddress: to, + amount: value.toString(), + blockNumber: event.blockNumber, + logIndex: event.index, + }); + } catch (error) { + // 重复交易等预期错误,忽略 + if (!error.message.includes('already exists')) { + this.logger.error(`Error processing event: ${error.message}`); + } + } + } + } + + /** + * 手动触发全量扫描 + */ + async fullScan(chainType: ChainTypeEnum, fromBlock: bigint, toBlock: bigint): Promise { + this.logger.log(`Starting full scan for ${chainType} from ${fromBlock} to ${toBlock}`); + await this.scanBlockRange(chainType, fromBlock, toBlock); + this.logger.log(`Full scan completed for ${chainType}`); + } +} +``` + +### 5.4 地址缓存服务 + +```typescript +// src/infrastructure/redis/address-cache.service.ts + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { RedisService } from './redis.service'; +import { IMonitoredAddressRepository, MONITORED_ADDRESS_REPOSITORY } from '@/domain/repositories'; +import { Inject } from '@nestjs/common'; +import { ChainTypeEnum } from '@/domain/value-objects'; + +const CACHE_KEY_PREFIX = 'blockchain:addresses:'; +const CACHE_TTL = 300; // 5 分钟 + +@Injectable() +export class AddressCacheService implements OnModuleInit { + private readonly logger = new Logger(AddressCacheService.name); + private localCache: Map> = new Map(); + + constructor( + private readonly redis: RedisService, + @Inject(MONITORED_ADDRESS_REPOSITORY) + private readonly addressRepo: IMonitoredAddressRepository, + ) {} + + async onModuleInit(): Promise { + await this.refreshCache(); + } + + /** + * 检查地址是否被监控 + */ + async isMonitoredAddress(chainType: ChainTypeEnum, address: string): Promise { + const normalizedAddress = address.toLowerCase(); + + // 先查本地缓存 + const localSet = this.localCache.get(chainType); + if (localSet?.has(normalizedAddress)) { + return true; + } + + // 再查 Redis + const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; + const exists = await this.redis.sismember(cacheKey, normalizedAddress); + + if (exists) { + // 更新本地缓存 + if (!localSet) { + this.localCache.set(chainType, new Set([normalizedAddress])); + } else { + localSet.add(normalizedAddress); + } + } + + return exists; + } + + /** + * 获取链的所有监控地址 + */ + async getMonitoredAddresses(chainType: ChainTypeEnum): Promise> { + const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; + const addresses = await this.redis.smembers(cacheKey); + return new Set(addresses); + } + + /** + * 添加监控地址 + */ + async addAddress(chainType: ChainTypeEnum, address: string): Promise { + const normalizedAddress = address.toLowerCase(); + const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; + + await this.redis.sadd(cacheKey, normalizedAddress); + + // 更新本地缓存 + let localSet = this.localCache.get(chainType); + if (!localSet) { + localSet = new Set(); + this.localCache.set(chainType, localSet); + } + localSet.add(normalizedAddress); + } + + /** + * 移除监控地址 + */ + async removeAddress(chainType: ChainTypeEnum, address: string): Promise { + const normalizedAddress = address.toLowerCase(); + const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; + + await this.redis.srem(cacheKey, normalizedAddress); + + // 更新本地缓存 + this.localCache.get(chainType)?.delete(normalizedAddress); + } + + /** + * 从数据库刷新缓存 + */ + async refreshCache(): Promise { + this.logger.log('Refreshing address cache from database...'); + + for (const chainType of Object.values(ChainTypeEnum)) { + const addresses = await this.addressRepo.findActiveByChain( + { value: chainType } as any + ); + + const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; + const addressSet = new Set(addresses.map(a => a.address.value.toLowerCase())); + + // 更新 Redis + if (addressSet.size > 0) { + await this.redis.del(cacheKey); + await this.redis.sadd(cacheKey, ...addressSet); + } + + // 更新本地缓存 + this.localCache.set(chainType, addressSet); + + this.logger.log(`Loaded ${addressSet.size} addresses for ${chainType}`); + } + } +} +``` + +--- + +## 6. 应用层设计 + +### 6.1 充值检测服务 + +```typescript +// src/application/services/deposit-detection.service.ts + +import { Injectable, Inject, Logger } from '@nestjs/common'; +import { + IDepositTransactionRepository, + DEPOSIT_TRANSACTION_REPOSITORY, + IMonitoredAddressRepository, + MONITORED_ADDRESS_REPOSITORY, +} from '@/domain/repositories'; +import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber, ChainTypeEnum } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { WalletClientService } from '@/infrastructure/external/wallet-service/wallet-client.service'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; + +export interface TransferEventData { + chainType: ChainTypeEnum; + txHash: string; + fromAddress: string; + toAddress: string; + amount: string; + blockNumber: number; + logIndex: number; +} + +@Injectable() +export class DepositDetectionService { + private readonly logger = new Logger(DepositDetectionService.name); + + constructor( + @Inject(DEPOSIT_TRANSACTION_REPOSITORY) + private readonly depositRepo: IDepositTransactionRepository, + @Inject(MONITORED_ADDRESS_REPOSITORY) + private readonly addressRepo: IMonitoredAddressRepository, + private readonly eventPublisher: EventPublisherService, + private readonly walletClient: WalletClientService, + private readonly evmProvider: EvmProviderAdapter, + ) {} + + /** + * 处理检测到的 Transfer 事件 + */ + async handleTransferEvent(data: TransferEventData): Promise { + const txHash = TxHash.create(data.txHash); + + // 检查是否已处理 + const exists = await this.depositRepo.existsByTxHash(txHash); + if (exists) { + this.logger.debug(`Deposit ${data.txHash} already exists, skipping`); + return; + } + + // 查找监控地址信息 + const chainType = ChainType.create(data.chainType); + const toAddress = EvmAddress.create(data.toAddress); + const addressInfo = await this.addressRepo.findByAddress(chainType, toAddress); + + if (!addressInfo) { + this.logger.warn(`Address ${data.toAddress} not found in monitored addresses`); + return; + } + + // 获取区块时间 + const block = await this.evmProvider.getRpcProvider(data.chainType).getBlock(data.blockNumber); + const blockTimestamp = new Date(block!.timestamp * 1000); + + // 获取代币精度 + const chainConfig = this.evmProvider.getChainConfig(data.chainType); + + // 创建充值记录 + const deposit = DepositTransaction.create({ + chainType, + txHash, + fromAddress: EvmAddress.create(data.fromAddress), + toAddress, + tokenContract: EvmAddress.create(chainConfig.usdtContract), + amount: TokenAmount.create(data.amount, chainConfig.decimals), + blockNumber: BlockNumber.create(data.blockNumber), + blockTimestamp, + logIndex: data.logIndex, + addressId: addressInfo.id, + userId: addressInfo.userId, + }); + + // 保存并发布事件 + await this.depositRepo.save(deposit); + await this.eventPublisher.publishAll(deposit.domainEvents); + deposit.clearDomainEvents(); + + this.logger.log(`Deposit detected: ${data.txHash}, amount: ${deposit.amount.formatted}`); + + // 检查确认数 + await this.checkConfirmationsAndNotify(deposit); + } + + /** + * 检查确认数并通知 wallet-service + */ + async checkConfirmationsAndNotify(deposit: DepositTransaction): Promise { + const chainType = deposit.chainType; + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType.value as ChainTypeEnum); + const confirmations = Number(currentBlock - deposit.blockNumber.value) + 1; + const requiredConfirmations = chainType.requiredConfirmations; + + deposit.updateConfirmations(confirmations, requiredConfirmations); + + if (deposit.isConfirmed && !deposit.isNotified) { + try { + // 调用 wallet-service 入账 + await this.walletClient.handleDeposit({ + userId: deposit.userId.toString(), + amount: deposit.amount.value, + chainType: deposit.chainType.value, + txHash: deposit.txHash.value, + }); + + deposit.markNotified(); + this.logger.log(`Deposit notified: ${deposit.txHash.value}`); + } catch (error) { + deposit.recordNotifyFailure(error.message); + this.logger.error(`Failed to notify deposit: ${error.message}`); + } + } + + await this.depositRepo.save(deposit); + await this.eventPublisher.publishAll(deposit.domainEvents); + deposit.clearDomainEvents(); + } + + /** + * 定时任务:更新未确认交易的确认数 + */ + async updatePendingConfirmations(): Promise { + for (const chainType of Object.values(ChainTypeEnum)) { + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + const minBlock = currentBlock - 100n; // 只处理最近 100 块内的 + + const pendingDeposits = await this.depositRepo.findUnconfirmed( + ChainType.create(chainType), + minBlock, + ); + + for (const deposit of pendingDeposits) { + await this.checkConfirmationsAndNotify(deposit); + } + } + } + + /** + * 定时任务:重试失败的通知 + */ + async retryFailedNotifications(): Promise { + const pendingNotifications = await this.depositRepo.findPendingNotification(50); + + for (const deposit of pendingNotifications) { + if (deposit.toProps().notifyAttempts >= 10) { + this.logger.error(`Deposit ${deposit.txHash.value} exceeded max retry attempts`); + continue; + } + + await this.checkConfirmationsAndNotify(deposit); + } + } +} +``` + +### 6.2 余额查询服务 + +```typescript +// src/application/services/balance-query.service.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { ChainTypeEnum } from '@/domain/value-objects'; + +export interface BalanceResult { + chainType: string; + address: string; + usdtBalance: string; + nativeBalance: string; +} + +@Injectable() +export class BalanceQueryService { + private readonly logger = new Logger(BalanceQueryService.name); + + constructor(private readonly evmProvider: EvmProviderAdapter) {} + + /** + * 查询单个地址的余额 + */ + async getBalance(chainType: ChainTypeEnum, address: string): Promise { + const [usdtBalance, nativeBalance] = await Promise.all([ + this.evmProvider.getBalance(chainType, address), + this.getNativeBalance(chainType, address), + ]); + + return { + chainType, + address, + usdtBalance, + nativeBalance, + }; + } + + /** + * 批量查询余额 + */ + async getBatchBalances( + addresses: Array<{ chainType: ChainTypeEnum; address: string }>, + ): Promise { + return Promise.all( + addresses.map(({ chainType, address }) => this.getBalance(chainType, address)), + ); + } + + private async getNativeBalance(chainType: ChainTypeEnum, address: string): Promise { + const provider = this.evmProvider.getRpcProvider(chainType); + const balance = await provider.getBalance(address); + const { ethers } = await import('ethers'); + return ethers.formatEther(balance); + } +} +``` + +--- + +## 7. Kafka 事件设计 + +### 7.1 发布的事件 + +```typescript +// src/infrastructure/kafka/event-publisher.service.ts + +export const BLOCKCHAIN_TOPICS = { + // 地址派生完成事件 (发给 identity-service, wallet-service) + WALLET_ADDRESS_CREATED: 'blockchain.WalletAddressCreated', + + // 充值相关事件 + DEPOSIT_DETECTED: 'blockchain.DepositDetected', + DEPOSIT_CONFIRMED: 'blockchain.DepositConfirmed', + + // 交易相关事件 + TRANSACTION_BROADCASTED: 'blockchain.TransactionBroadcasted', + TRANSACTION_CONFIRMED: 'blockchain.TransactionConfirmed', + + // 区块扫描事件 + BLOCK_SCANNED: 'blockchain.BlockScanned', +} as const; +``` + +### 7.2 消费的事件 + +```typescript +// src/infrastructure/kafka/mpc-event-consumer.service.ts + +// 消费 MPC 服务发布的事件 +export const MPC_CONSUME_TOPICS = { + KEYGEN_COMPLETED: 'mpc.KeygenCompleted', + SESSION_FAILED: 'mpc.SessionFailed', +} as const; + +export interface KeygenCompletedPayload { + sessionId: string; + partyId: string; + publicKey: string; // 33 bytes 压缩公钥 (hex) + shareId: string; + threshold: string; // "2-of-3" + extraPayload?: { + userId: string; + username: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; + }; +} +``` + +### 7.3 MPC Keygen 完成事件处理 + +```typescript +// src/application/event-handlers/mpc-keygen-completed.handler.ts + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { MpcEventConsumerService, KeygenCompletedPayload } from '@/infrastructure/kafka/mpc-event-consumer.service'; +import { AddressDerivationAdapter } from '@/infrastructure/blockchain/address-derivation.adapter'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { WalletAddressCreatedEvent } from '@/domain/events'; + +@Injectable() +export class MpcKeygenCompletedHandler implements OnModuleInit { + private readonly logger = new Logger(MpcKeygenCompletedHandler.name); + + constructor( + private readonly mpcEventConsumer: MpcEventConsumerService, + private readonly addressDerivation: AddressDerivationAdapter, + private readonly eventPublisher: EventPublisherService, + ) {} + + async onModuleInit() { + this.mpcEventConsumer.onKeygenCompleted(this.handleKeygenCompleted.bind(this)); + this.logger.log('Registered KeygenCompleted event handler'); + } + + private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise { + const userId = payload.extraPayload?.userId; + const username = payload.extraPayload?.username; + const publicKey = payload.publicKey; + + if (!userId || !username || !publicKey) { + this.logger.error('Missing required fields in KeygenCompleted payload'); + return; + } + + this.logger.log(`[DERIVE] Processing keygen completion for userId=${userId}`); + this.logger.log(`[DERIVE] PublicKey: ${publicKey.substring(0, 20)}...`); + + try { + // 从公钥派生所有链的地址 + const addresses = this.addressDerivation.deriveAllAddresses(publicKey); + + this.logger.log(`[DERIVE] Derived ${addresses.length} addresses:`); + addresses.forEach(addr => { + this.logger.log(`[DERIVE] ${addr.chainType}: ${addr.address}`); + }); + + // 发布 WalletAddressCreated 事件 + const event = new WalletAddressCreatedEvent({ + userId, + username, + publicKey, + addresses: addresses.map(a => ({ + chainType: a.chainType, + address: a.address, + addressType: a.addressType, + })), + mpcSessionId: payload.sessionId, + delegateShare: payload.extraPayload?.delegateShare, + }); + + await this.eventPublisher.publish(event); + this.logger.log(`[DERIVE] Published WalletAddressCreated event for userId=${userId}`); + + } catch (error) { + this.logger.error(`[DERIVE] Failed to derive addresses: ${error.message}`, error.stack); + } + } +} +``` + +### 7.4 消费的 Identity/Wallet 事件 + +```typescript +// src/infrastructure/kafka/event-consumer.controller.ts + +import { Controller, Logger } from '@nestjs/common'; +import { MessagePattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; +import { AddressRegistryService } from '@/application/services/address-registry.service'; + +@Controller() +export class EventConsumerController { + private readonly logger = new Logger(EventConsumerController.name); + + constructor(private readonly addressRegistry: AddressRegistryService) {} + + /** + * 监听用户创建事件,注册监控地址 + */ + @MessagePattern('identity.UserAccountCreated') + async handleUserCreated( + @Payload() message: any, + @Ctx() context: KafkaContext, + ): Promise { + this.logger.log(`Received UserAccountCreated: ${message.payload.userId}`); + + // 注册用户的钱包地址到监控列表 + // 注意:地址信息可能在后续的 WalletBound 事件中 + } + + /** + * 监听钱包绑定事件,注册监控地址 + */ + @MessagePattern('identity.WalletBound') + async handleWalletBound( + @Payload() message: any, + @Ctx() context: KafkaContext, + ): Promise { + this.logger.log(`Received WalletBound: userId=${message.payload.userId}`); + + await this.addressRegistry.registerAddress({ + userId: BigInt(message.payload.userId), + chainType: message.payload.chainType, + address: message.payload.address, + }); + } + + /** + * 监听多钱包绑定事件 + */ + @MessagePattern('identity.MultipleWalletsBound') + async handleMultipleWalletsBound( + @Payload() message: any, + @Ctx() context: KafkaContext, + ): Promise { + this.logger.log(`Received MultipleWalletsBound: userId=${message.payload.userId}`); + + for (const wallet of message.payload.wallets) { + await this.addressRegistry.registerAddress({ + userId: BigInt(message.payload.userId), + chainType: wallet.chainType, + address: wallet.address, + }); + } + } +} +``` + +--- + +## 8. API 设计 + +### 8.1 健康检查 + +```typescript +// src/api/controllers/health.controller.ts + +@Controller('blockchain/health') +export class HealthController { + @Get() + @Public() + async check(): Promise<{ status: string; chains: Record }> { + // 检查各链连接状态 + } +} +``` + +### 8.2 余额查询 (内部 API) + +```typescript +// src/api/controllers/balance.controller.ts + +@Controller('blockchain/balance') +export class BalanceController { + @Get(':chainType/:address') + @Public() // 内部服务调用 + async getBalance( + @Param('chainType') chainType: string, + @Param('address') address: string, + ): Promise { + // ... + } +} +``` + +--- + +## 9. 配置设计 + +### 9.1 环境变量 + +```bash +# .env.example + +# 服务配置 +PORT=3012 +NODE_ENV=development + +# 数据库 +DATABASE_URL=postgresql://rwa_user:password@localhost:5432/rwa_blockchain + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=11 + +# Kafka +KAFKA_BROKERS=localhost:9092 +KAFKA_CLIENT_ID=blockchain-service + +# 区块链 RPC +KAVA_RPC_URL=https://evm.kava.io +KAVA_WS_URL=wss://evm.kava.io/ws +BSC_RPC_URL=https://bsc-dataseed.binance.org +BSC_WS_URL=wss://bsc-ws-node.nariox.org:443 + +# 服务间调用 +WALLET_SERVICE_URL=http://localhost:3001 +IDENTITY_SERVICE_URL=http://localhost:3000 +``` + +--- + +## 10. 部署配置 + +### 10.1 docker-compose.yml 配置 + +在主 `docker-compose.yml` 中添加: + +```yaml +blockchain-service: + build: + context: ./blockchain-service + dockerfile: Dockerfile + container_name: rwa-blockchain-service + ports: + - "3012:3012" + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_blockchain?schema=public + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_DB=11 + - KAFKA_BROKERS=kafka:29092 + - KAFKA_CLIENT_ID=blockchain-service + - KAVA_RPC_URL=${KAVA_RPC_URL:-https://evm.kava.io} + - KAVA_WS_URL=${KAVA_WS_URL} + - BSC_RPC_URL=${BSC_RPC_URL:-https://bsc-dataseed.binance.org} + - BSC_WS_URL=${BSC_WS_URL} + - WALLET_SERVICE_URL=http://rwa-wallet-service:3001 + - IDENTITY_SERVICE_URL=http://rwa-identity-service:3000 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + kafka: + condition: service_healthy + networks: + - rwa-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3012/api/v1/blockchain/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +### 10.2 Kong 路由配置 + +```yaml +# 在 kong.yml 中添加 +- name: blockchain-service + url: http://192.168.1.111:3012 + routes: + - name: blockchain-api + paths: + - /api/v1/blockchain + strip_path: false +``` + +--- + +## 11. 初始化脚本 + +在 `scripts/init-databases.sh` 中添加 `rwa_blockchain`: + +```bash +for db in rwa_identity rwa_wallet ... rwa_blockchain; do +``` + +--- + +## 12. 关键设计决策 + +### 12.1 双重保障机制 + +``` +实时监听 (WebSocket) 补扫任务 (定时) + │ │ + ▼ ▼ + 检测充值 ───────────────> 去重处理 + │ │ + └──────────┬───────────┘ + ▼ + 确认数检查 + │ + ▼ + 通知 wallet-service +``` + +### 12.2 确认数策略 + +| 链 | 要求确认数 | 约等待时间 | +|----|-----------|-----------| +| KAVA | 12 块 | ~72 秒 | +| BSC | 15 块 | ~45 秒 | + +### 12.3 失败重试策略 + +- 通知失败最多重试 10 次 +- 指数退避:1s, 2s, 4s, 8s, ... +- 超过最大重试次数告警 + +--- + +## 13. 测试策略 + +### 13.1 单元测试 + +- 领域对象测试 (DepositTransaction, ValueObjects) +- 确认数计算逻辑 +- 事件生成逻辑 + +### 13.2 集成测试 + +- 区块链 Provider 连接 +- 事件监听和解析 +- 数据库读写 + +### 13.3 E2E 测试 + +- 完整充值流程模拟 +- 补扫逻辑验证 +- 失败重试验证 + +--- + +## 14. 监控指标 + +- `blockchain_deposit_detected_total` - 检测到的充值数 +- `blockchain_deposit_confirmed_total` - 确认的充值数 +- `blockchain_notify_failed_total` - 通知失败数 +- `blockchain_block_scan_lag` - 扫描延迟 (当前块 - 已扫描块) +- `blockchain_rpc_latency` - RPC 调用延迟 +- `blockchain_ws_reconnect_total` - WebSocket 重连次数 + +--- + +## 15. 迁移计划 + +### 从 identity-service 迁移 + +1. 将 `BlockchainQueryService` 逻辑迁移到 `BalanceQueryService` +2. 将 `DepositService` 逻辑拆分: + - 地址获取 → 保留在 identity-service + - 余额查询 → 迁移到 blockchain-service +3. 新增事件监听、补扫逻辑 +4. identity-service 发布 `WalletBound` 事件 +5. blockchain-service 消费事件并注册监控地址 + +--- + +## 16. 架构兼容性说明 + +### 16.1 与其他服务的架构对比 + +本服务严格遵循与其他微服务相同的 **DDD + 六边形架构 + 微服务** 设计原则: + +| 特性 | blockchain-service | identity-service | wallet-service | admin-service | +|-----|-------------------|------------------|----------------|---------------| +| **DDD 分层** | ✅ | ✅ | ✅ | ✅ | +| **六边形架构** | ✅ | ✅ | ✅ | ✅ | +| **模块化设计** | ✅ (modules/) | ✅ | ✅ | ✅ | +| **聚合根基类** | ✅ (AggregateRoot) | ✅ | ✅ | ✅ | +| **值对象** | 5个 (ChainType, EvmAddress, TokenAmount, BlockNumber, TxHash) | 2个+ | 5个+ | 7个+ | +| **领域事件** | ✅ | ❌ | ✅ | ❌ | +| **CQRS** | ✅ | ✅ | ✅ | ✅ | +| **Kafka 集成** | ✅ | ✅ | ❌ | ❌ | +| **Redis 缓存** | ✅ | ✅ | ❌ | ❌ | +| **定时任务** | ✅ (@Cron) | ❌ | ❌ | ❌ | + +### 16.2 关键架构原则 + +#### 依赖倒置原则 (Dependency Inversion) + +``` +┌─────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Repository Interfaces (Ports) │ │ +│ │ - IDepositTransactionRepository │ │ +│ │ - IMonitoredAddressRepository │ │ +│ └─────────────────────────────────────────────────┘ │ +│ ▲ │ +│ │ 依赖 │ +│ │ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Infrastructure Layer │ │ +│ │ Repository Implementations │ │ +│ │ - DepositTransactionRepositoryImpl │ │ +│ │ - MonitoredAddressRepositoryImpl │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 六边形架构端口与适配器 + +| 类型 | 端口 (Port) | 适配器 (Adapter) | +|------|------------|------------------| +| **入站 (Driving)** | Controllers | HTTP API | +| **入站 (Driving)** | Kafka Consumer | Event Handler | +| **出站 (Driven)** | Repository Interfaces | Prisma Repositories | +| **出站 (Driven)** | Cache Interface | Redis Cache | +| **出站 (Driven)** | Event Publisher | Kafka Producer | +| **出站 (Driven)** | External Services | HTTP Clients | + +### 16.3 命名约定 + +为与其他服务保持一致,请遵循以下命名约定: + +| 类型 | 命名规则 | 示例 | +|------|---------|------| +| **聚合根** | `*.aggregate.ts` | `deposit-transaction.aggregate.ts` | +| **值对象** | `*.vo.ts` | `chain-type.vo.ts`, `block-number.vo.ts` | +| **领域事件** | `*.event.ts` | `deposit-detected.event.ts` | +| **仓储接口** | `*.repository.interface.ts` | `deposit-transaction.repository.interface.ts` | +| **仓储实现** | `*.repository.impl.ts` | `deposit-transaction.repository.impl.ts` | +| **映射器** | `*.mapper.ts` | `deposit-transaction.mapper.ts` | +| **模块** | `*.module.ts` | `api.module.ts`, `domain.module.ts` | + +### 16.4 Symbol Token 注入规范 + +使用 Symbol 作为依赖注入 Token,与其他服务保持一致: + +```typescript +// 定义 (domain/repositories/index.ts) +export const DEPOSIT_TRANSACTION_REPOSITORY = Symbol('DEPOSIT_TRANSACTION_REPOSITORY'); +export const MONITORED_ADDRESS_REPOSITORY = Symbol('MONITORED_ADDRESS_REPOSITORY'); +export const BLOCK_CHECKPOINT_REPOSITORY = Symbol('BLOCK_CHECKPOINT_REPOSITORY'); + +// 注册 (modules/domain.module.ts) +{ + provide: DEPOSIT_TRANSACTION_REPOSITORY, + useClass: DepositTransactionRepositoryImpl, +} + +// 注入 (application/services/*.ts) +constructor( + @Inject(DEPOSIT_TRANSACTION_REPOSITORY) + private readonly depositRepo: IDepositTransactionRepository, +) {} +``` + +--- + +## 17. MPC 集成事件流 + +### 17.1 完整事件流程图 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ MPC 钱包创建事件流 │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ [1] 用户创建账户 │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ identity-service│ │ +│ │ │ ────────────────────────────────────────┐ │ +│ │ 发布事件: │ │ │ +│ │ mpc.KeygenRequested │ │ +│ └─────────────────┘ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ [2] MPC 密钥生成 │ mpc-service │ │ +│ │ │ │ +│ ┌────────────────────────────────────────────│ 消费事件: │ │ +│ │ │ mpc.KeygenRequested │ +│ │ │ │ │ +│ │ [3] 生成公钥 │ 发布事件: │ │ +│ │ │ │ mpc.KeygenStarted │ +│ │ │ │ mpc.KeygenCompleted │ +│ │ ▼ └─────────────────┘ │ +│ │ publicKey: 33 bytes │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ blockchain-service │ │ +│ │ │ │ +│ │ [4] 消费: mpc.KeygenCompleted │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ AddressDerivationAdapter │ │ │ +│ │ │ │ │ │ +│ │ │ publicKey ──┬──> deriveEvmAddress() ──> 0x1234... │ │ │ +│ │ │ │ (BSC, KAVA) │ │ │ +│ │ │ │ │ │ │ +│ │ │ └──> deriveCosmosAddress() ──> kava1... │ │ │ +│ │ │ dst1... │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ [5] 发布: blockchain.WalletAddressCreated │ │ +│ │ │ │ │ +│ └───────┼──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ├──────────────────────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ identity-service│ │ wallet-service │ │ +│ │ │ │ │ │ +│ │ [6] 存储关联: │ │ [7] 存储钱包: │ │ +│ │ userId ↔ address│ │ 地址、余额管理 │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 17.2 事件 Topic 汇总 + +| Topic | 发布者 | 消费者 | 描述 | +|-------|--------|--------|------| +| `mpc.KeygenRequested` | identity-service | mpc-service | 请求生成 MPC 密钥 | +| `mpc.KeygenStarted` | mpc-service | identity-service | 密钥生成开始 | +| `mpc.KeygenCompleted` | mpc-service | blockchain-service | 密钥生成完成,包含公钥 | +| `mpc.SessionFailed` | mpc-service | identity-service, blockchain-service | 会话失败 | +| `blockchain.WalletAddressCreated` | blockchain-service | identity-service, wallet-service | 地址派生完成 | + +### 17.3 关键设计决策 + +1. **职责分离**: + - mpc-service 只负责 MPC 协议,不了解区块链地址格式 + - blockchain-service 封装所有区块链技术细节 + - identity-service 不直接处理公钥→地址转换 + +2. **多链扩展性**: + - 新增链类型只需在 `AddressDerivationAdapter` 添加派生逻辑 + - 事件格式保持不变,下游服务无需修改 + +3. **事件溯源**: + - 所有状态变更通过事件传递 + - 支持事件重放和故障恢复 + +--- + +*文档版本: 1.2.0* +*最后更新: 2025-12-06* +*变更说明: 添加公钥→地址派生职责、MPC 事件集成、AddressDerivationAdapter、WalletAddressCreated 事件* diff --git a/backend/services/mining-blockchain-service/Dockerfile b/backend/services/mining-blockchain-service/Dockerfile new file mode 100644 index 00000000..62bba4a3 --- /dev/null +++ b/backend/services/mining-blockchain-service/Dockerfile @@ -0,0 +1,82 @@ +# ============================================================================= +# Blockchain Service Dockerfile +# ============================================================================= + +# Build stage - use Alpine for smaller build context +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig*.json ./ +COPY nest-cli.json ./ + +# Copy Prisma schema +COPY prisma ./prisma/ + +# Install dependencies +RUN npm ci + +# Generate Prisma client (dummy DATABASE_URL for build time only) +RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate + +# Copy source code +COPY src ./src + +# Build TypeScript +RUN npm run build + +# Verify build output exists +RUN ls -la dist/ && test -f dist/main.js + +# Production stage - use Debian slim for OpenSSL compatibility +FROM node:20-slim + +# Create non-root user with home directory (npm cache needs it) +RUN groupadd -g 1001 nodejs && \ + useradd -u 1001 -g nodejs -m nestjs + +# Install OpenSSL and curl for health checks +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory with correct ownership +RUN mkdir -p /app && chown nestjs:nodejs /app +WORKDIR /app + +# Switch to non-root user before installing dependencies +USER nestjs + +# Install production dependencies only +COPY --chown=nestjs:nodejs package*.json ./ +RUN npm ci --only=production + +# Copy Prisma schema and generate client +COPY --chown=nestjs:nodejs prisma ./prisma/ +RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate + +# Copy built files +COPY --chown=nestjs:nodejs --from=builder /app/dist ./dist + +# Create startup script that syncs schema before starting the app +RUN echo '#!/bin/sh\n\ +set -e\n\ +echo "Syncing database schema..."\n\ +npx prisma db push --skip-generate --accept-data-loss\n\ +echo "Starting application..."\n\ +exec node dist/main.js\n' > /app/start.sh && chmod +x /app/start.sh + +ENV NODE_ENV=production + +# Expose port +EXPOSE 3012 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:3012/api/v1/health || exit 1 + +# Start service with migration +CMD ["/app/start.sh"] diff --git a/backend/services/mining-blockchain-service/contracts/TestUSDT.sol b/backend/services/mining-blockchain-service/contracts/TestUSDT.sol new file mode 100644 index 00000000..2f4136e3 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/TestUSDT.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title TestUSDT + * @dev 测试网专用 USDT 代币,任何人都可以免费 mint + * + * 部署步骤: + * 1. 使用 Remix IDE (https://remix.ethereum.org) + * 2. 连接 MetaMask 到 BSC Testnet (Chain ID: 97) 或 KAVA Testnet (Chain ID: 2221) + * 3. 部署此合约 + * 4. 调用 mint() 函数给自己铸造代币 + */ +contract TestUSDT is ERC20, Ownable { + uint8 private _decimals; + + constructor() ERC20("Test USDT", "USDT") Ownable(msg.sender) { + _decimals = 6; // USDT 标准是 6 位小数 + // 初始铸造 1,000,000 USDT 给部署者 + _mint(msg.sender, 1_000_000 * 10 ** _decimals); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + /** + * @dev 任何人都可以给自己 mint 代币 (仅限测试网使用!) + * @param amount 铸造数量 (注意: 需要乘以 10^6, 例如 1000 USDT = 1000000000) + */ + function mint(uint256 amount) external { + _mint(msg.sender, amount); + } + + /** + * @dev 便捷函数: 直接输入 USDT 数量,自动处理精度 + * @param usdtAmount USDT 数量 (例如输入 1000 就是 1000 USDT) + */ + function mintUsdt(uint256 usdtAmount) external { + _mint(msg.sender, usdtAmount * 10 ** _decimals); + } + + /** + * @dev Owner 可以给任意地址 mint + */ + function mintTo(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + /** + * @dev 水龙头功能: 一次性领取 10000 USDT + */ + function faucet() external { + _mint(msg.sender, 10_000 * 10 ** _decimals); + } +} diff --git a/backend/services/mining-blockchain-service/contracts/TestUSDT_Flat.sol b/backend/services/mining-blockchain-service/contracts/TestUSDT_Flat.sol new file mode 100644 index 00000000..5ea7ea20 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/TestUSDT_Flat.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title TestUSDT (Flattened) + * @dev 测试网专用 USDT,可直接在 Remix 部署,无需额外依赖 + * + * 部署网络: + * - BSC Testnet: Chain ID 97, RPC: https://data-seed-prebsc-1-s1.binance.org:8545 + * - KAVA Testnet: Chain ID 2221, RPC: https://evm.testnet.kava.io + */ + +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } +} + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + +interface IERC20Metadata is IERC20 { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); +} + +abstract contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + uint256 private _totalSupply; + string private _name; + string private _symbol; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + function name() public view virtual returns (string memory) { return _name; } + function symbol() public view virtual returns (string memory) { return _symbol; } + function decimals() public view virtual returns (uint8) { return 18; } + function totalSupply() public view virtual returns (uint256) { return _totalSupply; } + function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; } + + function transfer(address to, uint256 value) public virtual returns (bool) { + _transfer(_msgSender(), to, value); + return true; + } + + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 value) public virtual returns (bool) { + _approve(_msgSender(), spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + uint256 currentAllowance = allowance(from, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= value, "ERC20: insufficient allowance"); + unchecked { _approve(from, spender, currentAllowance - value); } + } + _transfer(from, to, value); + return true; + } + + function _transfer(address from, address to, uint256 value) internal { + require(from != address(0), "ERC20: transfer from zero address"); + require(to != address(0), "ERC20: transfer to zero address"); + uint256 fromBalance = _balances[from]; + require(fromBalance >= value, "ERC20: insufficient balance"); + unchecked { + _balances[from] = fromBalance - value; + _balances[to] += value; + } + emit Transfer(from, to, value); + } + + function _mint(address account, uint256 value) internal { + require(account != address(0), "ERC20: mint to zero address"); + _totalSupply += value; + unchecked { _balances[account] += value; } + emit Transfer(address(0), account, value); + } + + function _approve(address owner, address spender, uint256 value) internal { + require(owner != address(0) && spender != address(0), "ERC20: zero address"); + _allowances[owner][spender] = value; + emit Approval(owner, spender, value); + } +} + +contract TestUSDT is ERC20 { + uint8 private constant _decimals = 6; + address public owner; + + modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; + } + + constructor() ERC20("Test USDT", "USDT") { + owner = msg.sender; + _mint(msg.sender, 1_000_000 * 10 ** _decimals); + } + + function decimals() public pure override returns (uint8) { + return _decimals; + } + + /// @dev 任何人可以 mint (测试网专用) + function mint(uint256 amount) external { + _mint(msg.sender, amount); + } + + /// @dev 便捷函数: 输入 USDT 数量,自动处理精度 + function mintUsdt(uint256 usdtAmount) external { + _mint(msg.sender, usdtAmount * 10 ** _decimals); + } + + /// @dev Owner 给任意地址 mint + function mintTo(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + /// @dev 水龙头: 一次领 10000 USDT + function faucet() external { + _mint(msg.sender, 10_000 * 10 ** _decimals); + } +} diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/EnergyUSDT.sol b/backend/services/mining-blockchain-service/contracts/eUSDT/EnergyUSDT.sol new file mode 100644 index 00000000..ff0f90b4 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/EnergyUSDT.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @title EnergyUSDT + * @dev Fixed supply ERC-20 token - NO MINTING CAPABILITY + * Total Supply: 10,002,000,000 (100.02 Billion) tokens with 6 decimals (matching USDT) + * + * IMPORTANT: This contract has NO mint function and NO way to increase supply. + * All tokens are minted to the deployer at construction time. + */ +contract EnergyUSDT { + string public constant name = "Energy USDT"; + string public constant symbol = "eUSDT"; + uint8 public constant decimals = 6; + + // Fixed total supply: 100.02 billion tokens (10,002,000,000 * 10^6) + uint256 public constant totalSupply = 10_002_000_000 * 10**6; + + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Constructor - mints entire fixed supply to deployer + * No mint function exists - supply is permanently fixed + */ + constructor() { + _balances[msg.sender] = totalSupply; + emit Transfer(address(0), msg.sender, totalSupply); + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(to != address(0), "Transfer to zero address"); + require(_balances[msg.sender] >= amount, "Insufficient balance"); + + unchecked { + _balances[msg.sender] -= amount; + _balances[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public returns (bool) { + require(spender != address(0), "Approve to zero address"); + _allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + require(from != address(0), "Transfer from zero address"); + require(to != address(0), "Transfer to zero address"); + require(_balances[from] >= amount, "Insufficient balance"); + require(_allowances[from][msg.sender] >= amount, "Insufficient allowance"); + + unchecked { + _balances[from] -= amount; + _balances[to] += amount; + _allowances[from][msg.sender] -= amount; + } + + emit Transfer(from, to, amount); + return true; + } +} diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/README.md b/backend/services/mining-blockchain-service/contracts/eUSDT/README.md new file mode 100644 index 00000000..bc29581e --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/README.md @@ -0,0 +1,81 @@ +# eUSDT (Energy USDT) + +## 代币信息 + +| 属性 | 值 | +|------|-----| +| 名称 | Energy USDT | +| 符号 | eUSDT | +| 精度 | 6 decimals | +| 总供应量 | 10,002,000,000 (100.02亿) | +| 标准 | ERC-20 | +| 部署链 | KAVA Mainnet (Chain ID: 2222) | + +## 合约特性 + +- **固定供应量**:100.02亿代币,部署时全部铸造给部署者 +- **不可增发**:合约中没有 mint 函数,供应量永久固定 +- **不可销毁**:合约层面无销毁功能 +- **不可升级**:合约逻辑永久固定 +- **标准ERC-20**:完全兼容所有主流钱包和DEX + +## 部署步骤 + +### 1. 安装依赖 + +```bash +cd backend/services/blockchain-service/contracts/eUSDT +npm install +``` + +### 2. 编译合约 + +```bash +node compile.mjs +``` + +编译后会在 `build/` 目录生成: +- `EnergyUSDT.abi` - 合约ABI +- `EnergyUSDT.bin` - 合约字节码 + +### 3. 部署合约 + +确保部署账户有足够的 KAVA 支付 gas 费(约 0.02 KAVA)。 + +```bash +node deploy.mjs +``` + +## 合约函数 + +| 函数 | 说明 | +|------|------| +| `name()` | 返回 "Energy USDT" | +| `symbol()` | 返回 "eUSDT" | +| `decimals()` | 返回 6 | +| `totalSupply()` | 返回 10,002,000,000 * 10^6 | +| `balanceOf(address)` | 查询账户余额 | +| `transfer(address, uint256)` | 转账 | +| `approve(address, uint256)` | 授权额度 | +| `transferFrom(address, address, uint256)` | 代理转账 | +| `allowance(address, address)` | 查询授权额度 | + +## 事件 + +| 事件 | 说明 | +|------|------| +| `Transfer(from, to, value)` | 转账事件 | +| `Approval(owner, spender, value)` | 授权事件 | + +## 部署信息 + +| 网络 | 合约地址 | 区块浏览器 | +|------|---------|-----------| +| KAVA Mainnet | `0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931` | https://kavascan.com/address/0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931 | + +**部署详情:** +- 部署者/代币拥有者:`0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` +- 私钥:`0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a` +- 初始持有量:10,002,000,000 eUSDT(全部代币) +- 交易哈希:`0x5bebaa4a35378438ba5c891972024a1766935d2e01397a33502aa99e956a6b19` +- 部署时间:2026-01-19 diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/compile.mjs b/backend/services/mining-blockchain-service/contracts/eUSDT/compile.mjs new file mode 100644 index 00000000..e1c8a268 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/compile.mjs @@ -0,0 +1,51 @@ +import solc from 'solc'; +import fs from 'fs'; + +const source = fs.readFileSync('EnergyUSDT.sol', 'utf8'); + +const input = { + language: 'Solidity', + sources: { + 'EnergyUSDT.sol': { + content: source + } + }, + settings: { + optimizer: { + enabled: true, + runs: 200 + }, + evmVersion: 'paris', // Use paris to avoid PUSH0 + outputSelection: { + '*': { + '*': ['abi', 'evm.bytecode'] + } + } + } +}; + +const output = JSON.parse(solc.compile(JSON.stringify(input))); + +if (output.errors) { + output.errors.forEach(err => { + console.log(err.formattedMessage); + }); + + // Check for actual errors (not just warnings) + const hasErrors = output.errors.some(err => err.severity === 'error'); + if (hasErrors) { + process.exit(1); + } +} + +const contract = output.contracts['EnergyUSDT.sol']['EnergyUSDT']; +const bytecode = contract.evm.bytecode.object; +const abi = contract.abi; + +fs.mkdirSync('build', { recursive: true }); +fs.writeFileSync('build/EnergyUSDT.bin', bytecode); +fs.writeFileSync('build/EnergyUSDT.abi', JSON.stringify(abi, null, 2)); + +console.log('Compiled successfully!'); +console.log('Bytecode length:', bytecode.length); +console.log('ABI functions:', abi.filter(x => x.type === 'function').map(x => x.name).join(', ')); diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/deploy.mjs b/backend/services/mining-blockchain-service/contracts/eUSDT/deploy.mjs new file mode 100644 index 00000000..5e8d6a3a --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/deploy.mjs @@ -0,0 +1,86 @@ +import { ethers } from 'ethers'; +import fs from 'fs'; + +// Same deployer account as dUSDT +const PRIVATE_KEY = '0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a'; +const RPC_URL = 'https://evm.kava.io'; + +// Contract bytecode +const BYTECODE = '0x' + fs.readFileSync('build/EnergyUSDT.bin', 'utf8'); +const ABI = JSON.parse(fs.readFileSync('build/EnergyUSDT.abi', 'utf8')); + +async function deploy() { + // Connect to Kava mainnet + const provider = new ethers.JsonRpcProvider(RPC_URL); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + + console.log('Deployer address:', wallet.address); + + // Check balance + const balance = await provider.getBalance(wallet.address); + console.log('Balance:', ethers.formatEther(balance), 'KAVA'); + + if (parseFloat(ethers.formatEther(balance)) < 0.01) { + console.error('Insufficient KAVA balance for deployment!'); + process.exit(1); + } + + // Get network info + const network = await provider.getNetwork(); + console.log('Chain ID:', network.chainId.toString()); + + // Create contract factory + const factory = new ethers.ContractFactory(ABI, BYTECODE, wallet); + + console.log('Deploying EnergyUSDT (eUSDT) contract...'); + + // Deploy + const contract = await factory.deploy(); + console.log('Transaction hash:', contract.deploymentTransaction().hash); + + // Wait for deployment + console.log('Waiting for confirmation...'); + await contract.waitForDeployment(); + + const contractAddress = await contract.getAddress(); + console.log('Contract deployed at:', contractAddress); + + // Verify deployment + console.log('\nVerifying deployment...'); + const name = await contract.name(); + const symbol = await contract.symbol(); + const decimals = await contract.decimals(); + const totalSupply = await contract.totalSupply(); + const ownerBalance = await contract.balanceOf(wallet.address); + + console.log('Token name:', name); + console.log('Token symbol:', symbol); + console.log('Decimals:', decimals.toString()); + console.log('Total supply:', ethers.formatUnits(totalSupply, 6), 'eUSDT'); + console.log('Owner balance:', ethers.formatUnits(ownerBalance, 6), 'eUSDT'); + + console.log('\n=== DEPLOYMENT COMPLETE ==='); + console.log('Contract Address:', contractAddress); + console.log('Explorer:', `https://kavascan.com/address/${contractAddress}`); + + // Save deployment info + const deploymentInfo = { + network: 'KAVA Mainnet', + chainId: 2222, + contractAddress, + deployer: wallet.address, + transactionHash: contract.deploymentTransaction().hash, + deployedAt: new Date().toISOString(), + token: { + name, + symbol, + decimals: decimals.toString(), + totalSupply: totalSupply.toString() + } + }; + + fs.writeFileSync('deployment.json', JSON.stringify(deploymentInfo, null, 2)); + console.log('\nDeployment info saved to deployment.json'); +} + +deploy().catch(console.error); diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/deployment.json b/backend/services/mining-blockchain-service/contracts/eUSDT/deployment.json new file mode 100644 index 00000000..851f5134 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/deployment.json @@ -0,0 +1,14 @@ +{ + "network": "KAVA Mainnet", + "chainId": 2222, + "contractAddress": "0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931", + "deployer": "0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E", + "transactionHash": "0x5bebaa4a35378438ba5c891972024a1766935d2e01397a33502aa99e956a6b19", + "deployedAt": "2026-01-19T13:25:28.071Z", + "token": { + "name": "Energy USDT", + "symbol": "eUSDT", + "decimals": "6", + "totalSupply": "10002000000000000" + } +} \ No newline at end of file diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/package-lock.json b/backend/services/mining-blockchain-service/contracts/eUSDT/package-lock.json new file mode 100644 index 00000000..124b96d6 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/package-lock.json @@ -0,0 +1,222 @@ +{ + "name": "eusdt-contract", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eusdt-contract", + "version": "1.0.0", + "dependencies": { + "ethers": "^6.9.0", + "solc": "^0.8.19" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/solc": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.19.tgz", + "integrity": "sha512-yqurS3wzC4LdEvmMobODXqprV4MYJcVtinuxgrp61ac8K2zz40vXA0eSAskSHPgv8dQo7Nux39i3QBsHx4pqyA==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/backend/services/mining-blockchain-service/contracts/eUSDT/package.json b/backend/services/mining-blockchain-service/contracts/eUSDT/package.json new file mode 100644 index 00000000..b8618ace --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/eUSDT/package.json @@ -0,0 +1,14 @@ +{ + "name": "eusdt-contract", + "version": "1.0.0", + "type": "module", + "description": "Energy USDT (eUSDT) ERC-20 Token Contract", + "scripts": { + "compile": "node compile.mjs", + "deploy": "node deploy.mjs" + }, + "dependencies": { + "ethers": "^6.9.0", + "solc": "^0.8.19" + } +} diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/FutureUSDT.sol b/backend/services/mining-blockchain-service/contracts/fUSDT/FutureUSDT.sol new file mode 100644 index 00000000..29dd172b --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/FutureUSDT.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @title FutureUSDT + * @dev Fixed supply ERC-20 token - NO MINTING CAPABILITY + * Total Supply: 1,000,000,000,000 (1 Trillion) tokens with 6 decimals (matching USDT) + * + * IMPORTANT: This contract has NO mint function and NO way to increase supply. + * All tokens are minted to the deployer at construction time. + */ +contract FutureUSDT { + string public constant name = "Future USDT"; + string public constant symbol = "fUSDT"; + uint8 public constant decimals = 6; + + // Fixed total supply: 1 trillion tokens (1,000,000,000,000 * 10^6) + uint256 public constant totalSupply = 1_000_000_000_000 * 10**6; + + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Constructor - mints entire fixed supply to deployer + * No mint function exists - supply is permanently fixed + */ + constructor() { + _balances[msg.sender] = totalSupply; + emit Transfer(address(0), msg.sender, totalSupply); + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function transfer(address to, uint256 amount) public returns (bool) { + require(to != address(0), "Transfer to zero address"); + require(_balances[msg.sender] >= amount, "Insufficient balance"); + + unchecked { + _balances[msg.sender] -= amount; + _balances[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public returns (bool) { + require(spender != address(0), "Approve to zero address"); + _allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + require(from != address(0), "Transfer from zero address"); + require(to != address(0), "Transfer to zero address"); + require(_balances[from] >= amount, "Insufficient balance"); + require(_allowances[from][msg.sender] >= amount, "Insufficient allowance"); + + unchecked { + _balances[from] -= amount; + _balances[to] += amount; + _allowances[from][msg.sender] -= amount; + } + + emit Transfer(from, to, amount); + return true; + } +} diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/README.md b/backend/services/mining-blockchain-service/contracts/fUSDT/README.md new file mode 100644 index 00000000..bdd497d3 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/README.md @@ -0,0 +1,81 @@ +# fUSDT (Future USDT) + +## 代币信息 + +| 属性 | 值 | +|------|-----| +| 名称 | Future USDT | +| 符号 | fUSDT | +| 精度 | 6 decimals | +| 总供应量 | 1,000,000,000,000 (1万亿) | +| 标准 | ERC-20 | +| 部署链 | KAVA Mainnet (Chain ID: 2222) | + +## 合约特性 + +- **固定供应量**:1万亿代币,部署时全部铸造给部署者 +- **不可增发**:合约中没有 mint 函数,供应量永久固定 +- **不可销毁**:合约层面无销毁功能 +- **不可升级**:合约逻辑永久固定 +- **标准ERC-20**:完全兼容所有主流钱包和DEX + +## 部署步骤 + +### 1. 安装依赖 + +```bash +cd backend/services/blockchain-service/contracts/fUSDT +npm install +``` + +### 2. 编译合约 + +```bash +node compile.mjs +``` + +编译后会在 `build/` 目录生成: +- `FutureUSDT.abi` - 合约ABI +- `FutureUSDT.bin` - 合约字节码 + +### 3. 部署合约 + +确保部署账户有足够的 KAVA 支付 gas 费(约 0.02 KAVA)。 + +```bash +node deploy.mjs +``` + +## 合约函数 + +| 函数 | 说明 | +|------|------| +| `name()` | 返回 "Future USDT" | +| `symbol()` | 返回 "fUSDT" | +| `decimals()` | 返回 6 | +| `totalSupply()` | 返回 1,000,000,000,000 * 10^6 | +| `balanceOf(address)` | 查询账户余额 | +| `transfer(address, uint256)` | 转账 | +| `approve(address, uint256)` | 授权额度 | +| `transferFrom(address, address, uint256)` | 代理转账 | +| `allowance(address, address)` | 查询授权额度 | + +## 事件 + +| 事件 | 说明 | +|------|------| +| `Transfer(from, to, value)` | 转账事件 | +| `Approval(owner, spender, value)` | 授权事件 | + +## 部署信息 + +| 网络 | 合约地址 | 区块浏览器 | +|------|---------|-----------| +| KAVA Mainnet | `0x14dc4f7d3E4197438d058C3D156dd9826A161134` | https://kavascan.com/address/0x14dc4f7d3E4197438d058C3D156dd9826A161134 | + +**部署详情:** +- 部署者/代币拥有者:`0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E` +- 私钥:`0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a` +- 初始持有量:1,000,000,000,000 fUSDT(全部代币) +- 交易哈希:`0x071f535971bc3a134dd26c182b6f05c53f0c3783e91fe6ef471d6c914e4cdb06` +- 部署时间:2026-01-19 diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/compile.mjs b/backend/services/mining-blockchain-service/contracts/fUSDT/compile.mjs new file mode 100644 index 00000000..dff00fcb --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/compile.mjs @@ -0,0 +1,51 @@ +import solc from 'solc'; +import fs from 'fs'; + +const source = fs.readFileSync('FutureUSDT.sol', 'utf8'); + +const input = { + language: 'Solidity', + sources: { + 'FutureUSDT.sol': { + content: source + } + }, + settings: { + optimizer: { + enabled: true, + runs: 200 + }, + evmVersion: 'paris', // Use paris to avoid PUSH0 + outputSelection: { + '*': { + '*': ['abi', 'evm.bytecode'] + } + } + } +}; + +const output = JSON.parse(solc.compile(JSON.stringify(input))); + +if (output.errors) { + output.errors.forEach(err => { + console.log(err.formattedMessage); + }); + + // Check for actual errors (not just warnings) + const hasErrors = output.errors.some(err => err.severity === 'error'); + if (hasErrors) { + process.exit(1); + } +} + +const contract = output.contracts['FutureUSDT.sol']['FutureUSDT']; +const bytecode = contract.evm.bytecode.object; +const abi = contract.abi; + +fs.mkdirSync('build', { recursive: true }); +fs.writeFileSync('build/FutureUSDT.bin', bytecode); +fs.writeFileSync('build/FutureUSDT.abi', JSON.stringify(abi, null, 2)); + +console.log('Compiled successfully!'); +console.log('Bytecode length:', bytecode.length); +console.log('ABI functions:', abi.filter(x => x.type === 'function').map(x => x.name).join(', ')); diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/deploy.mjs b/backend/services/mining-blockchain-service/contracts/fUSDT/deploy.mjs new file mode 100644 index 00000000..ead71119 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/deploy.mjs @@ -0,0 +1,86 @@ +import { ethers } from 'ethers'; +import fs from 'fs'; + +// Same deployer account as dUSDT +const PRIVATE_KEY = '0x886ea4cffe76c386fecf3ff321ac9ae913737c46c17bc6ce2413752144668a2a'; +const RPC_URL = 'https://evm.kava.io'; + +// Contract bytecode +const BYTECODE = '0x' + fs.readFileSync('build/FutureUSDT.bin', 'utf8'); +const ABI = JSON.parse(fs.readFileSync('build/FutureUSDT.abi', 'utf8')); + +async function deploy() { + // Connect to Kava mainnet + const provider = new ethers.JsonRpcProvider(RPC_URL); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + + console.log('Deployer address:', wallet.address); + + // Check balance + const balance = await provider.getBalance(wallet.address); + console.log('Balance:', ethers.formatEther(balance), 'KAVA'); + + if (parseFloat(ethers.formatEther(balance)) < 0.01) { + console.error('Insufficient KAVA balance for deployment!'); + process.exit(1); + } + + // Get network info + const network = await provider.getNetwork(); + console.log('Chain ID:', network.chainId.toString()); + + // Create contract factory + const factory = new ethers.ContractFactory(ABI, BYTECODE, wallet); + + console.log('Deploying FutureUSDT (fUSDT) contract...'); + + // Deploy + const contract = await factory.deploy(); + console.log('Transaction hash:', contract.deploymentTransaction().hash); + + // Wait for deployment + console.log('Waiting for confirmation...'); + await contract.waitForDeployment(); + + const contractAddress = await contract.getAddress(); + console.log('Contract deployed at:', contractAddress); + + // Verify deployment + console.log('\nVerifying deployment...'); + const name = await contract.name(); + const symbol = await contract.symbol(); + const decimals = await contract.decimals(); + const totalSupply = await contract.totalSupply(); + const ownerBalance = await contract.balanceOf(wallet.address); + + console.log('Token name:', name); + console.log('Token symbol:', symbol); + console.log('Decimals:', decimals.toString()); + console.log('Total supply:', ethers.formatUnits(totalSupply, 6), 'fUSDT'); + console.log('Owner balance:', ethers.formatUnits(ownerBalance, 6), 'fUSDT'); + + console.log('\n=== DEPLOYMENT COMPLETE ==='); + console.log('Contract Address:', contractAddress); + console.log('Explorer:', `https://kavascan.com/address/${contractAddress}`); + + // Save deployment info + const deploymentInfo = { + network: 'KAVA Mainnet', + chainId: 2222, + contractAddress, + deployer: wallet.address, + transactionHash: contract.deploymentTransaction().hash, + deployedAt: new Date().toISOString(), + token: { + name, + symbol, + decimals: decimals.toString(), + totalSupply: totalSupply.toString() + } + }; + + fs.writeFileSync('deployment.json', JSON.stringify(deploymentInfo, null, 2)); + console.log('\nDeployment info saved to deployment.json'); +} + +deploy().catch(console.error); diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/deployment.json b/backend/services/mining-blockchain-service/contracts/fUSDT/deployment.json new file mode 100644 index 00000000..1ae8c632 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/deployment.json @@ -0,0 +1,14 @@ +{ + "network": "KAVA Mainnet", + "chainId": 2222, + "contractAddress": "0x14dc4f7d3E4197438d058C3D156dd9826A161134", + "deployer": "0x4F7E78d6B7C5FC502Ec7039848690f08c8970F1E", + "transactionHash": "0x071f535971bc3a134dd26c182b6f05c53f0c3783e91fe6ef471d6c914e4cdb06", + "deployedAt": "2026-01-19T13:26:05.111Z", + "token": { + "name": "Future USDT", + "symbol": "fUSDT", + "decimals": "6", + "totalSupply": "1000000000000000000" + } +} \ No newline at end of file diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/package-lock.json b/backend/services/mining-blockchain-service/contracts/fUSDT/package-lock.json new file mode 100644 index 00000000..6dcae991 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/package-lock.json @@ -0,0 +1,222 @@ +{ + "name": "fusdt-contract", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fusdt-contract", + "version": "1.0.0", + "dependencies": { + "ethers": "^6.9.0", + "solc": "^0.8.19" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/solc": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.19.tgz", + "integrity": "sha512-yqurS3wzC4LdEvmMobODXqprV4MYJcVtinuxgrp61ac8K2zz40vXA0eSAskSHPgv8dQo7Nux39i3QBsHx4pqyA==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/backend/services/mining-blockchain-service/contracts/fUSDT/package.json b/backend/services/mining-blockchain-service/contracts/fUSDT/package.json new file mode 100644 index 00000000..db440ba4 --- /dev/null +++ b/backend/services/mining-blockchain-service/contracts/fUSDT/package.json @@ -0,0 +1,14 @@ +{ + "name": "fusdt-contract", + "version": "1.0.0", + "type": "module", + "description": "Future USDT (fUSDT) ERC-20 Token Contract", + "scripts": { + "compile": "node compile.mjs", + "deploy": "node deploy.mjs" + }, + "dependencies": { + "ethers": "^6.9.0", + "solc": "^0.8.19" + } +} diff --git a/backend/services/mining-blockchain-service/deploy.sh b/backend/services/mining-blockchain-service/deploy.sh new file mode 100644 index 00000000..ad2ce99c --- /dev/null +++ b/backend/services/mining-blockchain-service/deploy.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# ============================================================================= +# Blockchain Service - Individual Deployment Script +# ============================================================================= + +set -e + +SERVICE_NAME="blockchain-service" +CONTAINER_NAME="rwa-blockchain-service" +IMAGE_NAME="services-blockchain-service" +PORT=3012 + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SERVICES_DIR="$(dirname "$SCRIPT_DIR")" + +# Load environment +if [ -f "$SERVICES_DIR/.env" ]; then + export $(cat "$SERVICES_DIR/.env" | grep -v '^#' | xargs) +fi + +case "$1" in + build) + log_info "Building $SERVICE_NAME..." + docker build -t "$IMAGE_NAME" "$SCRIPT_DIR" + log_success "$SERVICE_NAME built successfully" + ;; + + build-no-cache) + log_info "Building $SERVICE_NAME (no cache)..." + docker build --no-cache -t "$IMAGE_NAME" "$SCRIPT_DIR" + log_success "$SERVICE_NAME built successfully" + ;; + + start) + log_info "Starting $SERVICE_NAME..." + cd "$SERVICES_DIR" + docker compose up -d "$SERVICE_NAME" + log_success "$SERVICE_NAME started" + ;; + + stop) + log_info "Stopping $SERVICE_NAME..." + docker stop "$CONTAINER_NAME" 2>/dev/null || true + docker rm "$CONTAINER_NAME" 2>/dev/null || true + log_success "$SERVICE_NAME stopped" + ;; + + restart) + $0 stop + $0 start + ;; + + logs) + docker logs -f "$CONTAINER_NAME" + ;; + + logs-tail) + docker logs --tail 100 "$CONTAINER_NAME" + ;; + + status) + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_success "$SERVICE_NAME is running" + docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Status}}\t{{.Ports}}" + else + log_warn "$SERVICE_NAME is not running" + fi + ;; + + health) + log_info "Checking health of $SERVICE_NAME..." + if curl -sf "http://localhost:$PORT/health" > /dev/null 2>&1; then + log_success "$SERVICE_NAME is healthy" + else + log_error "$SERVICE_NAME health check failed" + exit 1 + fi + ;; + + migrate) + log_info "Running migrations for $SERVICE_NAME..." + docker exec "$CONTAINER_NAME" npx prisma migrate deploy + log_success "Migrations completed" + ;; + + migrate-dev) + log_info "Running dev migrations for $SERVICE_NAME..." + docker exec "$CONTAINER_NAME" npx prisma migrate dev + ;; + + prisma-studio) + log_info "Starting Prisma Studio..." + docker exec -it "$CONTAINER_NAME" npx prisma studio + ;; + + shell) + log_info "Opening shell in $SERVICE_NAME container..." + docker exec -it "$CONTAINER_NAME" sh + ;; + + test) + log_info "Running tests for $SERVICE_NAME..." + cd "$SCRIPT_DIR" + npm test + ;; + + scan-blocks) + log_info "Manually triggering block scan..." + curl -X POST "http://localhost:$PORT/internal/scan-blocks" \ + -H "Content-Type: application/json" + ;; + + check-balance) + if [ -z "$2" ] || [ -z "$3" ]; then + log_error "Usage: $0 check-balance
" + log_info "Example: $0 check-balance KAVA 0x1234..." + exit 1 + fi + log_info "Checking balance on $2 for $3..." + curl -s "http://localhost:$PORT/balance?chainType=$2&address=$3" | jq '.' + ;; + + *) + echo "Usage: $0 {build|build-no-cache|start|stop|restart|logs|logs-tail|status|health|migrate|migrate-dev|prisma-studio|shell|test|scan-blocks|check-balance}" + echo "" + echo "Commands:" + echo " build - Build Docker image" + echo " build-no-cache - Build Docker image without cache" + echo " start - Start the service" + echo " stop - Stop the service" + echo " restart - Restart the service" + echo " logs - Follow logs" + echo " logs-tail - Show last 100 log lines" + echo " status - Show service status" + echo " health - Check service health" + echo " migrate - Run database migrations" + echo " migrate-dev - Run dev migrations" + echo " prisma-studio - Open Prisma Studio" + echo " shell - Open shell in container" + echo " test - Run tests locally" + echo " scan-blocks - Manually trigger block scanning" + echo " check-balance - Check address balance (usage: check-balance
)" + exit 1 + ;; +esac diff --git a/backend/services/mining-blockchain-service/docker-compose.yml b/backend/services/mining-blockchain-service/docker-compose.yml new file mode 100644 index 00000000..d593ad13 --- /dev/null +++ b/backend/services/mining-blockchain-service/docker-compose.yml @@ -0,0 +1,53 @@ +# ============================================================================= +# Blockchain Service - Docker Compose (Development/Standalone) +# ============================================================================= +# For production, use the root docker-compose.yml in ../ +# +# For standalone development: +# 1. First start shared infrastructure: cd .. && ./deploy.sh up postgres redis kafka +# 2. Then: docker compose up -d --build +# ============================================================================= + +services: + blockchain-service: + build: + context: . + dockerfile: Dockerfile + container_name: rwa-blockchain-service + ports: + - "3012:3012" + environment: + # Application + NODE_ENV: production + APP_PORT: 3012 + API_PREFIX: api/v1 + # Database (shared PostgreSQL) + DATABASE_URL: postgresql://rwa_user:rwa_secure_password@rwa-postgres:5432/rwa_blockchain?schema=public + # Redis (shared) + REDIS_HOST: rwa-redis + REDIS_PORT: 6379 + REDIS_DB: 11 + # Kafka (shared) + KAFKA_BROKERS: rwa-kafka:29092 + KAFKA_CLIENT_ID: blockchain-service + KAFKA_GROUP_ID: blockchain-service-group + # Blockchain RPC + KAVA_RPC_URL: https://evm.kava.io + BSC_RPC_URL: https://bsc-dataseed.binance.org + # MPC Hot Wallet (用于提现转账) + MPC_SERVICE_URL: http://rwa-mpc-service:3013 + HOT_WALLET_USERNAME: ${HOT_WALLET_USERNAME:-} + HOT_WALLET_ADDRESS: ${HOT_WALLET_ADDRESS:-} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3012/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + networks: + - rwa-network + +networks: + rwa-network: + external: true diff --git a/backend/services/mining-blockchain-service/nest-cli.json b/backend/services/mining-blockchain-service/nest-cli.json new file mode 100644 index 00000000..f5e93169 --- /dev/null +++ b/backend/services/mining-blockchain-service/nest-cli.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "classValidatorShim": true, + "introspectComments": true + } + } + ] + } +} diff --git a/backend/services/mining-blockchain-service/package-lock.json b/backend/services/mining-blockchain-service/package-lock.json new file mode 100644 index 00000000..5bee135a --- /dev/null +++ b/backend/services/mining-blockchain-service/package-lock.json @@ -0,0 +1,10587 @@ +{ + "name": "blockchain-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "blockchain-service", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/axios": "^3.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.0.0", + "@nestjs/swagger": "^7.1.17", + "@prisma/client": "^5.7.0", + "@scure/bip32": "^1.3.2", + "@scure/bip39": "^1.6.0", + "bcrypt": "^6.0.0", + "bech32": "^2.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "ethers": "^6.9.0", + "ioredis": "^5.3.2", + "kafkajs": "^2.2.4", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.0", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "prisma": "^5.7.0", + "solc": "^0.8.17", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@nestjs/axios": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz", + "integrity": "sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.20.tgz", + "integrity": "sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.20.tgz", + "integrity": "sha512-kRdtyKA3+Tu70N3RQ4JgmO1E3LzAMs/eppj7SfjabC7TgqNWoS4RLhWl4BqmsNVmjj6D5jgfPVtHtgYkU3AfpQ==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/microservices": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.4.20.tgz", + "integrity": "sha512-zu/o84Z0uTUClNnGIGfIjcrO3z6T60h/pZPSJK50o4mehbEvJ76fijj6R/WTW0VP+1N16qOv/NsiYLKJA5Cc3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz", + "integrity": "sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.3", + "cors": "2.8.5", + "express": "4.21.2", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz", + "integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==", + "license": "MIT", + "dependencies": { + "cron": "3.2.1", + "uuid": "11.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/swagger": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.17.14" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz", + "integrity": "sha512-nMkRDukDKskdPruM6EsgMq7yJua+CPZM6I6FrLP8yXw8BiVSPv9Nm0CtcGGwt3kgZF9hfxKjGqLjsvVBsv6Vfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", + "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz", + "integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.31", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.31.tgz", + "integrity": "sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/solc": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.17.tgz", + "integrity": "sha512-Dtidk2XtTTmkB3IKdyeg6wLYopJnBVxdoykN8oP8VY3PQjN16BScYoUJTXFm2OP7P0hXNAqWiJNmmfuELtLf8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/services/mining-blockchain-service/package.json b/backend/services/mining-blockchain-service/package.json new file mode 100644 index 00000000..a032651a --- /dev/null +++ b/backend/services/mining-blockchain-service/package.json @@ -0,0 +1,105 @@ +{ + "name": "mining-blockchain-service", + "version": "1.0.0", + "description": "Mining Blockchain Service - dUSDT transfer for C2C Bot", + "author": "RWA Team", + "private": true, + "license": "UNLICENSED", + "prisma": { + "schema": "prisma/schema.prisma", + "seed": "ts-node prisma/seed.ts" + }, + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:migrate:prod": "prisma migrate deploy", + "prisma:studio": "prisma studio" + }, + "dependencies": { + "@nestjs/axios": "^3.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.0.0", + "@nestjs/swagger": "^7.1.17", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "@prisma/client": "^5.7.0", + "@scure/bip32": "^1.3.2", + "@scure/bip39": "^1.6.0", + "bcrypt": "^6.0.0", + "bech32": "^2.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "ethers": "^6.9.0", + "ioredis": "^5.3.2", + "kafkajs": "^2.2.4", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.17", + "@types/passport-jwt": "^4.0.1", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "prisma": "^5.7.0", + "solc": "^0.8.17", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^@/(.*)$": "/$1" + } + } +} diff --git a/backend/services/mining-blockchain-service/prisma/migrations/20241207000000_init/migration.sql b/backend/services/mining-blockchain-service/prisma/migrations/20241207000000_init/migration.sql new file mode 100644 index 00000000..dbd4c285 --- /dev/null +++ b/backend/services/mining-blockchain-service/prisma/migrations/20241207000000_init/migration.sql @@ -0,0 +1,141 @@ +-- CreateTable +CREATE TABLE "monitored_addresses" ( + "address_id" BIGSERIAL NOT NULL, + "chain_type" VARCHAR(20) NOT NULL, + "address" VARCHAR(42) NOT NULL, + "user_id" BIGINT NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "monitored_addresses_pkey" PRIMARY KEY ("address_id") +); + +-- CreateTable +CREATE TABLE "deposit_transactions" ( + "deposit_id" BIGSERIAL NOT NULL, + "chain_type" VARCHAR(20) NOT NULL, + "tx_hash" VARCHAR(66) NOT NULL, + "from_address" VARCHAR(42) NOT NULL, + "to_address" VARCHAR(42) NOT NULL, + "token_contract" VARCHAR(42) NOT NULL, + "amount" DECIMAL(36,18) NOT NULL, + "amount_formatted" DECIMAL(20,8) NOT NULL, + "block_number" BIGINT NOT NULL, + "block_timestamp" TIMESTAMP(3) NOT NULL, + "log_index" INTEGER NOT NULL, + "confirmations" INTEGER NOT NULL DEFAULT 0, + "status" VARCHAR(20) NOT NULL DEFAULT 'DETECTED', + "address_id" BIGINT NOT NULL, + "user_id" BIGINT NOT NULL, + "notified_at" TIMESTAMP(3), + "notify_attempts" INTEGER NOT NULL DEFAULT 0, + "last_notify_error" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "deposit_transactions_pkey" PRIMARY KEY ("deposit_id") +); + +-- CreateTable +CREATE TABLE "block_checkpoints" ( + "checkpoint_id" BIGSERIAL NOT NULL, + "chain_type" VARCHAR(20) NOT NULL, + "last_scanned_block" BIGINT NOT NULL, + "last_scanned_at" TIMESTAMP(3) NOT NULL, + "is_healthy" BOOLEAN NOT NULL DEFAULT true, + "last_error" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "block_checkpoints_pkey" PRIMARY KEY ("checkpoint_id") +); + +-- CreateTable +CREATE TABLE "transaction_requests" ( + "request_id" BIGSERIAL NOT NULL, + "chain_type" VARCHAR(20) NOT NULL, + "source_service" VARCHAR(50) NOT NULL, + "source_order_id" VARCHAR(100) NOT NULL, + "from_address" VARCHAR(42) NOT NULL, + "to_address" VARCHAR(42) NOT NULL, + "value" DECIMAL(36,18) NOT NULL, + "data" TEXT, + "signed_tx" TEXT, + "tx_hash" VARCHAR(66), + "status" VARCHAR(20) NOT NULL DEFAULT 'PENDING', + "gas_limit" BIGINT, + "gas_price" DECIMAL(36,18), + "nonce" INTEGER, + "error_message" TEXT, + "retry_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "transaction_requests_pkey" PRIMARY KEY ("request_id") +); + +-- CreateTable +CREATE TABLE "blockchain_events" ( + "event_id" BIGSERIAL NOT NULL, + "event_type" VARCHAR(50) NOT NULL, + "aggregate_id" VARCHAR(100) NOT NULL, + "aggregate_type" VARCHAR(50) NOT NULL, + "event_data" JSONB NOT NULL, + "chain_type" VARCHAR(20), + "tx_hash" VARCHAR(66), + "occurred_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "blockchain_events_pkey" PRIMARY KEY ("event_id") +); + +-- CreateIndex +CREATE INDEX "idx_user" ON "monitored_addresses"("user_id"); + +-- CreateIndex +CREATE INDEX "idx_chain_active" ON "monitored_addresses"("chain_type", "is_active"); + +-- CreateIndex +CREATE UNIQUE INDEX "monitored_addresses_chain_type_address_key" ON "monitored_addresses"("chain_type", "address"); + +-- CreateIndex +CREATE UNIQUE INDEX "deposit_transactions_tx_hash_key" ON "deposit_transactions"("tx_hash"); + +-- CreateIndex +CREATE INDEX "idx_chain_status" ON "deposit_transactions"("chain_type", "status"); + +-- CreateIndex +CREATE INDEX "idx_deposit_user" ON "deposit_transactions"("user_id"); + +-- CreateIndex +CREATE INDEX "idx_block" ON "deposit_transactions"("block_number"); + +-- CreateIndex +CREATE INDEX "idx_pending_notify" ON "deposit_transactions"("status", "notified_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "block_checkpoints_chain_type_key" ON "block_checkpoints"("chain_type"); + +-- CreateIndex +CREATE INDEX "idx_tx_chain_status" ON "transaction_requests"("chain_type", "status"); + +-- CreateIndex +CREATE INDEX "idx_tx_hash" ON "transaction_requests"("tx_hash"); + +-- CreateIndex +CREATE UNIQUE INDEX "transaction_requests_source_service_source_order_id_key" ON "transaction_requests"("source_service", "source_order_id"); + +-- CreateIndex +CREATE INDEX "idx_event_aggregate" ON "blockchain_events"("aggregate_type", "aggregate_id"); + +-- CreateIndex +CREATE INDEX "idx_event_type" ON "blockchain_events"("event_type"); + +-- CreateIndex +CREATE INDEX "idx_event_chain" ON "blockchain_events"("chain_type"); + +-- CreateIndex +CREATE INDEX "idx_event_occurred" ON "blockchain_events"("occurred_at"); + +-- AddForeignKey +ALTER TABLE "deposit_transactions" ADD CONSTRAINT "deposit_transactions_address_id_fkey" FOREIGN KEY ("address_id") REFERENCES "monitored_addresses"("address_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/services/mining-blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql b/backend/services/mining-blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql new file mode 100644 index 00000000..d1c9aa02 --- /dev/null +++ b/backend/services/mining-blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql @@ -0,0 +1,65 @@ +-- ============================================= +-- Migration: Add system accounts support and recovery mnemonics +-- ============================================= + +-- AlterTable: monitored_addresses +-- Add new columns for system accounts and account_sequence +ALTER TABLE "monitored_addresses" ADD COLUMN "address_type" VARCHAR(20) NOT NULL DEFAULT 'USER'; +ALTER TABLE "monitored_addresses" ADD COLUMN "account_sequence" VARCHAR(20); +ALTER TABLE "monitored_addresses" ADD COLUMN "system_account_type" VARCHAR(50); +ALTER TABLE "monitored_addresses" ADD COLUMN "system_account_id" BIGINT; +ALTER TABLE "monitored_addresses" ADD COLUMN "region_code" VARCHAR(10); + +-- Make user_id nullable (system accounts don't have user_id) +ALTER TABLE "monitored_addresses" ALTER COLUMN "user_id" DROP NOT NULL; + +-- AlterTable: deposit_transactions +-- Add new columns for system accounts and account_sequence +ALTER TABLE "deposit_transactions" ADD COLUMN "address_type" VARCHAR(20) NOT NULL DEFAULT 'USER'; +ALTER TABLE "deposit_transactions" ADD COLUMN "account_sequence" VARCHAR(20); +ALTER TABLE "deposit_transactions" ADD COLUMN "system_account_type" VARCHAR(50); +ALTER TABLE "deposit_transactions" ADD COLUMN "system_account_id" BIGINT; + +-- Make user_id nullable (system account deposits don't have user_id) +ALTER TABLE "deposit_transactions" ALTER COLUMN "user_id" DROP NOT NULL; + +-- CreateTable: recovery_mnemonics +CREATE TABLE "recovery_mnemonics" ( + "id" BIGSERIAL NOT NULL, + "account_sequence" VARCHAR(20) NOT NULL, + "public_key" VARCHAR(130) NOT NULL, + "encrypted_mnemonic" TEXT NOT NULL, + "mnemonic_hash" VARCHAR(64) NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + "is_backed_up" BOOLEAN NOT NULL DEFAULT false, + "revoked_at" TIMESTAMP(3), + "revoked_reason" VARCHAR(200), + "replaced_by_id" BIGINT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "recovery_mnemonics_pkey" PRIMARY KEY ("id") +); + +-- ============================================= +-- CreateIndex: monitored_addresses (new indexes) +-- ============================================= +CREATE INDEX "idx_account_sequence" ON "monitored_addresses"("account_sequence"); +CREATE INDEX "idx_type_active" ON "monitored_addresses"("address_type", "is_active"); +CREATE INDEX "idx_system_account_type" ON "monitored_addresses"("system_account_type"); + +-- Rename unique constraint to match schema +DROP INDEX IF EXISTS "monitored_addresses_chain_type_address_key"; +CREATE UNIQUE INDEX "uk_chain_address" ON "monitored_addresses"("chain_type", "address"); + +-- ============================================= +-- CreateIndex: deposit_transactions (new indexes) +-- ============================================= +CREATE INDEX "idx_deposit_account" ON "deposit_transactions"("account_sequence"); + +-- ============================================= +-- CreateIndex: recovery_mnemonics +-- ============================================= +CREATE UNIQUE INDEX "uk_account_active_mnemonic" ON "recovery_mnemonics"("account_sequence", "status"); +CREATE INDEX "idx_recovery_account" ON "recovery_mnemonics"("account_sequence"); +CREATE INDEX "idx_recovery_public_key" ON "recovery_mnemonics"("public_key"); +CREATE INDEX "idx_recovery_status" ON "recovery_mnemonics"("status"); diff --git a/backend/services/mining-blockchain-service/prisma/migrations/20241209000000_add_outbox_events/migration.sql b/backend/services/mining-blockchain-service/prisma/migrations/20241209000000_add_outbox_events/migration.sql new file mode 100644 index 00000000..4268fe9a --- /dev/null +++ b/backend/services/mining-blockchain-service/prisma/migrations/20241209000000_add_outbox_events/migration.sql @@ -0,0 +1,30 @@ +-- CreateTable +CREATE TABLE "outbox_events" ( + "event_id" BIGSERIAL NOT NULL, + "event_type" VARCHAR(100) NOT NULL, + "aggregate_id" VARCHAR(100) NOT NULL, + "aggregate_type" VARCHAR(50) NOT NULL, + "payload" JSONB NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'PENDING', + "retry_count" INTEGER NOT NULL DEFAULT 0, + "max_retries" INTEGER NOT NULL DEFAULT 10, + "last_error" TEXT, + "next_retry_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "sent_at" TIMESTAMP(3), + "acked_at" TIMESTAMP(3), + + CONSTRAINT "outbox_events_pkey" PRIMARY KEY ("event_id") +); + +-- CreateIndex +CREATE INDEX "idx_outbox_pending" ON "outbox_events"("status", "next_retry_at"); + +-- CreateIndex +CREATE INDEX "idx_outbox_aggregate" ON "outbox_events"("aggregate_type", "aggregate_id"); + +-- CreateIndex +CREATE INDEX "idx_outbox_event_type" ON "outbox_events"("event_type"); + +-- CreateIndex +CREATE INDEX "idx_outbox_created" ON "outbox_events"("created_at"); diff --git a/backend/services/mining-blockchain-service/prisma/schema.prisma b/backend/services/mining-blockchain-service/prisma/schema.prisma new file mode 100644 index 00000000..78230ecd --- /dev/null +++ b/backend/services/mining-blockchain-service/prisma/schema.prisma @@ -0,0 +1,260 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================ +// 监控地址表 +// 存储需要监听充值的地址(用户地址和系统账户地址) +// ============================================ +model MonitoredAddress { + id BigInt @id @default(autoincrement()) @map("address_id") + + chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC + address String @db.VarChar(42) // 0x地址 + + // 地址类型: USER (用户钱包) 或 SYSTEM (系统账户) + addressType String @default("USER") @map("address_type") @db.VarChar(20) + + // 用户地址关联 (addressType = USER 时使用) + accountSequence String? @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) + userId BigInt? @map("user_id") // 保留兼容 + + // 系统账户关联 (addressType = SYSTEM 时使用) + systemAccountType String? @map("system_account_type") @db.VarChar(50) // COST_ACCOUNT, OPERATION_ACCOUNT, etc. + systemAccountId BigInt? @map("system_account_id") + regionCode String? @map("region_code") @db.VarChar(10) // 省市代码(省市账户用) + + isActive Boolean @default(true) @map("is_active") // 是否激活监听 + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + deposits DepositTransaction[] + + @@unique([chainType, address], name: "uk_chain_address") + @@index([accountSequence], name: "idx_account_sequence") + @@index([userId], name: "idx_user") + @@index([addressType, isActive], name: "idx_type_active") + @@index([chainType, isActive], name: "idx_chain_active") + @@index([systemAccountType], name: "idx_system_account_type") + @@map("monitored_addresses") +} + +// ============================================ +// 充值交易表 (Append-Only) +// 记录检测到的所有充值交易 +// ============================================ +model DepositTransaction { + id BigInt @id @default(autoincrement()) @map("deposit_id") + + chainType String @map("chain_type") @db.VarChar(20) + txHash String @unique @map("tx_hash") @db.VarChar(66) + + fromAddress String @map("from_address") @db.VarChar(42) + toAddress String @map("to_address") @db.VarChar(42) + + tokenContract String @map("token_contract") @db.VarChar(42) // USDT合约地址 + amount Decimal @db.Decimal(78, 0) // 原始金额 (wei单位,无小数) + amountFormatted Decimal @map("amount_formatted") @db.Decimal(36, 8) // 格式化金额 (支持大额) + + blockNumber BigInt @map("block_number") + blockTimestamp DateTime @map("block_timestamp") + logIndex Int @map("log_index") + + // 确认状态 + confirmations Int @default(0) + status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED + + // 关联 - 使用 accountSequence 作为跨服务主键 + addressId BigInt @map("address_id") + addressType String @default("USER") @map("address_type") @db.VarChar(20) // USER 或 SYSTEM + + // 用户地址关联 + accountSequence String? @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) + userId BigInt? @map("user_id") // 保留兼容 + + // 系统账户关联(当 addressType = SYSTEM 时) + systemAccountType String? @map("system_account_type") @db.VarChar(50) + systemAccountId BigInt? @map("system_account_id") + + // 通知状态 + notifiedAt DateTime? @map("notified_at") + notifyAttempts Int @default(0) @map("notify_attempts") + lastNotifyError String? @map("last_notify_error") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + monitoredAddress MonitoredAddress @relation(fields: [addressId], references: [id]) + + @@index([chainType, status], name: "idx_chain_status") + @@index([accountSequence], name: "idx_deposit_account") + @@index([userId], name: "idx_deposit_user") + @@index([blockNumber], name: "idx_block") + @@index([status, notifiedAt], name: "idx_pending_notify") + @@map("deposit_transactions") +} + +// ============================================ +// 区块扫描检查点 (每条链一条记录) +// 记录扫描进度,用于断点续扫 +// ============================================ +model BlockCheckpoint { + id BigInt @id @default(autoincrement()) @map("checkpoint_id") + + chainType String @unique @map("chain_type") @db.VarChar(20) + + lastScannedBlock BigInt @map("last_scanned_block") + lastScannedAt DateTime @map("last_scanned_at") + + // 健康状态 + isHealthy Boolean @default(true) @map("is_healthy") + lastError String? @map("last_error") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("block_checkpoints") +} + +// ============================================ +// 交易广播请求表 +// 记录待广播和已广播的交易 +// ============================================ +model TransactionRequest { + id BigInt @id @default(autoincrement()) @map("request_id") + + chainType String @map("chain_type") @db.VarChar(20) + + // 请求来源 + sourceService String @map("source_service") @db.VarChar(50) + sourceOrderId String @map("source_order_id") @db.VarChar(100) + + // 交易数据 + fromAddress String @map("from_address") @db.VarChar(42) + toAddress String @map("to_address") @db.VarChar(42) + value Decimal @db.Decimal(36, 18) + data String? @db.Text // 合约调用数据 + + // 签名数据 (由 MPC 服务提供) + signedTx String? @map("signed_tx") @db.Text + + // 广播结果 + txHash String? @map("tx_hash") @db.VarChar(66) + status String @default("PENDING") @db.VarChar(20) // PENDING, SIGNED, BROADCASTED, CONFIRMED, FAILED + + // Gas 信息 + gasLimit BigInt? @map("gas_limit") + gasPrice Decimal? @map("gas_price") @db.Decimal(36, 18) + nonce Int? + + // 错误信息 + errorMessage String? @map("error_message") @db.Text + retryCount Int @default(0) @map("retry_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([sourceService, sourceOrderId], name: "uk_source_order") + @@index([chainType, status], name: "idx_tx_chain_status") + @@index([txHash], name: "idx_tx_hash") + @@map("transaction_requests") +} + +// ============================================ +// 账户恢复助记词 +// 与账户序列号关联,用于账户恢复验证 +// ============================================ +model RecoveryMnemonic { + id BigInt @id @default(autoincrement()) + accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号 (格式: D + YYMMDD + 5位序号) + publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥 + + // 助记词存储 (加密) + encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词 + mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证) + + // 状态管理 + status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED + isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份 + + // 挂失/更换相关 + revokedAt DateTime? @map("revoked_at") + revokedReason String? @map("revoked_reason") @db.VarChar(200) + replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代 + + createdAt DateTime @default(now()) @map("created_at") + + @@unique([accountSequence, status], name: "uk_account_active_mnemonic") // 一个账户只有一个ACTIVE助记词 + @@index([accountSequence], name: "idx_recovery_account") + @@index([publicKey], name: "idx_recovery_public_key") + @@index([status], name: "idx_recovery_status") + @@map("recovery_mnemonics") +} + +// ============================================ +// Outbox 事件表 (发件箱模式) +// 保证事件发布的可靠性 +// ============================================ +model OutboxEvent { + id BigInt @id @default(autoincrement()) @map("event_id") + + // 事件信息 + eventType String @map("event_type") @db.VarChar(100) + aggregateId String @map("aggregate_id") @db.VarChar(100) + aggregateType String @map("aggregate_type") @db.VarChar(50) + payload Json @map("payload") + + // 发送状态: PENDING -> SENT -> ACKED / FAILED + status String @default("PENDING") @db.VarChar(20) + + // 重试信息 + retryCount Int @default(0) @map("retry_count") + maxRetries Int @default(10) @map("max_retries") + lastError String? @map("last_error") @db.Text + nextRetryAt DateTime? @map("next_retry_at") + + // 时间戳 + createdAt DateTime @default(now()) @map("created_at") + sentAt DateTime? @map("sent_at") + ackedAt DateTime? @map("acked_at") + + @@index([status, nextRetryAt], name: "idx_outbox_pending") + @@index([aggregateType, aggregateId], name: "idx_outbox_aggregate") + @@index([eventType], name: "idx_outbox_event_type") + @@index([createdAt], name: "idx_outbox_created") + @@map("outbox_events") +} + +// ============================================ +// 区块链事件日志 (Append-Only 审计) +// ============================================ +model BlockchainEvent { + id BigInt @id @default(autoincrement()) @map("event_id") + + eventType String @map("event_type") @db.VarChar(50) + + aggregateId String @map("aggregate_id") @db.VarChar(100) + aggregateType String @map("aggregate_type") @db.VarChar(50) + + eventData Json @map("event_data") + + chainType String? @map("chain_type") @db.VarChar(20) + txHash String? @map("tx_hash") @db.VarChar(66) + + occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6) + + @@index([aggregateType, aggregateId], name: "idx_event_aggregate") + @@index([eventType], name: "idx_event_type") + @@index([chainType], name: "idx_event_chain") + @@index([occurredAt], name: "idx_event_occurred") + @@map("blockchain_events") +} diff --git a/backend/services/mining-blockchain-service/scripts/README.md b/backend/services/mining-blockchain-service/scripts/README.md new file mode 100644 index 00000000..cebecae4 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/README.md @@ -0,0 +1,235 @@ +# 测试和健康检查脚本 + +## 使用流程 + +### 1️⃣ 启动基础服务 + +```bash +# 启动 Redis +redis-server --daemonize yes + +# 或使用 Docker +docker compose up -d redis +``` + +### 2️⃣ 启动 Blockchain Service + +```bash +# 在项目根目录 +npm run start:dev +``` + +### 3️⃣ 运行健康检查 + +```bash +# 进入 scripts 目录 +cd scripts + +# 运行健康检查 +./health-check.sh +``` + +**期望输出:** +``` +🏥 开始健康检查... + +=== 数据库服务 === +Checking PostgreSQL ... ✓ OK +=== 缓存服务 === +Checking Redis ... ✓ OK +=== 消息队列服务 === +Checking Kafka ... ✓ OK +=== 区块链 RPC === +Checking KAVA RPC ... ✓ OK +Checking BSC RPC ... ✓ OK +=== 应用服务 === +Checking Blockchain Service ... ✓ OK +=== API 文档 === +Checking Swagger UI ... ✓ OK + +====================================== +健康检查完成! +正常: 7 +异常: 0 +====================================== +✓ 所有服务正常! + +现在可以运行测试: + ./scripts/quick-test.sh +``` + +### 4️⃣ 运行快速功能测试 + +```bash +./quick-test.sh +``` + +这个脚本会自动测试所有核心功能: +- ✅ 健康检查 +- ✅ 余额查询(单链/多链) +- ✅ 地址派生 +- ✅ 用户地址查询 +- ✅ 错误场景处理 +- ✅ API 文档可访问性 + +--- + +## 脚本说明 + +### `health-check.sh` +- **作用**: 检查所有依赖服务是否正常运行 +- **使用场景**: 部署前、调试时 +- **检查项目**: + - PostgreSQL 数据库 + - Redis 缓存 + - Kafka 消息队列 + - KAVA/BSC RPC 端点 + - Blockchain Service 应用 + +### `quick-test.sh` +- **作用**: 快速测试所有核心 API 功能 +- **使用场景**: 验证功能完整性、回归测试 +- **前置条件**: `health-check.sh` 通过 + +### `start-all.sh` +- **作用**: 一键启动所有服务 +- **使用场景**: 初次启动、快速启动环境 +- **前置条件**: 依赖已安装 + +### `stop-service.sh` +- **作用**: 停止 Blockchain Service +- **使用场景**: 需要停止服务时 + +### `rebuild-kafka.sh` +- **作用**: 重建 Kafka 容器 +- **使用场景**: Kafka 配置变更后 + +--- + +## 主要 API 端点 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/health` | GET | 健康检查 | +| `/health/ready` | GET | 就绪检查 | +| `/balance` | GET | 查询单链余额 | +| `/balance/multi-chain` | GET | 查询多链余额 | +| `/internal/derive-address` | POST | 从公钥派生地址 | +| `/internal/user/:userId/addresses` | GET | 获取用户地址 | +| `/api` | GET | Swagger 文档 | + +--- + +## 部署脚本 (deploy.sh) + +主部署脚本位于项目根目录,提供以下命令: + +```bash +# 构建 Docker 镜像 +./deploy.sh build + +# 启动服务 +./deploy.sh start + +# 停止服务 +./deploy.sh stop + +# 重启服务 +./deploy.sh restart + +# 查看日志 +./deploy.sh logs + +# 健康检查 +./deploy.sh health + +# 运行数据库迁移 +./deploy.sh migrate + +# 打开 Prisma Studio +./deploy.sh prisma-studio + +# 进入容器 shell +./deploy.sh shell + +# 查询余额 +./deploy.sh check-balance KAVA 0x1234... + +# 触发区块扫描 +./deploy.sh scan-blocks +``` + +--- + +## 常见问题 + +### Q: 为什么 RPC 检查失败? +**A:** 检查网络连接,或者 RPC 端点可能暂时不可用 + +### Q: Redis 启动失败? +**A:** 检查是否已经在运行 +```bash +ps aux | grep redis +redis-cli shutdown # 如果已运行 +redis-server --daemonize yes +``` + +### Q: Kafka 连接失败? +**A:** 重建 Kafka 容器 +```bash +./scripts/rebuild-kafka.sh +``` + +--- + +## 完整测试流程 + +```bash +# 1. 进入项目目录 +cd ~/work/rwadurian/backend/services/blockchain-service + +# 2. 安装依赖(首次) +npm install + +# 3. 生成 Prisma Client +npx prisma generate + +# 4. 运行数据库迁移 +npx prisma migrate dev + +# 5. 启动所有服务 +./scripts/start-all.sh + +# 6. 运行健康检查 +./scripts/health-check.sh + +# 7. 运行快速测试 +./scripts/quick-test.sh + +# 8. 运行完整测试 +npm test +npm run test:e2e +``` + +--- + +## 区块链特定测试 + +### 测试余额查询 +```bash +# KAVA 链 +curl "http://localhost:3012/balance?chainType=KAVA&address=0x..." + +# 多链查询 +curl "http://localhost:3012/balance/multi-chain?address=0x..." +``` + +### 测试地址派生 +```bash +curl -X POST "http://localhost:3012/internal/derive-address" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "12345", + "publicKey": "0x02..." + }' +``` diff --git a/backend/services/mining-blockchain-service/scripts/deploy-kava-simple.ts b/backend/services/mining-blockchain-service/scripts/deploy-kava-simple.ts new file mode 100644 index 00000000..4e3d4902 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/deploy-kava-simple.ts @@ -0,0 +1,134 @@ +/** + * Deploy TestUSDT to KAVA Testnet using inline Solidity compilation + */ +import { ethers } from 'ethers'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const solc = require('solc'); + +const KAVA_TESTNET_RPC = 'https://evm.testnet.kava.io'; +const KAVA_TESTNET_CHAIN_ID = 2221; +const privateKey = '0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'; + +const sourceCode = ` +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract TestUSDT { + string public constant name = "Test USDT"; + string public constant symbol = "USDT"; + uint8 public constant decimals = 6; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor() { + _mint(msg.sender, 1000000 * 1e6); + } + + function _mint(address to, uint256 amount) internal { + totalSupply += amount; + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) public returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + function approve(address spender, uint256 amount) public returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + allowance[from][msg.sender] -= amount; + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + return true; + } + + function mint(uint256 amount) external { + _mint(msg.sender, amount); + } + + function faucet() external { + _mint(msg.sender, 10000 * 1e6); + } +} +`; + +async function deploy() { + console.log('🔨 Compiling contract...'); + + const input = { + language: 'Solidity', + sources: { 'TestUSDT.sol': { content: sourceCode } }, + settings: { outputSelection: { '*': { '*': ['abi', 'evm.bytecode'] } } }, + }; + + const output = JSON.parse(solc.compile(JSON.stringify(input))); + + if (output.errors) { + const errors = output.errors.filter((e: any) => e.severity === 'error'); + if (errors.length > 0) { + console.error('❌ Compilation errors:', errors); + process.exit(1); + } + } + + const contract = output.contracts['TestUSDT.sol']['TestUSDT']; + const bytecode = contract.evm.bytecode.object; + const compiledAbi = contract.abi; + + console.log('🌐 Connecting to KAVA Testnet...'); + const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC, { + chainId: KAVA_TESTNET_CHAIN_ID, + name: 'kava-testnet', + }); + + const wallet = new ethers.Wallet(privateKey, provider); + console.log(`📍 Deployer: ${wallet.address}`); + + const balance = await provider.getBalance(wallet.address); + console.log(`💰 Balance: ${ethers.formatEther(balance)} TKAVA`); + + if (balance < ethers.parseEther('0.01')) { + console.error('❌ Insufficient TKAVA'); + process.exit(1); + } + + console.log('📦 Deploying...'); + const factory = new ethers.ContractFactory(compiledAbi, bytecode, wallet); + const deployedContract = await factory.deploy({ + gasLimit: 5000000, + }); + + console.log(`⏳ Waiting for confirmation...`); + console.log(` TX: https://testnet.kavascan.com/tx/${deployedContract.deploymentTransaction()?.hash}`); + + await deployedContract.waitForDeployment(); + + const address = await deployedContract.getAddress(); + console.log(''); + console.log('='.repeat(60)); + console.log(`✅ SUCCESS! TestUSDT deployed on KAVA Testnet`); + console.log(`📋 Contract Address: ${address}`); + console.log('='.repeat(60)); + console.log(''); + console.log(`🔗 KavaScan: https://testnet.kavascan.com/address/${address}`); + console.log(''); + console.log('Next: Update KAVA_USDT_CONTRACT in .env'); +} + +deploy().catch((e) => { + console.error('❌ Error:', e.message); + process.exit(1); +}); diff --git a/backend/services/mining-blockchain-service/scripts/deploy-test-usdt-kava.ts b/backend/services/mining-blockchain-service/scripts/deploy-test-usdt-kava.ts new file mode 100644 index 00000000..d1b591d3 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/deploy-test-usdt-kava.ts @@ -0,0 +1,106 @@ +/** + * Deploy TestUSDT to KAVA Testnet + * + * Usage: + * npx ts-node scripts/deploy-test-usdt-kava.ts + * + * Example: + * npx ts-node scripts/deploy-test-usdt-kava.ts 0xabc123... + * + * Get KAVA Testnet TKAVA from: https://faucet.kava.io + */ + +import { ethers, ContractFactory } from 'ethers'; + +// KAVA Testnet 配置 +const KAVA_TESTNET_RPC = 'https://evm.testnet.kava.io'; +const KAVA_TESTNET_CHAIN_ID = 2221; + +// TestUSDT 合约 ABI +const CONTRACT_ABI = [ + 'constructor()', + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'function transferFrom(address from, address to, uint256 amount) returns (bool)', + 'function mint(uint256 amount)', + 'function mintUsdt(uint256 usdtAmount)', + 'function mintTo(address to, uint256 amount)', + 'function faucet()', + 'function owner() view returns (address)', + 'event Transfer(address indexed from, address indexed to, uint256 value)', + 'event Approval(address indexed owner, address indexed spender, uint256 value)', +]; + +// TestUSDT_Flat.sol 编译后的 bytecode +const CONTRACT_BYTECODE = `0x608060405234801561001057600080fd5b506040518060400160405280600981526020017f54657374205553445400000000000000000000000000000000000000000000008152506040518060400160405280600481526020017f55534454000000000000000000000000000000000000000000000000000000008152508160039081610091919061042e565b5080600490816100a1919061042e565b5050503360008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555061019f336006600a61019591906105ff565b620f42406101a4565b61064a565b80600260008282546101b691906106a5565b92505081905550806005600084815260200190815260200160002060008282546101e0919061069b565b925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161024591906106e3565b60405180910390a35050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806102d257607f821691505b6020821081036102e5576102e461028b565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b60006008830261034d7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82610310565b6103578683610310565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b600061039e6103996103948461036f565b610379565b61036f565b9050919050565b6000819050919050565b6103b883610383565b6103cc6103c4826103a5565b84845461031d565b825550505050565b600090565b6103e16103d4565b6103ec8184846103af565b505050565b5b81811015610410576104056000826103d9565b6001810190506103f2565b5050565b601f82111561045d5761042681610feb565b61042f84610300565b8101602085101561043e578190505b61045261044a85610300565b8301826103f1565b50505b505050565b600082821c905092915050565b60006104786000198460080261045a565b1980831691505092915050565b60006104918383610467565b9150826002028217905092915050565b6104aa82610251565b67ffffffffffffffff8111156104c3576104c261025c565b5b6104cd82546102ba565b6104d8828285610414565b600060209050601f83116001811461050b57600084156104f9578287015190505b6105038582610485565b86555061056b565b601f19841661051986610feb565b60005b828110156105415784890151825560018201915060208501945060208101905061051c565b8683101561055e578489015161055a601f891682610467565b8355505b6001600288020188555050505b505050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b60018511156105f9578086048111156105d5576105d4610573565b5b60018516156105e45780820291505b80810290506105f2856105a2565b94506105b9565b94509492505050565b60006106178683851115610467565b5b8015610628578291508190506106475b509250929050565b600061063c858461064a565b9150826002028217905092915050565b60006106578261036f565b9150826106675761066661062d565b5b828202905092915050565b600061067d8261036f565b91508282019050828112156106955761069461062d565b5b92915050565b6000819050919050565b60006106b08261069b565b91506106bb8361069b565b92508282019050808211156106d3576106d2610573565b5b92915050565b6106e28161069b565b82525050565b60006020820190506106fd60008301846106d9565b92915050565b610c0d8061070c6000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806370a0823111610097578063a9059cbb11610066578063a9059cbb14610286578063dd62ed3e146102b6578063de0e9a3e146102e6578063e1f21c6714610302576100f5565b806370a08231146101f05780638da5cb5b1461022057806395d89b411461023e578063a0712d681461025c576100f5565b806323b872dd116100d357806323b872dd1461016a578063313ce5671461019a57806340c10f19146101b85780636a627842146101d4576100f5565b806306fdde03146100fa578063095ea7b31461011857806318160ddd14610148575b600080fd5b61010261031e565b60405161010f9190610981565b60405180910390f35b610132600480360381019061012d91906109ec565b6103b0565b60405161013f9190610a47565b60405180910390f35b6101506103d3565b60405161015d9190610a71565b60405180910390f35b610184600480360381019061017f9190610a8c565b6103dd565b6040516101919190610a47565b60405180910390f35b6101a261040c565b6040516101af9190610afb565b60405180910390f35b6101d260048036038101906101cd91906109ec565b610415565b005b6101ee60048036038101906101e99190610b16565b6104a9565b005b61020a60048036038101906102059190610b16565b6104ce565b6040516102179190610a71565b60405180910390f35b610228610517565b6040516102359190610b52565b60405180910390f35b610246610540565b6040516102539190610981565b60405180910390f35b61027660048036038101906102719190610b6d565b6105d2565b005b6102a0600480360381019061029b91906109ec565b6105fe565b6040516102ad9190610a47565b60405180910390f35b6102d060048036038101906102cb9190610b9a565b610621565b6040516102dd9190610a71565b60405180910390f35b61030060048036038101906102fb9190610b6d565b6106a8565b005b61031c60048036038101906103179190610a8c565b6106d4565b005b60606003805461032d90610c09565b80601f016020809104026020016040519081016040528092919081815260200182805461035990610c09565b80156103a65780601f1061037b576101008083540402835291602001916103a6565b820191906000526020600020905b81548152906001019060200180831161038957829003601f168201915b5050505050905090565b6000806103bb610763565b90506103c881858561076b565b600191505092915050565b6000600254905090565b6000806103e8610763565b90506103f5858285610934565b6104008585856109c0565b60019150509392505050565b60006006905090565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146104a3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161049a90610c86565b60405180910390fd5b6104a582826106a8565b5050565b6104cb336127106006600a6104be9190610c3a565b6104c89190610c85565b6106a8565b50565b6000600560008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60606004805461054f90610c09565b80601f016020809104026020016040519081016040528092919081815260200182805461057b90610c09565b80156105c85780601f1061059d576101008083540402835291602001916105c8565b820191906000526020600020905b8154815290600101906020018083116105ab57829003601f168201915b5050505050905090565b6105fb336006600a6105e49190610c3a565b826105ef9190610c85565b6106a8565b50565b600080610609610763565b90506106168185856109c0565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6106d133826006600a6106bb9190610c3a565b6106c59190610c85565b6106a8565b50565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610762576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161075990610c86565b60405180910390fd5b50505050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107df576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107d690610d18565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361084e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161084590610daa565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258360405161092c9190610a71565b60405180910390a3505050565b60006109408484610621565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146109ba57818110156109ac576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109a390610e16565b60405180910390fd5b6109b9848484840361076b565b5b50505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610a005780820151818401526020810190506109e5565b60008484015250505050565b6000601f19601f8301169050919050565b6000610a28826109c5565b610a3281856109d0565b9350610a428185602086016109e1565b610a4b81610a0c565b840191505092915050565b60006020820190508181036000830152610a708184610a1d565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610aa882610a7d565b9050919050565b610ab881610a9d565b8114610ac357600080fd5b50565b600081359050610ad581610aaf565b92915050565b6000819050919050565b610aee81610adb565b8114610af957600080fd5b50565b600081359050610b0b81610ae5565b92915050565b60008060408385031215610b2857610b27610a78565b5b6000610b3685828601610ac6565b9250506020610b4785828601610afc565b9150509250929050565b60008115159050919050565b610b6681610b51565b82525050565b6000602082019050610b816000830184610b5d565b92915050565b610b9081610adb565b82525050565b6000602082019050610bab6000830184610b87565b92915050565b600080600060608486031215610bca57610bc9610a78565b5b6000610bd886828701610ac6565b9350506020610be986828701610ac6565b9250506040610bfa86828701610afc565b9150509250925092565b600060ff82169050919050565b610c1a81610c04565b82525050565b6000602082019050610c356000830184610c11565b92915050565b6000602082019050610c506000830184610b87565b92915050565b610c5f81610a9d565b82525050565b6000602082019050610c7a6000830184610c56565b92915050565b6000602082019050610c956000830184610b87565b92915050565b60008060408385031215610cb257610cb1610a78565b5b6000610cc085828601610ac6565b9250506020610cd185828601610ac6565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610d2257607f821691505b602082108103610d3557610d34610cdb565b5b50919050565b7f4e6f74206f776e65720000000000000000000000000000000000000000000000600082015250565b6000610d716009836109d0565b9150610d7c82610d3b565b602082019050919050565b60006020820190508181036000830152610da081610d64565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008160011c9050919050565b6000808291508390505b6001851115610e2d57808604811115610e0957610e08610da7565b5b6001851615610e185780820291505b8081029050610e2685610dd6565b9450610ded565b94509492505050565b600082610e465760019050610f02565b81610e545760009050610f02565b8160018114610e6a5760028114610e7457610ea3565b6001915050610f02565b60ff841115610e8657610e85610da7565b5b8360020a915084821115610e9d57610e9c610da7565b5b50610f02565b5060208310610133831016604e8410600b8410161715610ed85782820a905083811115610ed357610ed2610da7565b5b610f02565b610ee58484846001610de3565b92509050818404811115610efc57610efb610da7565b5b81810290505b9392505050565b6000610f1482610adb565b9150610f1f83610c04565b9250610f4c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8484610e36565b905092915050565b6000610f5f82610adb565b9150610f6a83610adb565b9250828202610f7881610adb565b91508282048414831517610f8f57610f8e610da7565b5b5092915050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000610ff26024836109d0565b9150610ffd82610f96565b604082019050919050565b6000602082019050818103600083015261102181610fe5565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b60006110846022836109d0565b915061108f82611028565b604082019050919050565b600060208201905081810360008301526110b381611077565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b60006110f0601d836109d0565b91506110fb826110ba565b602082019050919050565b6000602082019050818103600083015261111f816110e3565b905091505056fea2646970667358221220`; + +async function main() { + const privateKey = process.argv[2]; + + if (!privateKey) { + console.error(` +❌ Usage: npx ts-node scripts/deploy-test-usdt-kava.ts + +Steps: +1. Get TKAVA from https://faucet.kava.io +2. Export your private key from MetaMask +3. Run: npx ts-node scripts/deploy-test-usdt-kava.ts 0xYourPrivateKey + `); + process.exit(1); + } + + console.log('🚀 Deploying TestUSDT to KAVA Testnet...\n'); + + // 连接到 KAVA Testnet + const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC, { + chainId: KAVA_TESTNET_CHAIN_ID, + name: 'kava-testnet', + }); + + // 创建钱包 + const wallet = new ethers.Wallet(privateKey, provider); + console.log(`📍 Deployer address: ${wallet.address}`); + + // 检查余额 + const balance = await provider.getBalance(wallet.address); + console.log(`💰 Balance: ${ethers.formatEther(balance)} TKAVA`); + + if (balance < ethers.parseEther('0.01')) { + console.error('\n❌ Insufficient TKAVA. Get some from https://faucet.kava.io'); + process.exit(1); + } + + // 部署合约 + console.log('\n📦 Deploying contract...'); + const factory = new ContractFactory(CONTRACT_ABI, CONTRACT_BYTECODE, wallet); + const contract = await factory.deploy(); + + console.log(`⏳ Waiting for confirmation...`); + console.log(` Transaction: https://testnet.kavascan.com/tx/${contract.deploymentTransaction()?.hash}`); + + await contract.waitForDeployment(); + const contractAddress = await contract.getAddress(); + + console.log(` +✅ TestUSDT deployed successfully on KAVA Testnet! + +📋 Contract Address: ${contractAddress} +🔗 KavaScan: https://testnet.kavascan.com/address/${contractAddress} + +Next steps: +1. Update .env: KAVA_USDT_CONTRACT=${contractAddress} +2. Call faucet() to mint 10,000 USDT for testing +3. Or call mintUsdt(100000) to mint 100,000 USDT + `); +} + +main().catch((error) => { + console.error('❌ Deployment failed:', error.message); + process.exit(1); +}); diff --git a/backend/services/mining-blockchain-service/scripts/deploy-test-usdt.ts b/backend/services/mining-blockchain-service/scripts/deploy-test-usdt.ts new file mode 100644 index 00000000..2bdd1292 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/deploy-test-usdt.ts @@ -0,0 +1,107 @@ +/** + * Deploy TestUSDT to BSC Testnet + * + * Usage: + * npx ts-node scripts/deploy-test-usdt.ts + * + * Example: + * npx ts-node scripts/deploy-test-usdt.ts 0xabc123... + * + * Get BSC Testnet tBNB from: https://www.bnbchain.org/en/testnet-faucet + */ + +import { ethers, ContractFactory } from 'ethers'; + +// BSC Testnet 配置 +const BSC_TESTNET_RPC = 'https://data-seed-prebsc-1-s1.binance.org:8545'; +const BSC_TESTNET_CHAIN_ID = 97; + +// TestUSDT 合约 ABI 和 Bytecode +const CONTRACT_ABI = [ + 'constructor()', + 'function name() view returns (string)', + 'function symbol() view returns (string)', + 'function decimals() view returns (uint8)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function allowance(address owner, address spender) view returns (uint256)', + 'function transferFrom(address from, address to, uint256 amount) returns (bool)', + 'function mint(uint256 amount)', + 'function mintUsdt(uint256 usdtAmount)', + 'function mintTo(address to, uint256 amount)', + 'function faucet()', + 'function owner() view returns (address)', + 'event Transfer(address indexed from, address indexed to, uint256 value)', + 'event Approval(address indexed owner, address indexed spender, uint256 value)', +]; + +// 编译后的 bytecode (TestUSDT_Flat.sol) +const CONTRACT_BYTECODE = + '0x608060405234801561001057600080fd5b506040518060400160405280600981526020017f5465737420555344540000000000000000000000000000000000000000000000815250604051806040016040528060048152602001635553445460e01b81525081600390816100739190610293565b5060046100808282610293565b505050336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506100e633660f4240600660ff16600a6100dc919061049c565b6100e691906104e7565b6100eb565b610529565b80600260008282546100fd9190610529565b90915550506001600160a01b0382166000818152600560209081526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061018057607f821691505b6020821081036101a057634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156101f057600081815260208120601f850160051c810160208610156101cd5750805b601f850160051c820191505b818110156101ec578281556001016101d9565b5050505b505050565b81516001600160401b0381111561020e5761020e610156565b6102228161021c845461016c565b846101a6565b602080601f831160018114610257576000841561023f5750858301515b600019600386901b1c1916600185901b1785556101ec565b600085815260208120601f198616915b8281101561028657888601518255948401946001909101908401610267565b50858210156102a45787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b600052601160045260246000fd5b600181815b808511156103055781600019048211156102eb576102eb6102b4565b808516156102f857918102915b93841c93908002906102cf565b509250929050565b60008261031c57506001610496565b8161032957506000610496565b816001811461033f576002811461034957610365565b6001915050610496565b60ff84111561035a5761035a6102b4565b50506001821b610496565b5060208310610133831016604e8410600b8410161715610388575081810a610496565b61039283836102ca565b80600019048211156103a6576103a66102b4565b029392505050565b60006103bd60ff84168361030d565b9392505050565b600082198211156103d7576103d76102b4565b500190565b6000826103f957634e487b7160e01b600052601260045260246000fd5b500490565b600060208083528351808285015260005b8181101561042b5785810183015185820160400152820161040f565b8181111561043d576000604083870101525b50601f01601f1916929092016040019392505050565b60006020828403121561046557600080fd5b81356001600160a01b038116811461047c57600080fd5b9392505050565b60006020828403121561049557600080fd5b5035919050565b60006103bd83836103ae565b80820281158282048414176104bf576104bf6102b4565b92915050565b6000602082840312156104d757600080fd5b5051919050565b60006104bd83836103ae565b80820281158282048414176104bf576104bf6102b4565b634e487b7160e01b600052601260045260246000fd5b60008261052857610528610501565b500690565b808201808211156104bf576104bf6102b4565b610984806105476000396000f3fe608060405234801561001057600080fd5b50600436106101005760003560e01c806370a0823111610097578063a9059cbb11610066578063a9059cbb14610215578063de0e9a3e14610228578063dd62ed3e1461023b578063e1f21c671461027457600080fd5b806370a08231146101bc5780638da5cb5b146101e557806395d89b4114610200578063a0712d681461020857600080fd5b806323b872dd116100d357806323b872dd14610166578063313ce5671461017957806340c10f19146101885780636a627842146101a957600080fd5b806306fdde0314610105578063095ea7b31461012357806318160ddd146101465780631e83409a14610158575b600080fd5b61010d610287565b60405161011a91906107e4565b60405180910390f35b610136610131366004610853565b610319565b604051901515815260200161011a565b6002545b60405190815260200161011a565b61014a61271081565b61013661017436600461087d565b610333565b60405160068152602001610161565b61019b610196366004610853565b610357565b005b61019b6101b73660046108b9565b6103a1565b61014a6101ca3660046108b9565b6001600160a01b031660009081526005602052604090205490565b6000546040516001600160a01b03909116815260200161011a565b61010d6103c6565b61019b6102163660046108d4565b6103d5565b610136610223366004610853565b6103fb565b61019b6102363660046108d4565b610409565b61014a6102493660046108ed565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b61019b61028236600461087d565b61042c565b60606003805461029690610920565b80601f01602080910402602001604051908101604052809291908181526020018280546102c290610920565b801561030f5780601f106102e45761010080835404028352916020019161030f565b820191906000526020600020905b8154815290600101906020018083116102f257829003601f168201915b5050505050905090565b6000336103278185856104a0565b60019150505b92915050565b6000336103418582856105c4565b61034c85858561063e565b506001949350505050565b6000546001600160a01b0316331461038a5760405162461bcd60e51b81526004016103819061095a565b60405180910390fd5b61039d82826006600a610a6e57610790565b5050565b6103c3336127106006600a6103ba91906106e2565b6103039190610a6e565b50565b60606004805461029690610920565b6103c3336006600a6103e791906106e2565b6103f19084610a6e565b610790565b60003361032781858561063e565b6103c333826006600a61041c91906106e2565b6104269190610a6e565b610790565b6000546001600160a01b031633146104565760405162461bcd60e51b81526004016103819061095a565b61049b83838080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050506001600160a01b038616915084905061063e565b505050565b6001600160a01b0383166105025760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608401610381565b6001600160a01b0382166105635760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608401610381565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b6001600160a01b038381166000908152600160209081526040808320938616835292905220546000198114610638578181101561062b5760405162461bcd60e51b815260206004820152600560248201526422a9292a1960d91b6044820152606401610381565b61063884848484036104a0565b50505050565b6001600160a01b03831661067f5760405162461bcd60e51b81526020600482015260086024820152672166726f6d203d3d60c01b6044820152606401610381565b6001600160a01b0382166106be5760405162461bcd60e51b8152602060048201526006602482015265021746f203d3d60d41b6044820152606401610381565b6001600160a01b0383166000908152600560205260409020548181101561070c5760405162461bcd60e51b8152602060048201526002602482015261212160f01b6044820152606401610381565b6001600160a01b0380851660008181526005602052604080822086860390559286168082529083902080548601905591517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906107689086815260200190565b60405180910390a350505050565b600061078182610a85565b61078d90610a9a565b92915050565b80600260008282546107a59190610ab0565b90915550506001600160a01b0382166000818152600560209081526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91015b60405180910390a35050565b600060208083528351808285015260005b81811015610811578581018301518582016040015282016107f5565b81811115610823576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461085057600080fd5b919050565b6000806040838503121561086857600080fd5b61087183610839565b946020939093013593505050565b60008060006060848603121561089457600080fd5b61089d84610839565b92506108ab60208501610839565b9150604084013590509250925092565b6000602082840312156108cd57600080fd5b5035919050565b6000602082840312156108e857600080fd5b6108f182610839565b9392505050565b6000806040838503121561090b57600080fd5b61091483610839565b915061092260208401610839565b90509250929050565b600181811c9082168061093f57607f821691505b60208210810361095f57634e487b7160e01b600052602260045260246000fd5b50919050565b6020808252600990820152682737ba1037bbb732b960b91b604082015260600190565b634e487b7160e01b600052601160045260246000fd5b600181815b808511156109d95781600019048211156109bf576109bf610988565b808516156109cc57918102915b93841c93908002906109a3565b509250929050565b6000826109f05750600161032d565b816109fd5750600061032d565b8160018114610a135760028114610a1d57610a39565b600191505061032d565b60ff841115610a2e57610a2e610988565b50506001821b61032d565b5060208310610133831016604e8410600b8410161715610a5c575081810a61032d565b610a66838361099e565b8060001904821115610a7a57610a7a610988565b029392505050565b60006108f160ff8416836109e1565b8082028115828204841417610aa857610aa8610988565b92915050565b8082018082111561032d5761032d61098856fea264697066735822122012'; + +async function main() { + const privateKey = process.argv[2]; + + if (!privateKey) { + console.error(` +❌ Usage: npx ts-node scripts/deploy-test-usdt.ts + +Steps: +1. Get tBNB from https://www.bnbchain.org/en/testnet-faucet +2. Export your private key from MetaMask +3. Run: npx ts-node scripts/deploy-test-usdt.ts 0xYourPrivateKey + `); + process.exit(1); + } + + console.log('🚀 Deploying TestUSDT to BSC Testnet...\n'); + + // 连接到 BSC Testnet + const provider = new ethers.JsonRpcProvider(BSC_TESTNET_RPC, { + chainId: BSC_TESTNET_CHAIN_ID, + name: 'bsc-testnet', + }); + + // 创建钱包 + const wallet = new ethers.Wallet(privateKey, provider); + console.log(`📍 Deployer address: ${wallet.address}`); + + // 检查余额 + const balance = await provider.getBalance(wallet.address); + console.log(`💰 Balance: ${ethers.formatEther(balance)} tBNB`); + + if (balance < ethers.parseEther('0.01')) { + console.error('\n❌ Insufficient tBNB. Get some from https://www.bnbchain.org/en/testnet-faucet'); + process.exit(1); + } + + // 部署合约 + console.log('\n📦 Deploying contract...'); + const factory = new ContractFactory(CONTRACT_ABI, CONTRACT_BYTECODE, wallet); + const contract = await factory.deploy(); + + console.log(`⏳ Waiting for confirmation...`); + console.log(` Transaction: https://testnet.bscscan.com/tx/${contract.deploymentTransaction()?.hash}`); + + await contract.waitForDeployment(); + const contractAddress = await contract.getAddress(); + + console.log(` +✅ TestUSDT deployed successfully! + +📋 Contract Address: ${contractAddress} +🔗 BSCScan: https://testnet.bscscan.com/address/${contractAddress} + +Next steps: +1. Update .env: BSC_USDT_CONTRACT=${contractAddress} +2. Call faucet() to mint 10,000 USDT for testing +3. Or call mintUsdt(100000) to mint 100,000 USDT + `); +} + +main().catch((error) => { + console.error('❌ Deployment failed:', error.message); + process.exit(1); +}); diff --git a/backend/services/mining-blockchain-service/scripts/generate-wallet.ts b/backend/services/mining-blockchain-service/scripts/generate-wallet.ts new file mode 100644 index 00000000..b0ac3025 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/generate-wallet.ts @@ -0,0 +1,26 @@ +/** + * Generate a new wallet for BSC Testnet deployment + * + * Usage: npx ts-node scripts/generate-wallet.ts + */ + +import { ethers } from 'ethers'; + +const wallet = ethers.Wallet.createRandom(); + +console.log(` +🔐 New Wallet Generated for BSC Testnet +======================================== + +Address: ${wallet.address} +Private Key: ${wallet.privateKey} +Mnemonic: ${wallet.mnemonic?.phrase} + +Next steps: +1. Go to https://www.bnbchain.org/en/testnet-faucet +2. Paste your address: ${wallet.address} +3. Get 0.1 tBNB +4. Run: npx ts-node scripts/deploy-test-usdt.ts ${wallet.privateKey} + +⚠️ SAVE YOUR PRIVATE KEY! You'll need it for future contract interactions. +`); diff --git a/backend/services/mining-blockchain-service/scripts/health-check.sh b/backend/services/mining-blockchain-service/scripts/health-check.sh new file mode 100644 index 00000000..01ebda35 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/health-check.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# 健康检查脚本 - 检查所有依赖服务是否正常 + +echo "🏥 开始健康检查..." +echo "" + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 检查计数 +PASS=0 +FAIL=0 +FAILED_SERVICES=() + +# 检查函数 +check_service() { + local service_name=$1 + local check_command=$2 + local fix_command=$3 + + echo -n "Checking $service_name ... " + + if eval "$check_command" > /dev/null 2>&1; then + echo -e "${GREEN}✓ OK${NC}" + PASS=$((PASS + 1)) + else + echo -e "${RED}✗ FAIL${NC}" + FAIL=$((FAIL + 1)) + FAILED_SERVICES+=("$service_name:$fix_command") + fi +} + +# 检查 PostgreSQL +echo -e "${YELLOW}=== 数据库服务 ===${NC}" +check_service "PostgreSQL" "pg_isready -h localhost -p 5432" "sudo systemctl start postgresql" + +# 检查 Redis (支持 Docker 和本地) +echo -e "${YELLOW}=== 缓存服务 ===${NC}" +if command -v redis-cli &> /dev/null; then + check_service "Redis" "redis-cli -h localhost -p 6379 ping" "docker start blockchain-service-redis-1 或 redis-server --daemonize yes" +elif command -v docker &> /dev/null; then + check_service "Redis" "docker exec blockchain-service-redis-1 redis-cli ping" "docker start blockchain-service-redis-1" +else + check_service "Redis" "nc -zv localhost 6379" "docker start blockchain-service-redis-1" +fi + +# 检查 Kafka +echo -e "${YELLOW}=== 消息队列服务 ===${NC}" +check_service "Kafka" "nc -zv localhost 9092" "启动 Kafka (需要手动启动)" + +# 检查区块链 RPC +echo -e "${YELLOW}=== 区块链 RPC ===${NC}" +check_service "KAVA RPC" "curl -sf https://evm.kava.io -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}'" "检查网络连接或 RPC 端点" +check_service "BSC RPC" "curl -sf https://bsc-dataseed.binance.org -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}'" "检查网络连接或 RPC 端点" + +# 检查应用服务 +echo -e "${YELLOW}=== 应用服务 ===${NC}" +check_service "Blockchain Service" "curl -f http://localhost:3012/health" "npm run start:dev" + +# 检查 Swagger 文档 +echo -e "${YELLOW}=== API 文档 ===${NC}" +check_service "Swagger UI" "curl -f http://localhost:3012/api" "等待 Blockchain Service 启动" + +echo "" +echo -e "${YELLOW}======================================${NC}" +echo -e "${YELLOW}健康检查完成!${NC}" +echo -e "${GREEN}正常: $PASS${NC}" +echo -e "${RED}异常: $FAIL${NC}" +echo -e "${YELLOW}======================================${NC}" + +if [ $FAIL -eq 0 ]; then + echo -e "${GREEN}✓ 所有服务正常!${NC}" + echo "" + echo -e "${BLUE}现在可以运行测试:${NC}" + echo " ./scripts/quick-test.sh" + exit 0 +else + echo -e "${RED}✗ 存在异常的服务!${NC}" + echo "" + echo -e "${BLUE}修复建议:${NC}" + for service_info in "${FAILED_SERVICES[@]}"; do + service_name="${service_info%%:*}" + fix_command="${service_info#*:}" + echo -e "${YELLOW} • $service_name:${NC} $fix_command" + done + echo "" + echo -e "${BLUE}或者运行一键启动脚本:${NC}" + echo " ./scripts/start-all.sh" + exit 1 +fi diff --git a/backend/services/mining-blockchain-service/scripts/quick-test.sh b/backend/services/mining-blockchain-service/scripts/quick-test.sh new file mode 100644 index 00000000..d500f500 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/quick-test.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# 快速测试脚本 - 在本地环境快速验证核心功能 + +set -e + +echo "🚀 开始快速测试 Blockchain Service..." +echo "" + +# 颜色定义 +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +BASE_URL="http://localhost:3012" + +# 测试结果统计 +PASS=0 +FAIL=0 + +# 测试函数 +test_api() { + local test_name=$1 + local method=$2 + local endpoint=$3 + local data=$4 + local expected_status=$5 + + echo -n "Testing: $test_name ... " + + if [ -n "$data" ]; then + response=$(curl -s -w "\n%{http_code}" -X $method "$BASE_URL$endpoint" \ + -H "Content-Type: application/json" \ + -d "$data") + else + response=$(curl -s -w "\n%{http_code}" -X $method "$BASE_URL$endpoint" \ + -H "Content-Type: application/json") + fi + + status=$(echo "$response" | tail -n1) + body=$(echo "$response" | head -n-1) + + if [ "$status" -eq "$expected_status" ]; then + echo -e "${GREEN}✓ PASS${NC}" + PASS=$((PASS + 1)) + if command -v jq &> /dev/null && [ -n "$body" ]; then + echo "$body" | jq '.' 2>/dev/null || echo "$body" + else + echo "$body" + fi + else + echo -e "${RED}✗ FAIL${NC} (Expected: $expected_status, Got: $status)" + FAIL=$((FAIL + 1)) + echo "$body" + fi + echo "" +} + +# 1. 健康检查 +echo -e "${YELLOW}=== 1. 健康检查 ===${NC}" +test_api "Health Check" "GET" "/health" "" 200 +test_api "Ready Check" "GET" "/health/ready" "" 200 + +# 2. 余额查询测试 +echo -e "${YELLOW}=== 2. 余额查询 ===${NC}" +# 使用一个已知的测试地址 (Binance Hot Wallet) +TEST_ADDRESS="0x8894E0a0c962CB723c1976a4421c95949bE2D4E3" + +test_api "Query KAVA Balance" "GET" "/balance?chainType=KAVA&address=$TEST_ADDRESS" "" 200 +test_api "Query Multi-Chain Balance" "GET" "/balance/multi-chain?address=$TEST_ADDRESS" "" 200 + +# 3. 地址派生测试 +echo -e "${YELLOW}=== 3. 地址派生测试 ===${NC}" +# 测试用压缩公钥 (仅用于测试) +TEST_PUBLIC_KEY="0x02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737" +TEST_USER_ID="999999" + +test_api "Derive Address" "POST" "/internal/derive-address" \ + "{\"userId\": \"$TEST_USER_ID\", \"publicKey\": \"$TEST_PUBLIC_KEY\"}" \ + 201 + +# 4. 获取用户地址 +echo -e "${YELLOW}=== 4. 获取用户地址 ===${NC}" +test_api "Get User Addresses" "GET" "/internal/user/$TEST_USER_ID/addresses" "" 200 + +# 5. 错误场景测试 +echo -e "${YELLOW}=== 5. 错误场景测试 ===${NC}" + +# 无效地址格式 +test_api "Invalid Address Format" "GET" "/balance?chainType=KAVA&address=invalid" "" 400 + +# 无效链类型 +test_api "Invalid Chain Type" "GET" "/balance?chainType=INVALID&address=$TEST_ADDRESS" "" 400 + +# 无效公钥格式 +test_api "Invalid Public Key" "POST" "/internal/derive-address" \ + "{\"userId\": \"1\", \"publicKey\": \"invalid\"}" \ + 400 + +# 6. API 文档测试 +echo -e "${YELLOW}=== 6. API 文档 ===${NC}" +test_api "Swagger API Docs" "GET" "/api" "" 200 + +# 总结 +echo "" +echo -e "${YELLOW}======================================${NC}" +echo -e "${YELLOW}测试完成!${NC}" +echo -e "${GREEN}通过: $PASS${NC}" +echo -e "${RED}失败: $FAIL${NC}" +echo -e "${YELLOW}======================================${NC}" + +if [ $FAIL -eq 0 ]; then + echo -e "${GREEN}✓ 所有测试通过!${NC}" + exit 0 +else + echo -e "${RED}✗ 存在失败的测试!${NC}" + exit 1 +fi diff --git a/backend/services/mining-blockchain-service/scripts/rebuild-kafka.sh b/backend/services/mining-blockchain-service/scripts/rebuild-kafka.sh new file mode 100644 index 00000000..849743b2 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/rebuild-kafka.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# 重建 Kafka 容器以应用新的配置 + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${YELLOW}🔄 重建 Kafka 容器...${NC}" +echo "" + +# 1. 停止 Blockchain Service (如果在运行) +echo -e "${BLUE}步骤 1: 停止 Blockchain Service${NC}" +PID=$(lsof -ti :3012 2>/dev/null) +if [ ! -z "$PID" ]; then + echo "停止 Blockchain Service (PID: $PID)..." + kill $PID + sleep 2 + echo -e "${GREEN}✓ Blockchain Service 已停止${NC}" +else + echo -e "${YELLOW}⚠️ Blockchain Service 未在运行${NC}" +fi +echo "" + +# 2. 停止并删除 Kafka 容器 +echo -e "${BLUE}步骤 2: 停止并删除旧容器${NC}" +docker compose stop kafka 2>/dev/null || true +docker compose rm -f kafka 2>/dev/null || true +echo -e "${GREEN}✓ 旧容器已删除${NC}" +echo "" + +# 3. 重新创建容器 +echo -e "${BLUE}步骤 3: 创建新容器${NC}" +docker compose up -d kafka +echo "等待 Kafka 启动..." +sleep 20 +echo -e "${GREEN}✓ Kafka 容器已创建${NC}" +echo "" + +# 4. 验证配置 +echo -e "${BLUE}步骤 4: 验证配置${NC}" +CONTAINER_NAME=$(docker compose ps -q kafka 2>/dev/null) +if [ ! -z "$CONTAINER_NAME" ]; then + ADVERTISED=$(docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -A 1 "KAFKA_ADVERTISED_LISTENERS" | head -1) + if echo "$ADVERTISED" | grep -q "localhost:9092"; then + echo -e "${GREEN}✓ Kafka 配置已更新!${NC}" + echo "$ADVERTISED" + else + echo -e "${YELLOW}⚠ Kafka 配置可能需要检查${NC}" + fi +else + echo -e "${YELLOW}⚠ 未找到 Kafka 容器${NC}" +fi +echo "" + +# 5. 测试连接 +echo -e "${BLUE}步骤 5: 测试 Kafka 连接${NC}" +if nc -zv localhost 9092 2>&1 | grep -q "succeeded\|Connected"; then + echo -e "${GREEN}✓ Kafka 端口可访问${NC}" +else + echo -e "${RED}✗ Kafka 端口不可访问${NC}" + exit 1 +fi +echo "" + +echo -e "${YELLOW}======================================${NC}" +echo -e "${GREEN}✓ Kafka 重建完成!${NC}" +echo -e "${YELLOW}======================================${NC}" +echo "" +echo -e "${BLUE}下一步:${NC}" +echo "1. 启动 Blockchain Service: npm run start:dev" +echo "2. 运行健康检查: ./scripts/health-check.sh" +echo "3. 运行快速测试: ./scripts/quick-test.sh" diff --git a/backend/services/mining-blockchain-service/scripts/start-all.sh b/backend/services/mining-blockchain-service/scripts/start-all.sh new file mode 100644 index 00000000..452e0d10 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/start-all.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# 一键启动所有服务 + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}🚀 启动所有服务...${NC}" +echo "" + +# 1. 启动 Redis +echo -e "${YELLOW}启动 Redis...${NC}" +if ! pgrep -x "redis-server" > /dev/null; then + if command -v redis-server &> /dev/null; then + redis-server --daemonize yes + echo -e "${GREEN}✓ Redis 已启动${NC}" + else + echo -e "${YELLOW}⚠ Redis 未安装,尝试使用 Docker...${NC}" + docker start blockchain-service-redis-1 2>/dev/null || docker compose up -d redis + fi +else + echo -e "${GREEN}✓ Redis 已在运行${NC}" +fi + +# 2. 检查 PostgreSQL +echo -e "${YELLOW}检查 PostgreSQL...${NC}" +if pg_isready -h localhost -p 5432 > /dev/null 2>&1; then + echo -e "${GREEN}✓ PostgreSQL 已在运行${NC}" +else + echo -e "${YELLOW}⚠ PostgreSQL 未运行,请手动启动${NC}" +fi + +# 3. 检查 Kafka +echo -e "${YELLOW}检查 Kafka...${NC}" +if nc -zv localhost 9092 > /dev/null 2>&1; then + echo -e "${GREEN}✓ Kafka 已在运行${NC}" +else + echo -e "${YELLOW}⚠ Kafka 未运行,请手动启动${NC}" +fi + +# 4. 启动 Blockchain Service +echo -e "${YELLOW}启动 Blockchain Service...${NC}" +cd "$(dirname "$0")/.." +npm run start:dev & + +# 等待服务启动 +echo "等待服务启动 (最多 30 秒)..." +for i in {1..30}; do + if curl -f http://localhost:3012/health > /dev/null 2>&1; then + echo -e "${GREEN}✓ Blockchain Service 已启动${NC}" + break + fi + sleep 1 + echo -n "." +done + +echo "" +echo -e "${GREEN}✓ 所有服务已启动!${NC}" +echo "" +echo "运行健康检查:" +echo " ./scripts/health-check.sh" +echo "" +echo "运行快速测试:" +echo " ./scripts/quick-test.sh" diff --git a/backend/services/mining-blockchain-service/scripts/stop-service.sh b/backend/services/mining-blockchain-service/scripts/stop-service.sh new file mode 100644 index 00000000..6a69a4c0 --- /dev/null +++ b/backend/services/mining-blockchain-service/scripts/stop-service.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# 停止 Blockchain Service + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${YELLOW}🛑 停止 Blockchain Service...${NC}" + +# 查找监听 3012 端口的进程 +PID=$(lsof -ti :3012) + +if [ -z "$PID" ]; then + echo -e "${YELLOW}⚠️ Blockchain Service 未在运行${NC}" + exit 0 +fi + +echo "找到进程: PID=$PID" + +# 尝试优雅停止 +echo "发送 SIGTERM 信号..." +kill $PID + +# 等待进程结束 +for i in {1..10}; do + if ! kill -0 $PID 2>/dev/null; then + echo -e "${GREEN}✓ Blockchain Service 已停止${NC}" + exit 0 + fi + sleep 1 + echo -n "." +done + +echo "" +echo -e "${YELLOW}⚠️ 进程未响应,强制停止...${NC}" +kill -9 $PID + +if ! kill -0 $PID 2>/dev/null; then + echo -e "${GREEN}✓ Blockchain Service 已强制停止${NC}" +else + echo -e "${RED}✗ 无法停止进程${NC}" + exit 1 +fi diff --git a/backend/services/mining-blockchain-service/src/api/api.module.ts b/backend/services/mining-blockchain-service/src/api/api.module.ts new file mode 100644 index 00000000..2fbae4a1 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/api.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ApplicationModule } from '@/application/application.module'; +import { DomainModule } from '@/domain/domain.module'; +import { HealthController } from './controllers'; +import { TransferController } from './controllers/transfer.controller'; + +@Module({ + imports: [ + ApplicationModule, + DomainModule, + ], + controllers: [HealthController, TransferController], +}) +export class ApiModule {} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/balance.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/balance.controller.ts new file mode 100644 index 00000000..ff6b5359 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/balance.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BalanceQueryService } from '@/application/services/balance-query.service'; +import { QueryBalanceDto, QueryMultiChainBalanceDto } from '../dto/request'; +import { BalanceResponseDto, MultiChainBalanceResponseDto } from '../dto/response'; +import { ChainType } from '@/domain/value-objects'; + +@ApiTags('Balance') +@Controller('balance') +export class BalanceController { + constructor(private readonly balanceService: BalanceQueryService) {} + + @Get() + @ApiOperation({ summary: '查询单链余额' }) + @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) + async getBalance(@Query() dto: QueryBalanceDto): Promise { + if (!dto.chainType) { + throw new Error('chainType is required'); + } + const chainType = ChainType.create(dto.chainType); + return this.balanceService.getBalance(chainType, dto.address); + } + + @Get('multi-chain') + @ApiOperation({ summary: '查询多链余额' }) + @ApiResponse({ status: 200, description: '多链余额信息', type: MultiChainBalanceResponseDto }) + async getMultiChainBalance( + @Query() dto: QueryMultiChainBalanceDto, + ): Promise { + const balances = await this.balanceService.getBalances(dto.address, dto.chainTypes); + return { + address: dto.address, + balances, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/deposit-repair.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/deposit-repair.controller.ts new file mode 100644 index 00000000..048c57b9 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/deposit-repair.controller.ts @@ -0,0 +1,72 @@ +import { Controller, Get, Post, Param, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { DepositRepairService } from '@/application/services/deposit-repair.service'; + +/** + * 充值修复控制器 + * + * 内部 API,用于诊断和修复历史遗留充值问题 + */ +@ApiTags('Deposit Repair') +@Controller('internal/deposit-repair') +export class DepositRepairController { + private readonly logger = new Logger(DepositRepairController.name); + + constructor(private readonly repairService: DepositRepairService) {} + + @Get('diagnose') + @ApiOperation({ summary: '诊断充值状态' }) + @ApiResponse({ + status: 200, + description: '返回需要修复的充值统计', + }) + async diagnose() { + this.logger.log('Running deposit diagnosis...'); + const result = await this.repairService.diagnose(); + this.logger.log( + `Diagnosis complete: ${result.confirmedNotNotified.length} deposits need repair`, + ); + return result; + } + + @Post('repair/:depositId') + @ApiOperation({ summary: '修复单个充值' }) + @ApiResponse({ + status: 200, + description: '修复结果', + }) + async repairDeposit(@Param('depositId') depositId: string) { + this.logger.log(`Repairing deposit ${depositId}...`); + const result = await this.repairService.repairDeposit(BigInt(depositId)); + this.logger.log(`Repair result: ${result.message}`); + return result; + } + + @Post('repair-all') + @ApiOperation({ summary: '批量修复所有未通知的充值' }) + @ApiResponse({ + status: 200, + description: '批量修复结果', + }) + async repairAll() { + this.logger.log('Starting batch repair...'); + const result = await this.repairService.repairAll(); + this.logger.log( + `Batch repair complete: ${result.success}/${result.total} success`, + ); + return result; + } + + @Post('reset-failed-outbox') + @ApiOperation({ summary: '重置失败的 Outbox 事件' }) + @ApiResponse({ + status: 200, + description: '重置结果', + }) + async resetFailedOutbox() { + this.logger.log('Resetting failed outbox events...'); + const result = await this.repairService.resetFailedOutboxEvents(); + this.logger.log(`Reset ${result.reset} failed events`); + return result; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/deposit.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/deposit.controller.ts new file mode 100644 index 00000000..d3b9ce61 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/deposit.controller.ts @@ -0,0 +1,102 @@ +/** + * Deposit Controller + * + * Provides deposit-related endpoints for the mobile app. + * Queries on-chain USDT balances for user's monitored addresses. + */ + +import { Controller, Get, Logger, Inject, UseGuards, Req } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { Request } from 'express'; +import { BalanceQueryService } from '@/application/services/balance-query.service'; +import { ChainTypeEnum } from '@/domain/enums'; +import { + IMonitoredAddressRepository, + MONITORED_ADDRESS_REPOSITORY, +} from '@/domain/repositories/monitored-address.repository.interface'; +import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard'; + +interface JwtPayload { + userId: string; + accountSequence: string; + deviceId: string; +} + +interface UsdtBalanceDto { + chainType: string; + address: string; + balance: string; + rawBalance: string; + decimals: number; +} + +interface DepositBalancesResponseDto { + kava: UsdtBalanceDto | null; + bsc: UsdtBalanceDto | null; +} + +@ApiTags('Deposit') +@Controller('deposit') +export class DepositController { + private readonly logger = new Logger(DepositController.name); + + constructor( + private readonly balanceService: BalanceQueryService, + @Inject(MONITORED_ADDRESS_REPOSITORY) + private readonly monitoredAddressRepo: IMonitoredAddressRepository, + ) {} + + @Get('balances') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '查询用户 USDT 余额' }) + @ApiResponse({ status: 200, description: '余额信息' }) + async getBalances(@Req() req: Request): Promise { + const user = (req as Request & { user: JwtPayload }).user; + const userId = BigInt(user.userId); + + this.logger.log(`Querying deposit balances for user ${userId}`); + + // Get user's monitored addresses + const addresses = await this.monitoredAddressRepo.findByUserId(userId); + + const response: DepositBalancesResponseDto = { + kava: null, + bsc: null, + }; + + // Query balance for each chain + for (const addr of addresses) { + try { + const chainType = addr.chainType; + const addressStr = addr.address.toString(); + const chainTypeStr = chainType.toString(); + + const balance = await this.balanceService.getBalance(chainType, addressStr); + + const balanceDto: UsdtBalanceDto = { + chainType: chainTypeStr, + address: addressStr, + balance: balance.usdtBalance, + rawBalance: balance.usdtRawBalance, + decimals: balance.usdtDecimals, + }; + + if (chainTypeStr === ChainTypeEnum.KAVA) { + response.kava = balanceDto; + } else if (chainTypeStr === ChainTypeEnum.BSC) { + response.bsc = balanceDto; + } + } catch (error) { + this.logger.error(`Error querying balance for ${addr.chainType}:${addr.address}`, error); + } + } + + this.logger.log( + `Balance query complete for user ${userId}: ` + + `KAVA=${response.kava?.balance || '0'}, BSC=${response.bsc?.balance || '0'}`, + ); + + return response; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/health.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/health.controller.ts new file mode 100644 index 00000000..b30c6324 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/health.controller.ts @@ -0,0 +1,59 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { ChainType } from '@/domain/value-objects'; + +@ApiTags('Health') +@Controller('health') +export class HealthController { + constructor(private readonly chainConfig: ChainConfigService) {} + + @Get() + @ApiOperation({ summary: '健康检查' }) + @ApiResponse({ status: 200, description: '服务健康' }) + check() { + return { + status: 'ok', + service: 'blockchain-service', + timestamp: new Date().toISOString(), + }; + } + + @Get('ready') + @ApiOperation({ summary: '就绪检查' }) + @ApiResponse({ status: 200, description: '服务就绪' }) + ready() { + return { + status: 'ready', + service: 'blockchain-service', + timestamp: new Date().toISOString(), + }; + } + + @Get('network') + @ApiOperation({ summary: '网络配置信息' }) + @ApiResponse({ status: 200, description: '返回当前网络配置' }) + network() { + const supportedChains = this.chainConfig.getSupportedChains(); + const chains: Record = {}; + + for (const chainTypeEnum of supportedChains) { + const chainType = ChainType.fromEnum(chainTypeEnum); + const config = this.chainConfig.getConfig(chainType); + chains[chainTypeEnum] = { + chainId: config.chainId, + rpcUrl: config.rpcUrl, + usdtContract: config.usdtContract, + isTestnet: config.isTestnet, + }; + } + + return { + service: 'blockchain-service', + networkMode: this.chainConfig.getNetworkMode(), + isTestnet: this.chainConfig.isTestnetMode(), + chains, + timestamp: new Date().toISOString(), + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/index.ts b/backend/services/mining-blockchain-service/src/api/controllers/index.ts new file mode 100644 index 00000000..f54e4e7f --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/index.ts @@ -0,0 +1,5 @@ +export * from './health.controller'; +export * from './balance.controller'; +export * from './internal.controller'; +export * from './deposit.controller'; +export * from './deposit-repair.controller'; diff --git a/backend/services/mining-blockchain-service/src/api/controllers/internal.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/internal.controller.ts new file mode 100644 index 00000000..770b0f0d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/internal.controller.ts @@ -0,0 +1,113 @@ +import { Controller, Post, Body, Get, Param, Put } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { AddressDerivationService } from '@/application/services/address-derivation.service'; +import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service'; +import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain'; +import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto, RevokeMnemonicDto } from '../dto/request'; +import { DeriveAddressResponseDto } from '../dto/response'; + +/** + * 内部 API 控制器 + * 供其他微服务调用 + */ +@ApiTags('Internal') +@Controller('internal') +export class InternalController { + constructor( + private readonly addressDerivationService: AddressDerivationService, + private readonly mnemonicVerification: MnemonicVerificationService, + private readonly mnemonicDerivation: MnemonicDerivationAdapter, + ) {} + + @Post('derive-address') + @ApiOperation({ summary: '从公钥派生地址' }) + @ApiResponse({ status: 201, description: '派生成功', type: DeriveAddressResponseDto }) + async deriveAddress(@Body() dto: DeriveAddressDto): Promise { + const result = await this.addressDerivationService.deriveAndRegister({ + userId: BigInt(dto.userId), + accountSequence: dto.accountSequence, + publicKey: dto.publicKey, + }); + + return { + userId: result.userId.toString(), + publicKey: result.publicKey, + addresses: result.addresses.map((a) => ({ + chainType: a.chainType, + address: a.address, + })), + }; + } + + @Get('user/:userId/addresses') + @ApiOperation({ summary: '获取用户的所有地址' }) + async getUserAddresses(@Param('userId') userId: string) { + const addresses = await this.addressDerivationService.getUserAddresses(BigInt(userId)); + return { + userId, + addresses: addresses.map((a) => ({ + chainType: a.chainType.toString(), + address: a.address.toString(), + isActive: a.isActive, + })), + }; + } + + @Post('verify-mnemonic') + @ApiOperation({ summary: '验证助记词是否匹配指定地址' }) + @ApiResponse({ status: 200, description: '验证结果' }) + async verifyMnemonic(@Body() dto: VerifyMnemonicDto) { + const result = this.mnemonicDerivation.verifyMnemonic(dto.mnemonic, dto.expectedAddresses); + return { + valid: result.valid, + matchedAddresses: result.matchedAddresses, + mismatchedAddresses: result.mismatchedAddresses, + }; + } + + @Post('derive-from-mnemonic') + @ApiOperation({ summary: '从助记词派生所有链地址' }) + @ApiResponse({ status: 200, description: '派生的地址列表' }) + async deriveFromMnemonic(@Body() dto: { mnemonic: string }) { + const addresses = this.mnemonicDerivation.deriveAllAddresses(dto.mnemonic); + return { + addresses: addresses.map((a) => ({ + chainType: a.chainType, + address: a.address, + })), + }; + } + + @Post('verify-mnemonic-hash') + @ApiOperation({ summary: '通过账户序列号验证助记词' }) + @ApiResponse({ status: 200, description: '验证结果' }) + async verifyMnemonicHash(@Body() dto: VerifyMnemonicHashDto) { + const result = await this.mnemonicVerification.verifyMnemonicByAccount({ + accountSequence: dto.accountSequence, + mnemonic: dto.mnemonic, + }); + return { + valid: result.valid, + message: result.message, + }; + } + + @Put('mnemonic/backup') + @ApiOperation({ summary: '标记助记词已备份' }) + @ApiResponse({ status: 200, description: '标记成功' }) + async markMnemonicBackedUp(@Body() dto: MarkMnemonicBackupDto) { + await this.mnemonicVerification.markAsBackedUp(dto.accountSequence); + return { + success: true, + message: 'Mnemonic marked as backed up', + }; + } + + @Post('mnemonic/revoke') + @ApiOperation({ summary: '挂失助记词' }) + @ApiResponse({ status: 200, description: '挂失结果' }) + async revokeMnemonic(@Body() dto: RevokeMnemonicDto) { + const result = await this.mnemonicVerification.revokeMnemonic(dto.accountSequence, dto.reason); + return result; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts new file mode 100644 index 00000000..605f35a2 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts @@ -0,0 +1,114 @@ +import { Controller, Post, Body, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, Matches, IsNumberString } from 'class-validator'; +import { Erc20TransferService, TransferResult } from '@/domain/services/erc20-transfer.service'; +import { ChainTypeEnum } from '@/domain/enums'; + +/** + * dUSDT 转账请求 DTO + */ +class TransferDusdtDto { + @ApiProperty({ description: '接收者 Kava 地址', example: '0x1234567890abcdef1234567890abcdef12345678' }) + @IsString() + @IsNotEmpty() + @Matches(/^0x[a-fA-F0-9]{40}$/, { message: 'Invalid EVM address format' }) + toAddress: string; + + @ApiProperty({ description: '转账金额(人类可读格式)', example: '100.5' }) + @IsString() + @IsNotEmpty() + @IsNumberString({}, { message: 'Amount must be a valid number string' }) + amount: string; +} + +/** + * 转账结果响应 DTO + */ +class TransferResponseDto { + @ApiProperty({ description: '是否成功' }) + success: boolean; + + @ApiProperty({ description: '交易哈希', required: false }) + txHash?: string; + + @ApiProperty({ description: '错误信息', required: false }) + error?: string; + + @ApiProperty({ description: '消耗的 Gas', required: false }) + gasUsed?: string; + + @ApiProperty({ description: '区块高度', required: false }) + blockNumber?: number; +} + +/** + * 余额响应 DTO + */ +class BalanceResponseDto { + @ApiProperty({ description: '热钱包地址' }) + address: string; + + @ApiProperty({ description: 'dUSDT 余额' }) + balance: string; + + @ApiProperty({ description: '链类型' }) + chain: string; +} + +/** + * dUSDT 转账控制器 + * 供 trading-service C2C Bot 调用 + */ +@ApiTags('Transfer') +@Controller('transfer') +export class TransferController { + constructor(private readonly erc20TransferService: Erc20TransferService) {} + + @Post('dusdt') + @ApiOperation({ summary: '转账 dUSDT 到指定地址' }) + @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) + @ApiResponse({ status: 400, description: '参数错误' }) + @ApiResponse({ status: 500, description: '转账失败' }) + async transferDusdt(@Body() dto: TransferDusdtDto): Promise { + const result: TransferResult = await this.erc20TransferService.transferUsdt( + ChainTypeEnum.KAVA, + dto.toAddress, + dto.amount, + ); + + return { + success: result.success, + txHash: result.txHash, + error: result.error, + gasUsed: result.gasUsed, + blockNumber: result.blockNumber, + }; + } + + @Get('dusdt/balance') + @ApiOperation({ summary: '查询热钱包 dUSDT 余额' }) + @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) + async getHotWalletBalance(): Promise { + const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); + const balance = await this.erc20TransferService.getHotWalletBalance(ChainTypeEnum.KAVA); + + return { + address: address || '', + balance, + chain: 'KAVA', + }; + } + + @Get('status') + @ApiOperation({ summary: '检查转账服务状态' }) + @ApiResponse({ status: 200, description: '服务状态' }) + async getStatus(): Promise<{ configured: boolean; hotWalletAddress: string | null }> { + const configured = this.erc20TransferService.isConfigured(ChainTypeEnum.KAVA); + const hotWalletAddress = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); + + return { + configured, + hotWalletAddress, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/index.ts b/backend/services/mining-blockchain-service/src/api/dto/index.ts new file mode 100644 index 00000000..a0517597 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/index.ts @@ -0,0 +1,2 @@ +export * from './request'; +export * from './response'; diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/derive-address.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/derive-address.dto.ts new file mode 100644 index 00000000..81639c07 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/derive-address.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsNumberString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DeriveAddressDto { + @ApiProperty({ description: '用户ID', example: '12345' }) + @IsNumberString() + userId: string; + + @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' }) + @IsString() + accountSequence: string; + + @ApiProperty({ + description: '压缩公钥 (33 bytes, 0x02/0x03 开头)', + example: '0x02abc123...', + }) + @IsString() + publicKey: string; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/index.ts b/backend/services/mining-blockchain-service/src/api/dto/request/index.ts new file mode 100644 index 00000000..d0ec70ab --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/index.ts @@ -0,0 +1,6 @@ +export * from './query-balance.dto'; +export * from './derive-address.dto'; +export * from './verify-mnemonic.dto'; +export * from './verify-mnemonic-hash.dto'; +export * from './mark-mnemonic-backup.dto'; +export * from './revoke-mnemonic.dto'; diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts new file mode 100644 index 00000000..063bc11d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts @@ -0,0 +1,8 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MarkMnemonicBackupDto { + @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' }) + @IsString() + accountSequence: string; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/query-balance.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/query-balance.dto.ts new file mode 100644 index 00000000..736cf438 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/query-balance.dto.ts @@ -0,0 +1,34 @@ +import { IsString, IsOptional, IsEnum, IsArray } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ChainTypeEnum } from '@/domain/enums'; + +export class QueryBalanceDto { + @ApiProperty({ description: '钱包地址', example: '0x1234...' }) + @IsString() + address: string; + + @ApiPropertyOptional({ + description: '链类型', + enum: ChainTypeEnum, + example: ChainTypeEnum.KAVA, + }) + @IsOptional() + @IsEnum(ChainTypeEnum) + chainType?: ChainTypeEnum; +} + +export class QueryMultiChainBalanceDto { + @ApiProperty({ description: '钱包地址', example: '0x1234...' }) + @IsString() + address: string; + + @ApiPropertyOptional({ + description: '链类型列表', + type: [String], + enum: ChainTypeEnum, + }) + @IsOptional() + @IsArray() + @IsEnum(ChainTypeEnum, { each: true }) + chainTypes?: ChainTypeEnum[]; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts new file mode 100644 index 00000000..ae2c1b78 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RevokeMnemonicDto { + @ApiProperty({ example: 'D2512110001', description: '账户序列号' }) + @IsString() + @IsNotEmpty() + accountSequence: string; + + @ApiProperty({ example: '助记词泄露', description: '挂失原因' }) + @IsString() + @IsNotEmpty() + @MaxLength(200) + reason: string; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts new file mode 100644 index 00000000..b95a741b --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts @@ -0,0 +1,18 @@ +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class VerifyMnemonicHashDto { + @ApiProperty({ + description: '账户序列号 (格式: D + YYMMDD + 5位序号)', + example: 'D2512110008', + }) + @IsString() + accountSequence: string; + + @ApiProperty({ + description: '助记词 (12个单词,空格分隔)', + example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + }) + @IsString() + mnemonic: string; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic.dto.ts new file mode 100644 index 00000000..112291db --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/request/verify-mnemonic.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsArray, ArrayMinSize } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class VerifyMnemonicDto { + @ApiProperty({ + description: '助记词 (12或24个单词,空格分隔)', + example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + }) + @IsString() + mnemonic: string; + + @ApiProperty({ + description: '期望的钱包地址列表,用于验证助记词', + example: [{ chainType: 'KAVA', address: 'kava1abc...' }], + }) + @IsArray() + @ArrayMinSize(1) + expectedAddresses: Array<{ + chainType: string; + address: string; + }>; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/response/address.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/response/address.dto.ts new file mode 100644 index 00000000..e229ae25 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/response/address.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DerivedAddressDto { + @ApiProperty({ description: '链类型' }) + chainType: string; + + @ApiProperty({ description: '钱包地址' }) + address: string; +} + +export class DeriveAddressResponseDto { + @ApiProperty({ description: '用户ID' }) + userId: string; + + @ApiProperty({ description: '公钥' }) + publicKey: string; + + @ApiProperty({ description: '派生的地址列表', type: [DerivedAddressDto] }) + addresses: DerivedAddressDto[]; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/response/balance.dto.ts b/backend/services/mining-blockchain-service/src/api/dto/response/balance.dto.ts new file mode 100644 index 00000000..143916ab --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/response/balance.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class BalanceResponseDto { + @ApiProperty({ description: '链类型' }) + chainType: string; + + @ApiProperty({ description: '钱包地址' }) + address: string; + + @ApiProperty({ description: 'USDT 余额' }) + usdtBalance: string; + + @ApiProperty({ description: '原生代币余额' }) + nativeBalance: string; + + @ApiProperty({ description: '原生代币符号' }) + nativeSymbol: string; +} + +export class MultiChainBalanceResponseDto { + @ApiProperty({ description: '钱包地址' }) + address: string; + + @ApiProperty({ description: '各链余额', type: [BalanceResponseDto] }) + balances: BalanceResponseDto[]; +} diff --git a/backend/services/mining-blockchain-service/src/api/dto/response/index.ts b/backend/services/mining-blockchain-service/src/api/dto/response/index.ts new file mode 100644 index 00000000..d767a3e8 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/api/dto/response/index.ts @@ -0,0 +1,2 @@ +export * from './balance.dto'; +export * from './address.dto'; diff --git a/backend/services/mining-blockchain-service/src/app.module.ts b/backend/services/mining-blockchain-service/src/app.module.ts new file mode 100644 index 00000000..87e90a87 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { ApiModule } from '@/api/api.module'; +import { appConfig, databaseConfig, redisConfig, kafkaConfig, blockchainConfig } from '@/config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig, redisConfig, kafkaConfig, blockchainConfig], + }), + ScheduleModule.forRoot(), + ApiModule, + ], +}) +export class AppModule {} diff --git a/backend/services/mining-blockchain-service/src/application/application.module.ts b/backend/services/mining-blockchain-service/src/application/application.module.ts new file mode 100644 index 00000000..50a3c8c4 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/application.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { InfrastructureModule } from '@/infrastructure/infrastructure.module'; +import { DomainModule } from '@/domain/domain.module'; +import { MpcTransferInitializerService } from './services/mpc-transfer-initializer.service'; + +@Module({ + imports: [InfrastructureModule, DomainModule], + providers: [ + // MPC 签名客户端注入 + MpcTransferInitializerService, + ], + exports: [], +}) +export class ApplicationModule {} diff --git a/backend/services/mining-blockchain-service/src/application/event-handlers/index.ts b/backend/services/mining-blockchain-service/src/application/event-handlers/index.ts new file mode 100644 index 00000000..7231998d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/event-handlers/index.ts @@ -0,0 +1,2 @@ +export * from './mpc-keygen-completed.handler'; +export * from './withdrawal-requested.handler'; diff --git a/backend/services/mining-blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts b/backend/services/mining-blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts new file mode 100644 index 00000000..201f444c --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts @@ -0,0 +1,74 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { AddressDerivationService } from '../services/address-derivation.service'; +import { MpcEventConsumerService, KeygenCompletedPayload } from '@/infrastructure/kafka/mpc-event-consumer.service'; + +/** + * MPC 密钥生成完成事件处理器 + * + * 监听 mpc.KeygenCompleted 事件,从公钥派生多链钱包地址, + * 并发布 blockchain.WalletAddressCreated 事件通知 identity-service + */ +@Injectable() +export class MpcKeygenCompletedHandler implements OnModuleInit { + private readonly logger = new Logger(MpcKeygenCompletedHandler.name); + + constructor( + private readonly addressDerivationService: AddressDerivationService, + private readonly mpcEventConsumer: MpcEventConsumerService, + ) {} + + onModuleInit() { + // Register handler for keygen completed events + this.mpcEventConsumer.onKeygenCompleted(this.handleKeygenCompleted.bind(this)); + this.logger.log(`[INIT] MpcKeygenCompletedHandler registered with MpcEventConsumer`); + } + + /** + * 处理 MPC 密钥生成完成事件 + * 从 mpc-service 的 KeygenCompleted 事件中提取 publicKey、userId 和 accountSequence + */ + private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise { + this.logger.log(`[HANDLE] Received KeygenCompleted event`); + this.logger.log(`[HANDLE] sessionId: ${payload.sessionId}`); + this.logger.log(`[HANDLE] publicKey: ${payload.publicKey?.substring(0, 30)}...`); + this.logger.log(`[HANDLE] extraPayload: ${JSON.stringify(payload.extraPayload)}`); + + // Extract userId and accountSequence from extraPayload + const userId = payload.extraPayload?.userId; + const accountSequence = payload.extraPayload?.accountSequence; + + if (!userId) { + this.logger.error(`[ERROR] Missing userId in extraPayload, cannot derive addresses`); + return; + } + + if (!accountSequence) { + this.logger.error(`[ERROR] Missing accountSequence in extraPayload, cannot derive addresses`); + return; + } + + const publicKey = payload.publicKey; + if (!publicKey) { + this.logger.error(`[ERROR] Missing publicKey in payload, cannot derive addresses`); + return; + } + + try { + this.logger.log(`[DERIVE] Starting address derivation for user: ${userId}, account: ${accountSequence}`); + + const result = await this.addressDerivationService.deriveAndRegister({ + userId: BigInt(userId), + accountSequence: accountSequence, + publicKey, + }); + + this.logger.log(`[DERIVE] Successfully derived ${result.addresses.length} addresses for account ${accountSequence}`); + result.addresses.forEach((addr) => { + this.logger.log(`[DERIVE] - ${addr.chainType}: ${addr.address}`); + }); + } catch (error) { + this.logger.error(`[ERROR] Failed to derive addresses for account ${accountSequence}:`, error); + throw error; + } + } +} diff --git a/backend/services/mining-blockchain-service/src/application/event-handlers/system-withdrawal-requested.handler.ts b/backend/services/mining-blockchain-service/src/application/event-handlers/system-withdrawal-requested.handler.ts new file mode 100644 index 00000000..effc106a --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/event-handlers/system-withdrawal-requested.handler.ts @@ -0,0 +1,140 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { + WithdrawalEventConsumerService, + SystemWithdrawalRequestedPayload, +} from '@/infrastructure/kafka/withdrawal-event-consumer.service'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { Erc20TransferService } from '@/domain/services/erc20-transfer.service'; +import { ChainTypeEnum } from '@/domain/enums'; + +/** + * System Withdrawal Requested Event Handler + * + * Handles system account withdrawal requests from wallet-service. + * Executes ERC20 USDT transfers from hot wallet to user's address. + */ +@Injectable() +export class SystemWithdrawalRequestedHandler implements OnModuleInit { + private readonly logger = new Logger(SystemWithdrawalRequestedHandler.name); + + constructor( + private readonly withdrawalEventConsumer: WithdrawalEventConsumerService, + private readonly eventPublisher: EventPublisherService, + private readonly transferService: Erc20TransferService, + ) {} + + onModuleInit() { + this.withdrawalEventConsumer.onSystemWithdrawalRequested( + this.handleSystemWithdrawalRequested.bind(this), + ); + this.logger.log(`[INIT] SystemWithdrawalRequestedHandler registered`); + } + + /** + * Handle system withdrawal requested event from wallet-service + * + * Flow: + * 1. Receive system withdrawal request + * 2. Execute ERC20 transfer from hot wallet + * 3. Publish final status (CONFIRMED or FAILED) + */ + private async handleSystemWithdrawalRequested( + payload: SystemWithdrawalRequestedPayload, + ): Promise { + this.logger.log(`[HANDLE] ========== System Withdrawal Request ==========`); + this.logger.log(`[HANDLE] orderNo: ${payload.orderNo}`); + this.logger.log(`[HANDLE] fromAccountSequence: ${payload.fromAccountSequence}`); + this.logger.log(`[HANDLE] fromAccountName: ${payload.fromAccountName}`); + this.logger.log(`[HANDLE] toAccountSequence: ${payload.toAccountSequence}`); + this.logger.log(`[HANDLE] toAddress: ${payload.toAddress}`); + this.logger.log(`[HANDLE] amount: ${payload.amount}`); + this.logger.log(`[HANDLE] chainType: ${payload.chainType}`); + + try { + // Step 1: 验证链类型 + const chainType = this.parseChainType(payload.chainType); + if (!chainType) { + throw new Error(`Unsupported chain type: ${payload.chainType}`); + } + + // Step 2: 检查转账服务是否配置 + if (!this.transferService.isConfigured(chainType)) { + throw new Error(`Hot wallet not configured for chain: ${chainType}`); + } + + // Step 3: 执行 ERC20 转账 + this.logger.log(`[PROCESS] Executing ERC20 transfer for system withdrawal...`); + const result = await this.transferService.transferUsdt( + chainType, + payload.toAddress, + payload.amount, + ); + + if (result.success && result.txHash) { + // Step 4a: 转账成功,发布确认状态 + this.logger.log(`[SUCCESS] System withdrawal ${payload.orderNo} confirmed!`); + this.logger.log(`[SUCCESS] TxHash: ${result.txHash}`); + this.logger.log(`[SUCCESS] Block: ${result.blockNumber}`); + + await this.eventPublisher.publish({ + eventType: 'blockchain.system-withdrawal.confirmed', + toPayload: () => ({ + orderNo: payload.orderNo, + fromAccountSequence: payload.fromAccountSequence, + fromAccountName: payload.fromAccountName, + toAccountSequence: payload.toAccountSequence, + status: 'CONFIRMED', + txHash: result.txHash, + blockNumber: result.blockNumber, + chainType: payload.chainType, + toAddress: payload.toAddress, + amount: payload.amount, + }), + eventId: `sys-wd-confirmed-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + this.logger.log(`[COMPLETE] System withdrawal ${payload.orderNo} completed successfully`); + } else { + // Step 4b: 转账失败 + throw new Error(result.error || 'Transfer failed'); + } + + } catch (error) { + this.logger.error( + `[ERROR] Failed to process system withdrawal ${payload.orderNo}`, + error, + ); + + // 发布失败事件 + await this.eventPublisher.publish({ + eventType: 'blockchain.system-withdrawal.failed', + toPayload: () => ({ + orderNo: payload.orderNo, + fromAccountSequence: payload.fromAccountSequence, + fromAccountName: payload.fromAccountName, + toAccountSequence: payload.toAccountSequence, + status: 'FAILED', + error: error instanceof Error ? error.message : 'Unknown error', + chainType: payload.chainType, + toAddress: payload.toAddress, + amount: payload.amount, + }), + eventId: `sys-wd-failed-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + throw error; + } + } + + /** + * 解析链类型字符串 + */ + private parseChainType(chainType: string): ChainTypeEnum | null { + const normalized = chainType.toUpperCase(); + if (normalized === 'KAVA') return ChainTypeEnum.KAVA; + if (normalized === 'BSC') return ChainTypeEnum.BSC; + return null; + } +} diff --git a/backend/services/mining-blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts b/backend/services/mining-blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts new file mode 100644 index 00000000..f251331f --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts @@ -0,0 +1,165 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { + WithdrawalEventConsumerService, + WithdrawalRequestedPayload, +} from '@/infrastructure/kafka/withdrawal-event-consumer.service'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { Erc20TransferService } from '@/domain/services/erc20-transfer.service'; +import { ChainTypeEnum } from '@/domain/enums'; + +/** + * Withdrawal Requested Event Handler + * + * Handles withdrawal requests from wallet-service. + * Executes ERC20 USDT transfers on the specified chain (KAVA/BSC). + */ +@Injectable() +export class WithdrawalRequestedHandler implements OnModuleInit { + private readonly logger = new Logger(WithdrawalRequestedHandler.name); + + constructor( + private readonly withdrawalEventConsumer: WithdrawalEventConsumerService, + private readonly eventPublisher: EventPublisherService, + private readonly transferService: Erc20TransferService, + ) {} + + onModuleInit() { + this.withdrawalEventConsumer.onWithdrawalRequested( + this.handleWithdrawalRequested.bind(this), + ); + this.logger.log(`[INIT] WithdrawalRequestedHandler registered`); + } + + /** + * Handle withdrawal requested event from wallet-service + * + * Flow: + * 1. Receive withdrawal request + * 2. Publish "PROCESSING" status + * 3. Execute ERC20 transfer + * 4. Publish final status (CONFIRMED or FAILED) + */ + private async handleWithdrawalRequested( + payload: WithdrawalRequestedPayload, + ): Promise { + this.logger.log(`[HANDLE] ========== Withdrawal Request ==========`); + this.logger.log(`[HANDLE] orderNo: ${payload.orderNo}`); + this.logger.log(`[HANDLE] accountSequence: ${payload.accountSequence}`); + this.logger.log(`[HANDLE] userId: ${payload.userId}`); + this.logger.log(`[HANDLE] chainType: ${payload.chainType}`); + this.logger.log(`[HANDLE] toAddress: ${payload.toAddress}`); + this.logger.log(`[HANDLE] amount: ${payload.amount}`); + this.logger.log(`[HANDLE] fee: ${payload.fee}`); + this.logger.log(`[HANDLE] netAmount: ${payload.netAmount}`); + + try { + // Step 1: 验证链类型 + const chainType = this.parseChainType(payload.chainType); + if (!chainType) { + throw new Error(`Unsupported chain type: ${payload.chainType}`); + } + + // Step 2: 检查转账服务是否配置 + if (!this.transferService.isConfigured(chainType)) { + throw new Error(`Hot wallet not configured for chain: ${chainType}`); + } + + // Step 3: 发布处理中状态 + this.logger.log(`[PROCESS] Starting withdrawal ${payload.orderNo}`); + await this.publishStatus(payload, 'PROCESSING', 'Withdrawal is being processed'); + + // Step 4: 执行 ERC20 转账 + this.logger.log(`[PROCESS] Executing ERC20 transfer...`); + const result = await this.transferService.transferUsdt( + chainType, + payload.toAddress, + payload.netAmount.toString(), + ); + + if (result.success && result.txHash) { + // Step 5a: 转账成功,发布确认状态 + this.logger.log(`[SUCCESS] Withdrawal ${payload.orderNo} confirmed!`); + this.logger.log(`[SUCCESS] TxHash: ${result.txHash}`); + this.logger.log(`[SUCCESS] Block: ${result.blockNumber}`); + + await this.eventPublisher.publish({ + eventType: 'blockchain.withdrawal.confirmed', + toPayload: () => ({ + orderNo: payload.orderNo, + accountSequence: payload.accountSequence, + userId: payload.userId, + status: 'CONFIRMED', + txHash: result.txHash, + blockNumber: result.blockNumber, + chainType: payload.chainType, + toAddress: payload.toAddress, + netAmount: payload.netAmount, + }), + eventId: `wd-confirmed-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + this.logger.log(`[COMPLETE] Withdrawal ${payload.orderNo} completed successfully`); + } else { + // Step 5b: 转账失败 + throw new Error(result.error || 'Transfer failed'); + } + + } catch (error) { + this.logger.error( + `[ERROR] Failed to process withdrawal ${payload.orderNo}`, + error, + ); + + // 发布失败事件 + await this.eventPublisher.publish({ + eventType: 'blockchain.withdrawal.failed', + toPayload: () => ({ + orderNo: payload.orderNo, + accountSequence: payload.accountSequence, + userId: payload.userId, + status: 'FAILED', + error: error instanceof Error ? error.message : 'Unknown error', + chainType: payload.chainType, + toAddress: payload.toAddress, + netAmount: payload.netAmount, + }), + eventId: `wd-failed-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + throw error; + } + } + + /** + * 发布状态更新 + */ + private async publishStatus( + payload: WithdrawalRequestedPayload, + status: string, + message: string, + ): Promise { + await this.eventPublisher.publish({ + eventType: 'blockchain.withdrawal.status', + toPayload: () => ({ + orderNo: payload.orderNo, + accountSequence: payload.accountSequence, + status, + message, + }), + eventId: `wd-status-${payload.orderNo}-${status}-${Date.now()}`, + occurredAt: new Date(), + }); + } + + /** + * 解析链类型字符串 + */ + private parseChainType(chainType: string): ChainTypeEnum | null { + const normalized = chainType.toUpperCase(); + if (normalized === 'KAVA') return ChainTypeEnum.KAVA; + if (normalized === 'BSC') return ChainTypeEnum.BSC; + return null; + } +} diff --git a/backend/services/mining-blockchain-service/src/application/schedulers/hot-wallet-balance.scheduler.ts b/backend/services/mining-blockchain-service/src/application/schedulers/hot-wallet-balance.scheduler.ts new file mode 100644 index 00000000..e05b752c --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/schedulers/hot-wallet-balance.scheduler.ts @@ -0,0 +1,122 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Cron } from '@nestjs/schedule'; +import { Erc20TransferService } from '@/domain/services/erc20-transfer.service'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { ChainType } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; +import Redis from 'ioredis'; + +/** + * 热钱包余额定时更新调度器 + * [2026-01-07] 更新:添加原生代币 (KAVA) 余额缓存 + * + * 每 5 秒查询热钱包在各链上的余额,并更新到 Redis 缓存: + * - dUSDT (绿积分) 余额 + * - 原生代币 (KAVA/BNB) 余额 + * + * wallet-service 在用户发起转账时读取此缓存,预检查热钱包余额是否足够。 + * reporting-service 读取此缓存用于仪表板显示。 + * + * 注意:使用 Redis DB 0(公共数据库),以便所有服务都能读取。 + * + * Redis Key 格式: + * - hot_wallet:dusdt_balance:{chainType} - dUSDT 余额 + * - hot_wallet:native_balance:{chainType} - 原生代币余额 (KAVA/BNB) + * Redis Value: 余额字符串(如 "10000.00") + * TTL: 30 秒(防止服务故障时缓存过期) + * + * 回滚方式:恢复此文件到之前的版本(移除原生代币缓存逻辑) + */ +@Injectable() +export class HotWalletBalanceScheduler implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(HotWalletBalanceScheduler.name); + + // Redis key 前缀 + private readonly REDIS_KEY_PREFIX_DUSDT = 'hot_wallet:dusdt_balance:'; + private readonly REDIS_KEY_PREFIX_NATIVE = 'hot_wallet:native_balance:'; + + // 缓存过期时间(秒) + private readonly CACHE_TTL_SECONDS = 30; + + // 支持的链类型 + private readonly SUPPORTED_CHAINS = [ChainTypeEnum.KAVA, ChainTypeEnum.BSC]; + + // 使用独立的 Redis 连接,连接到 DB 0(公共数据库) + private readonly sharedRedis: Redis; + + constructor( + private readonly configService: ConfigService, + private readonly transferService: Erc20TransferService, + private readonly evmProvider: EvmProviderAdapter, + ) { + // 创建连接到 DB 0 的 Redis 客户端(公共数据库,所有服务可读取) + this.sharedRedis = new Redis({ + host: this.configService.get('redis.host') || 'localhost', + port: this.configService.get('redis.port') || 6379, + password: this.configService.get('redis.password') || undefined, + db: 0, // 使用 DB 0 作为公共数据库 + }); + + this.sharedRedis.on('connect', () => { + this.logger.log('[REDIS] Connected to shared Redis DB 0 for hot wallet balance'); + }); + + this.sharedRedis.on('error', (err) => { + this.logger.error('[REDIS] Shared Redis connection error', err); + }); + } + + onModuleDestroy() { + this.sharedRedis.disconnect(); + } + + async onModuleInit() { + this.logger.log('[INIT] HotWalletBalanceScheduler initialized'); + // 启动时立即执行一次 + await this.updateHotWalletBalances(); + } + + /** + * 每 5 秒更新热钱包余额到 Redis + */ + @Cron('*/5 * * * * *') // 每 5 秒执行 + async updateHotWalletBalances(): Promise { + for (const chainType of this.SUPPORTED_CHAINS) { + try { + // 检查该链是否已配置 + if (!this.transferService.isConfigured(chainType)) { + this.logger.debug(`[SKIP] Chain ${chainType} not configured, skipping balance update`); + continue; + } + + // 获取热钱包地址 + const hotWalletAddress = this.transferService.getHotWalletAddress(chainType); + if (!hotWalletAddress) { + this.logger.debug(`[SKIP] Hot wallet address not configured for ${chainType}`); + continue; + } + + // 查询 dUSDT 余额 + const dusdtBalance = await this.transferService.getHotWalletBalance(chainType); + const dusdtKey = `${this.REDIS_KEY_PREFIX_DUSDT}${chainType}`; + await this.sharedRedis.setex(dusdtKey, this.CACHE_TTL_SECONDS, dusdtBalance); + + // [2026-01-07] 新增:查询原生代币余额 (KAVA/BNB) + const nativeBalance = await this.evmProvider.getNativeBalance( + ChainType.fromEnum(chainType), + hotWalletAddress, + ); + const nativeKey = `${this.REDIS_KEY_PREFIX_NATIVE}${chainType}`; + await this.sharedRedis.setex(nativeKey, this.CACHE_TTL_SECONDS, nativeBalance.formatted); + + this.logger.debug( + `[UPDATE] ${chainType} hot wallet: dUSDT=${dusdtBalance}, native=${nativeBalance.formatted}`, + ); + } catch (error) { + this.logger.error(`[ERROR] Failed to update ${chainType} hot wallet balance`, error); + // 单链失败不影响其他链的更新 + } + } + } +} diff --git a/backend/services/mining-blockchain-service/src/application/schedulers/index.ts b/backend/services/mining-blockchain-service/src/application/schedulers/index.ts new file mode 100644 index 00000000..f6299b40 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/schedulers/index.ts @@ -0,0 +1 @@ +export * from './hot-wallet-balance.scheduler'; diff --git a/backend/services/mining-blockchain-service/src/application/services/address-derivation.service.ts b/backend/services/mining-blockchain-service/src/application/services/address-derivation.service.ts new file mode 100644 index 00000000..1f6c738f --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/address-derivation.service.ts @@ -0,0 +1,183 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { + AddressDerivationAdapter, + DerivedAddress, +} from '@/infrastructure/blockchain/address-derivation.adapter'; +import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { + MONITORED_ADDRESS_REPOSITORY, + IMonitoredAddressRepository, +} from '@/domain/repositories/monitored-address.repository.interface'; +import { MonitoredAddress } from '@/domain/aggregates/monitored-address'; +import { WalletAddressCreatedEvent } from '@/domain/events'; +import { ChainType, EvmAddress } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface DeriveAddressParams { + userId: bigint; + accountSequence: string; + publicKey: string; +} + +export interface DeriveAddressResult { + userId: bigint; + accountSequence: string; + publicKey: string; + addresses: DerivedAddress[]; +} + +/** + * 地址派生服务 + * 处理从 MPC 公钥派生钱包地址的业务逻辑 + * + * 派生策略: + * - KAVA: EVM 格式 (0x...) - Kava EVM 兼容链 + * - DST: Cosmos bech32 格式 (dst1...) + * - BSC: EVM 格式 (0x...) + * + * 监控策略: + * - EVM 链 (BSC, KAVA) 的地址会被注册到监控列表用于充值检测 + * - Cosmos 链 (DST) 需要不同的监控机制 + */ +@Injectable() +export class AddressDerivationService { + private readonly logger = new Logger(AddressDerivationService.name); + + // EVM 链类型列表,用于判断是否需要注册监控 + private readonly evmChains = new Set([ChainTypeEnum.BSC, ChainTypeEnum.KAVA]); + + constructor( + private readonly addressDerivation: AddressDerivationAdapter, + private readonly recoveryMnemonic: RecoveryMnemonicAdapter, + private readonly addressCache: AddressCacheService, + private readonly eventPublisher: EventPublisherService, + private readonly prisma: PrismaService, + @Inject(MONITORED_ADDRESS_REPOSITORY) + private readonly monitoredAddressRepo: IMonitoredAddressRepository, + ) {} + + /** + * 从公钥派生地址并注册监控 + */ + async deriveAndRegister(params: DeriveAddressParams): Promise { + const { userId, accountSequence, publicKey } = params; + this.logger.log(`[DERIVE] Starting address derivation for user ${userId}, account ${accountSequence}`); + this.logger.log(`[DERIVE] Public key: ${publicKey.substring(0, 30)}...`); + + // 1. 派生所有链的地址 (包括 Cosmos 和 EVM) + const derivedAddresses = this.addressDerivation.deriveAllAddresses(publicKey); + this.logger.log(`[DERIVE] Derived ${derivedAddresses.length} addresses`); + + // 2. 只为 EVM 链注册监控地址 (用于充值检测) + for (const derived of derivedAddresses) { + if (this.evmChains.has(derived.chainType)) { + await this.registerEvmAddressForMonitoring(userId, accountSequence, derived); + } else { + this.logger.log(`[DERIVE] Skipping monitoring registration for Cosmos chain: ${derived.chainType} - ${derived.address}`); + } + } + + // 3. 生成恢复助记词 (与账户序列号关联) + this.logger.log(`[MNEMONIC] Generating recovery mnemonic for account ${accountSequence}`); + const mnemonicResult = await this.recoveryMnemonic.generateMnemonic({ + userId: userId.toString(), + publicKey, + }); + this.logger.log(`[MNEMONIC] Recovery mnemonic generated, hash: ${mnemonicResult.mnemonicHash.slice(0, 16)}...`); + + // 4. 存储恢复助记词到 blockchain-service 数据库 (使用 accountSequence 关联) + // 检查是否已存在,避免重复创建 + const existingMnemonic = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'ACTIVE', + }, + }); + + if (existingMnemonic) { + this.logger.warn(`[MNEMONIC] Recovery mnemonic already exists for account ${accountSequence}, skipping creation`); + } else { + await this.prisma.recoveryMnemonic.create({ + data: { + accountSequence, + publicKey, + encryptedMnemonic: mnemonicResult.encryptedMnemonic, + mnemonicHash: mnemonicResult.mnemonicHash, + status: 'ACTIVE', + isBackedUp: false, + }, + }); + this.logger.log(`[MNEMONIC] Recovery mnemonic saved for account ${accountSequence}`); + } + + // 5. 发布钱包地址创建事件 (包含所有链的地址和助记词) + const event = new WalletAddressCreatedEvent({ + userId: userId.toString(), + accountSequence, + publicKey, + addresses: derivedAddresses.map((a) => ({ + chainType: a.chainType, + address: a.address, + })), + // 恢复助记词 (明文仅在事件中传递给客户端首次显示) + mnemonic: mnemonicResult.mnemonic, + encryptedMnemonic: mnemonicResult.encryptedMnemonic, + mnemonicHash: mnemonicResult.mnemonicHash, + }); + + this.logger.log(`[PUBLISH] Publishing WalletAddressCreated event for account ${accountSequence}`); + this.logger.log(`[PUBLISH] Addresses: ${JSON.stringify(derivedAddresses)}`); + await this.eventPublisher.publish(event); + this.logger.log(`[PUBLISH] WalletAddressCreated event published successfully`); + + return { + userId, + accountSequence, + publicKey, + addresses: derivedAddresses, + }; + } + + /** + * 注册 EVM 地址用于充值监控 + */ + private async registerEvmAddressForMonitoring( + userId: bigint, + accountSequence: string, + derived: DerivedAddress, + ): Promise { + const chainType = ChainType.fromEnum(derived.chainType); + const address = EvmAddress.create(derived.address); + + // 检查是否已存在 + const exists = await this.monitoredAddressRepo.existsByChainAndAddress(chainType, address); + if (!exists) { + // 创建监控地址 - 使用 accountSequence 作为跨服务关联键 + const monitored = MonitoredAddress.create({ + chainType, + address, + accountSequence, + userId, + }); + + await this.monitoredAddressRepo.save(monitored); + + // 添加到缓存 + await this.addressCache.addAddress(chainType, address.lowercase); + + this.logger.log(`[MONITOR] Registered EVM address for monitoring: ${derived.chainType} - ${derived.address} (account ${accountSequence})`); + } else { + this.logger.debug(`[MONITOR] Address already registered: ${derived.chainType} - ${derived.address}`); + } + } + + /** + * 获取用户的所有地址 + */ + async getUserAddresses(userId: bigint): Promise { + return this.monitoredAddressRepo.findByUserId(userId); + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/balance-query.service.ts b/backend/services/mining-blockchain-service/src/application/services/balance-query.service.ts new file mode 100644 index 00000000..70db5af5 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/balance-query.service.ts @@ -0,0 +1,93 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { ChainType } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface BalanceResult { + chainType: string; + address: string; + usdtBalance: string; + usdtRawBalance: string; + usdtDecimals: number; + nativeBalance: string; + nativeSymbol: string; +} + +/** + * 余额查询服务 + */ +@Injectable() +export class BalanceQueryService { + private readonly logger = new Logger(BalanceQueryService.name); + + constructor( + private readonly evmProvider: EvmProviderAdapter, + private readonly chainConfig: ChainConfigService, + ) {} + + /** + * 查询单个地址的余额 + */ + async getBalance(chainType: ChainType, address: string): Promise { + const config = this.chainConfig.getConfig(chainType); + + const [usdtBalance, nativeBalance] = await Promise.all([ + this.evmProvider.getTokenBalance(chainType, config.usdtContract, address), + this.evmProvider.getNativeBalance(chainType, address), + ]); + + return { + chainType: chainType.toString(), + address, + usdtBalance: usdtBalance.formatted, + usdtRawBalance: usdtBalance.raw.toString(), + usdtDecimals: usdtBalance.decimals, + nativeBalance: nativeBalance.formatted, + nativeSymbol: config.nativeSymbol, + }; + } + + /** + * 查询多条链的余额 + */ + async getBalances(address: string, chainTypes?: ChainTypeEnum[]): Promise { + const chains = chainTypes || this.chainConfig.getSupportedChains(); + const results: BalanceResult[] = []; + + for (const chainTypeEnum of chains) { + try { + const chainType = ChainType.fromEnum(chainTypeEnum); + const balance = await this.getBalance(chainType, address); + results.push(balance); + } catch (error) { + this.logger.error(`Error querying balance for ${chainTypeEnum}:`, error); + } + } + + return results; + } + + /** + * 批量查询地址余额 + */ + async getBatchBalances( + chainType: ChainType, + addresses: string[], + ): Promise> { + const results = new Map(); + + await Promise.all( + addresses.map(async (address) => { + try { + const balance = await this.getBalance(chainType, address); + results.set(address.toLowerCase(), balance); + } catch (error) { + this.logger.error(`Error querying balance for ${address}:`, error); + } + }), + ); + + return results; + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/deposit-detection.service.ts b/backend/services/mining-blockchain-service/src/application/services/deposit-detection.service.ts new file mode 100644 index 00000000..4a14e127 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/deposit-detection.service.ts @@ -0,0 +1,253 @@ +import { Injectable, Logger, Inject, OnModuleInit } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { + BlockScannerService, + DepositEvent, +} from '@/infrastructure/blockchain/block-scanner.service'; +import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { ConfirmationPolicyService } from '@/domain/services/confirmation-policy.service'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { Erc20TransferService } from '@/domain/services/erc20-transfer.service'; +import { + DEPOSIT_TRANSACTION_REPOSITORY, + IDepositTransactionRepository, +} from '@/domain/repositories/deposit-transaction.repository.interface'; +import { + MONITORED_ADDRESS_REPOSITORY, + IMonitoredAddressRepository, +} from '@/domain/repositories/monitored-address.repository.interface'; +import { + BLOCK_CHECKPOINT_REPOSITORY, + IBlockCheckpointRepository, +} from '@/domain/repositories/block-checkpoint.repository.interface'; +import { + OUTBOX_EVENT_REPOSITORY, + IOutboxEventRepository, +} from '@/domain/repositories/outbox-event.repository.interface'; +import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domain/value-objects'; +import { DepositConfirmedEvent } from '@/domain/events'; + +/** + * 充值检测服务 + * 负责扫描区块链、检测充值、更新确认状态 + */ +@Injectable() +export class DepositDetectionService implements OnModuleInit { + private readonly logger = new Logger(DepositDetectionService.name); + + constructor( + private readonly blockScanner: BlockScannerService, + private readonly evmProvider: EvmProviderAdapter, + private readonly eventPublisher: EventPublisherService, + private readonly addressCache: AddressCacheService, + private readonly confirmationPolicy: ConfirmationPolicyService, + private readonly chainConfig: ChainConfigService, + private readonly transferService: Erc20TransferService, + @Inject(DEPOSIT_TRANSACTION_REPOSITORY) + private readonly depositRepo: IDepositTransactionRepository, + @Inject(MONITORED_ADDRESS_REPOSITORY) + private readonly monitoredAddressRepo: IMonitoredAddressRepository, + @Inject(BLOCK_CHECKPOINT_REPOSITORY) + private readonly checkpointRepo: IBlockCheckpointRepository, + @Inject(OUTBOX_EVENT_REPOSITORY) + private readonly outboxRepo: IOutboxEventRepository, + ) {} + + async onModuleInit() { + // 初始化地址缓存 + await this.initializeAddressCache(); + this.logger.log('DepositDetectionService initialized'); + } + + /** + * 初始化地址缓存 + */ + private async initializeAddressCache(): Promise { + for (const chainTypeEnum of this.chainConfig.getSupportedChains()) { + const chainType = ChainType.fromEnum(chainTypeEnum); + const addresses = await this.monitoredAddressRepo.getAllActiveAddresses(chainType); + await this.addressCache.reloadCache(chainType, addresses); + this.logger.log(`Loaded ${addresses.length} addresses for ${chainTypeEnum} into cache`); + } + } + + /** + * 定时扫描区块(每5秒) + */ + @Cron(CronExpression.EVERY_5_SECONDS) + async scanBlocks(): Promise { + for (const chainTypeEnum of this.chainConfig.getSupportedChains()) { + try { + await this.scanChain(ChainType.fromEnum(chainTypeEnum)); + } catch (error) { + this.logger.error(`Error scanning ${chainTypeEnum}:`, error); + await this.checkpointRepo.recordError( + ChainType.fromEnum(chainTypeEnum), + error instanceof Error ? error.message : 'Unknown error', + ); + } + } + } + + /** + * 扫描单条链 + */ + private async scanChain(chainType: ChainType): Promise { + // 检查缓存是否为空,如果为空则自动从数据库重新加载 + const cacheCount = await this.addressCache.getCount(chainType); + if (cacheCount === 0) { + this.logger.warn(`Address cache empty for ${chainType}, reloading from database...`); + const addresses = await this.monitoredAddressRepo.getAllActiveAddresses(chainType); + await this.addressCache.reloadCache(chainType, addresses); + this.logger.log(`Reloaded ${addresses.length} addresses for ${chainType} into cache`); + } + + // 获取上次扫描位置 + let lastBlock = await this.checkpointRepo.getLastScannedBlock(chainType); + + if (!lastBlock) { + // 首次扫描,从当前区块开始 + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + lastBlock = currentBlock.subtract(10); // 从10个块前开始 + await this.checkpointRepo.initializeIfNotExists(chainType, lastBlock); + } + + // 执行扫描 + const { deposits, newLastBlock } = await this.blockScanner.executeScan(chainType, lastBlock); + + // 处理检测到的充值 + for (const deposit of deposits) { + await this.processDeposit(deposit); + } + + // 更新检查点 + if (newLastBlock.isGreaterThan(lastBlock)) { + await this.checkpointRepo.updateCheckpoint(chainType, newLastBlock); + } + } + + /** + * 处理检测到的充值 + */ + private async processDeposit(event: DepositEvent): Promise { + const txHash = TxHash.create(event.txHash); + + // 检查是否已处理 + if (await this.depositRepo.existsByTxHash(txHash)) { + this.logger.debug(`Deposit already processed: ${event.txHash}`); + return; + } + + const chainType = ChainType.fromEnum(event.chainType); + + // 过滤从热钱包发出的转账(内部转账/提现),避免重复入账 + const hotWalletAddress = this.transferService.getHotWalletAddress(event.chainType); + if (hotWalletAddress && event.from.toLowerCase() === hotWalletAddress.toLowerCase()) { + this.logger.debug(`Skipping hot wallet outgoing transfer: ${event.txHash} from ${event.from}`); + return; + } + + // 查找监控地址以获取用户ID或系统账户信息 + const monitoredAddress = await this.monitoredAddressRepo.findByChainAndAddress( + chainType, + EvmAddress.fromUnchecked(event.to), + ); + + if (!monitoredAddress || !monitoredAddress.id) { + this.logger.warn(`Monitored address not found: ${event.to}`); + return; + } + + // 获取代币的实际 decimals(USDT 通常是 6 位,而不是 18 位) + const tokenDecimals = await this.evmProvider.getTokenDecimals(chainType, event.tokenContract); + + // 创建充值记录 - 用户地址 + const deposit = DepositTransaction.create({ + chainType, + txHash, + fromAddress: EvmAddress.fromUnchecked(event.from), + toAddress: EvmAddress.fromUnchecked(event.to), + tokenContract: EvmAddress.fromUnchecked(event.tokenContract), + amount: TokenAmount.fromRaw(event.value, tokenDecimals), + blockNumber: BlockNumber.create(event.blockNumber), + blockTimestamp: event.blockTimestamp, + logIndex: event.logIndex, + addressId: monitoredAddress.id, + accountSequence: monitoredAddress.accountSequence, + userId: monitoredAddress.userId, + }); + + // 保存 + await this.depositRepo.save(deposit); + + // 发布事件 + for (const domainEvent of deposit.domainEvents) { + await this.eventPublisher.publish(domainEvent); + } + deposit.clearDomainEvents(); + + this.logger.log( + `User deposit saved: ${txHash.toShort()} -> ${event.to} (${deposit.amount.formatted} USDT)`, + ); + } + + /** + * 定时更新确认状态(每30秒) + */ + @Cron(CronExpression.EVERY_30_SECONDS) + async updateConfirmations(): Promise { + for (const chainTypeEnum of this.chainConfig.getSupportedChains()) { + try { + await this.updateChainConfirmations(ChainType.fromEnum(chainTypeEnum)); + } catch (error) { + this.logger.error(`Error updating confirmations for ${chainTypeEnum}:`, error); + } + } + } + + /** + * 更新单条链的确认状态 + */ + private async updateChainConfirmations(chainType: ChainType): Promise { + const pendingDeposits = await this.depositRepo.findPendingConfirmation(chainType); + if (pendingDeposits.length === 0) return; + + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + const requiredConfirmations = this.confirmationPolicy.getRequiredConfirmations(chainType); + + for (const deposit of pendingDeposits) { + deposit.updateConfirmations(currentBlock, requiredConfirmations); + + await this.depositRepo.save(deposit); + + // 处理领域事件 + for (const event of deposit.domainEvents) { + if (event instanceof DepositConfirmedEvent) { + // 重要事件写入 outbox,保证可靠投递 + await this.outboxRepo.create({ + eventType: event.eventType, + aggregateId: deposit.id?.toString() || deposit.txHash.toString(), + aggregateType: 'DepositTransaction', + payload: event.toPayload(), + }); + this.logger.log( + `Deposit confirmed event saved to outbox: ${deposit.txHash.toShort()} (${deposit.confirmations} confirmations)`, + ); + } else { + // 非关键事件直接发送(如 DepositDetectedEvent) + await this.eventPublisher.publish(event); + } + } + deposit.clearDomainEvents(); + + if (deposit.isConfirmed) { + this.logger.log( + `Deposit confirmed: ${deposit.txHash.toShort()} (${deposit.confirmations} confirmations)`, + ); + } + } + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/deposit-repair.service.ts b/backend/services/mining-blockchain-service/src/application/services/deposit-repair.service.ts new file mode 100644 index 00000000..9a98b6d7 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/deposit-repair.service.ts @@ -0,0 +1,195 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { + DEPOSIT_TRANSACTION_REPOSITORY, + IDepositTransactionRepository, +} from '@/domain/repositories/deposit-transaction.repository.interface'; +import { + OUTBOX_EVENT_REPOSITORY, + IOutboxEventRepository, + OutboxEventStatus, +} from '@/domain/repositories/outbox-event.repository.interface'; +import { DepositConfirmedEvent } from '@/domain/events'; + +/** + * 充值修复服务 + * + * 用于诊断和修复历史遗留的充值问题: + * 1. CONFIRMED 状态但未在 Outbox 中的充值 + * 2. Outbox 中 FAILED 状态的事件 + * 3. 手动重新发送充值事件 + */ +@Injectable() +export class DepositRepairService { + private readonly logger = new Logger(DepositRepairService.name); + + constructor( + @Inject(DEPOSIT_TRANSACTION_REPOSITORY) + private readonly depositRepo: IDepositTransactionRepository, + @Inject(OUTBOX_EVENT_REPOSITORY) + private readonly outboxRepo: IOutboxEventRepository, + ) {} + + /** + * 诊断:查询所有需要修复的充值 + */ + async diagnose(): Promise<{ + confirmedNotNotified: Array<{ + id: string; + txHash: string; + userId: string; + accountSequence: string; + amount: string; + confirmedAt: string; + }>; + outboxPending: number; + outboxSent: number; + outboxFailed: number; + }> { + // 查找 CONFIRMED 但未通知的充值 + const pendingDeposits = await this.depositRepo.findPendingNotification(); + + // 统计 Outbox 中各状态的事件数量 + const [pending, sent, failed] = await Promise.all([ + this.outboxRepo.findPendingEvents(1000), + this.outboxRepo.findUnackedEvents(0, 1000), // SENT 状态 + this.getFailedOutboxCount(), + ]); + + return { + confirmedNotNotified: pendingDeposits.map((d) => ({ + id: d.id?.toString() ?? '', + txHash: d.txHash.toString(), + userId: d.userId.toString(), + accountSequence: d.accountSequence, + amount: d.amount.toFixed(6), + confirmedAt: d.createdAt?.toISOString() ?? '', + })), + outboxPending: pending.length, + outboxSent: sent.length, + outboxFailed: failed, + }; + } + + /** + * 修复单个充值:重新创建 Outbox 事件 + */ + async repairDeposit(depositId: bigint): Promise<{ + success: boolean; + message: string; + }> { + const deposit = await this.depositRepo.findById(depositId); + + if (!deposit) { + return { success: false, message: `Deposit ${depositId} not found` }; + } + + if (deposit.notifiedAt) { + return { + success: false, + message: `Deposit ${depositId} already notified at ${deposit.notifiedAt.toISOString()}`, + }; + } + + // 创建 DepositConfirmedEvent + const event = new DepositConfirmedEvent({ + depositId: deposit.id?.toString() ?? '', + chainType: deposit.chainType.toString(), + txHash: deposit.txHash.toString(), + toAddress: deposit.toAddress.toString(), + amount: deposit.amount.raw.toString(), + amountFormatted: deposit.amount.toFixed(8), + confirmations: deposit.confirmations, + accountSequence: deposit.accountSequence, + userId: deposit.userId.toString(), + }); + + // 写入 Outbox + await this.outboxRepo.create({ + eventType: event.eventType, + aggregateId: deposit.id?.toString() ?? deposit.txHash.toString(), + aggregateType: 'DepositTransaction', + payload: event.toPayload(), + }); + + this.logger.log(`Created repair outbox event for deposit ${depositId}`); + + return { + success: true, + message: `Created outbox event for deposit ${depositId}, will be sent in next cycle`, + }; + } + + /** + * 批量修复所有未通知的充值 + */ + async repairAll(): Promise<{ + total: number; + success: number; + failed: number; + details: Array<{ id: string; success: boolean; message: string }>; + }> { + const pendingDeposits = await this.depositRepo.findPendingNotification(); + + this.logger.log(`Starting batch repair for ${pendingDeposits.length} deposits`); + + const results: Array<{ id: string; success: boolean; message: string }> = []; + let successCount = 0; + let failedCount = 0; + + for (const deposit of pendingDeposits) { + const depositId = deposit.id; + if (!depositId) { + results.push({ id: 'unknown', success: false, message: 'No deposit ID' }); + failedCount++; + continue; + } + + try { + const result = await this.repairDeposit(depositId); + results.push({ id: depositId.toString(), ...result }); + if (result.success) { + successCount++; + } else { + failedCount++; + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + results.push({ id: depositId.toString(), success: false, message }); + failedCount++; + this.logger.error(`Failed to repair deposit ${depositId}: ${message}`); + } + } + + this.logger.log(`Batch repair completed: ${successCount} success, ${failedCount} failed`); + + return { + total: pendingDeposits.length, + success: successCount, + failed: failedCount, + details: results, + }; + } + + /** + * 重置失败的 Outbox 事件为 PENDING + * 注意:当前接口不支持直接查询 FAILED 状态的事件 + * 此方法暂时返回空结果 + */ + async resetFailedOutboxEvents(): Promise<{ + reset: number; + message: string; + }> { + // 当前接口不支持查询 FAILED 状态的事件 + // 需要在 IOutboxEventRepository 中添加 findFailedEvents 方法 + this.logger.warn('resetFailedOutboxEvents: Not implemented - interface does not support finding FAILED events'); + return { + reset: 0, + message: 'Not implemented - use direct database query to find and reset FAILED events', + }; + } + + private async getFailedOutboxCount(): Promise { + // 当前接口不支持查询 FAILED 状态的事件 + return 0; + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/index.ts b/backend/services/mining-blockchain-service/src/application/services/index.ts new file mode 100644 index 00000000..56614ebf --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/index.ts @@ -0,0 +1,7 @@ +export * from './address-derivation.service'; +export * from './deposit-detection.service'; +export * from './balance-query.service'; +export * from './mnemonic-verification.service'; +export * from './outbox-publisher.service'; +export * from './deposit-repair.service'; +export * from './mpc-transfer-initializer.service'; diff --git a/backend/services/mining-blockchain-service/src/application/services/mnemonic-verification.service.ts b/backend/services/mining-blockchain-service/src/application/services/mnemonic-verification.service.ts new file mode 100644 index 00000000..6fba7cde --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/mnemonic-verification.service.ts @@ -0,0 +1,173 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter'; + +export interface VerifyMnemonicByAccountParams { + accountSequence: string; + mnemonic: string; +} + +export interface VerifyMnemonicResult { + valid: boolean; + message?: string; +} + +/** + * 助记词验证服务 + * 通过账户序列号查询存储的哈希并验证助记词 + */ +@Injectable() +export class MnemonicVerificationService { + private readonly logger = new Logger(MnemonicVerificationService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly recoveryMnemonic: RecoveryMnemonicAdapter, + ) {} + + /** + * 验证助记词是否匹配指定账户 + */ + async verifyMnemonicByAccount(params: VerifyMnemonicByAccountParams): Promise { + const { accountSequence, mnemonic } = params; + this.logger.log(`Verifying mnemonic for account ${accountSequence}`); + + // 1. 先检查是否有已挂失的助记词 (安全检查) + const revokedRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'REVOKED', + }, + orderBy: { revokedAt: 'desc' }, + }); + + if (revokedRecord) { + this.logger.warn(`Account ${accountSequence} has revoked mnemonic, rejecting recovery attempt`); + return { + valid: false, + message: '该助记词已被挂失,无法用于账户恢复。如需帮助请联系客服。', + }; + } + + // 2. 查询账户的 ACTIVE 助记词记录 + const recoveryRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'ACTIVE', + }, + }); + + if (!recoveryRecord) { + this.logger.warn(`No active recovery mnemonic found for account ${accountSequence}`); + return { + valid: false, + message: 'Account has no recovery mnemonic configured', + }; + } + + // 3. 使用 RecoveryMnemonicAdapter 验证哈希 (bcrypt 是异步的) + const result = await this.recoveryMnemonic.verifyMnemonic(mnemonic, recoveryRecord.mnemonicHash); + + if (result.valid) { + this.logger.log(`Mnemonic verified successfully for account ${accountSequence}`); + } else { + this.logger.warn(`Mnemonic verification failed for account ${accountSequence}: ${result.message}`); + } + + return result; + } + + /** + * 保存助记词记录(创建账户时调用) + */ + async saveRecoveryMnemonic(params: { + accountSequence: string; + publicKey: string; + encryptedMnemonic: string; + mnemonicHash: string; + }): Promise { + this.logger.log(`Saving recovery mnemonic for account ${params.accountSequence}`); + + await this.prisma.recoveryMnemonic.create({ + data: { + accountSequence: params.accountSequence, + publicKey: params.publicKey, + encryptedMnemonic: params.encryptedMnemonic, + mnemonicHash: params.mnemonicHash, + status: 'ACTIVE', + isBackedUp: false, + }, + }); + + this.logger.log(`Recovery mnemonic saved for account ${params.accountSequence}`); + } + + /** + * 标记助记词已备份 + */ + async markAsBackedUp(accountSequence: string): Promise { + await this.prisma.recoveryMnemonic.updateMany({ + where: { + accountSequence, + status: 'ACTIVE', + }, + data: { + isBackedUp: true, + }, + }); + this.logger.log(`Recovery mnemonic marked as backed up for account ${accountSequence}`); + } + + /** + * 挂失助记词 + * 将 ACTIVE 状态的助记词标记为 REVOKED + */ + async revokeMnemonic(accountSequence: string, reason: string): Promise<{ success: boolean; message: string }> { + this.logger.log(`Revoking mnemonic for account ${accountSequence}, reason: ${reason}`); + + // 查找 ACTIVE 状态的助记词 + const activeRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'ACTIVE', + }, + }); + + if (!activeRecord) { + this.logger.warn(`No active mnemonic found for account ${accountSequence}`); + return { + success: false, + message: '该账户没有可挂失的助记词', + }; + } + + // 更新状态为 REVOKED + await this.prisma.recoveryMnemonic.update({ + where: { id: activeRecord.id }, + data: { + status: 'REVOKED', + revokedAt: new Date(), + revokedReason: reason, + }, + }); + + this.logger.log(`Mnemonic revoked successfully for account ${accountSequence}`); + return { + success: true, + message: '助记词已挂失', + }; + } + + /** + * 检查助记词是否已挂失 + */ + async isMnemonicRevoked(accountSequence: string): Promise { + const revokedRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'REVOKED', + }, + }); + return !!revokedRecord; + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/mpc-transfer-initializer.service.ts b/backend/services/mining-blockchain-service/src/application/services/mpc-transfer-initializer.service.ts new file mode 100644 index 00000000..124c190d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/mpc-transfer-initializer.service.ts @@ -0,0 +1,26 @@ +/** + * MPC Transfer Initializer Service + * + * 在应用启动时将 MPC 签名客户端注入到 ERC20 转账服务中 + * 用于解决循环依赖问题(Domain 层不能直接依赖 Infrastructure 层) + */ + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Erc20TransferService } from '@/domain/services/erc20-transfer.service'; +import { MpcSigningClient } from '@/infrastructure/mpc'; + +@Injectable() +export class MpcTransferInitializerService implements OnModuleInit { + private readonly logger = new Logger(MpcTransferInitializerService.name); + + constructor( + private readonly erc20TransferService: Erc20TransferService, + private readonly mpcSigningClient: MpcSigningClient, + ) {} + + onModuleInit() { + this.logger.log('[INIT] Injecting MPC Signing Client into ERC20 Transfer Service'); + this.erc20TransferService.setMpcSigningClient(this.mpcSigningClient); + this.logger.log('[INIT] MPC Signing Client injection complete'); + } +} diff --git a/backend/services/mining-blockchain-service/src/application/services/outbox-publisher.service.ts b/backend/services/mining-blockchain-service/src/application/services/outbox-publisher.service.ts new file mode 100644 index 00000000..17decb6c --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/outbox-publisher.service.ts @@ -0,0 +1,148 @@ +import { Injectable, Logger, Inject, OnModuleInit } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { + OUTBOX_EVENT_REPOSITORY, + IOutboxEventRepository, + OutboxEventStatus, +} from '@/domain/repositories/outbox-event.repository.interface'; +import { + DEPOSIT_TRANSACTION_REPOSITORY, + IDepositTransactionRepository, +} from '@/domain/repositories/deposit-transaction.repository.interface'; + +/** + * Outbox 发布服务 + * + * 定时扫描 outbox_events 表,将待发送的事件发布到 Kafka。 + * 实现发件箱模式(Outbox Pattern),保证事件的可靠投递。 + */ +@Injectable() +export class OutboxPublisherService implements OnModuleInit { + private readonly logger = new Logger(OutboxPublisherService.name); + + // 发送超时时间(秒)- 超过此时间未收到 ACK 则重发 + private readonly SENT_TIMEOUT_SECONDS = 300; // 5 分钟 + + // 清理已确认事件的天数 + private readonly CLEANUP_DAYS = 7; + + constructor( + private readonly eventPublisher: EventPublisherService, + @Inject(OUTBOX_EVENT_REPOSITORY) + private readonly outboxRepo: IOutboxEventRepository, + @Inject(DEPOSIT_TRANSACTION_REPOSITORY) + private readonly depositRepo: IDepositTransactionRepository, + ) {} + + async onModuleInit() { + this.logger.log('OutboxPublisherService initialized'); + } + + /** + * 定时发布待发送事件(每5秒) + */ + @Cron(CronExpression.EVERY_5_SECONDS) + async publishPendingEvents(): Promise { + try { + const pendingEvents = await this.outboxRepo.findPendingEvents(50); + + if (pendingEvents.length === 0) return; + + this.logger.debug(`Found ${pendingEvents.length} pending events to publish`); + + for (const event of pendingEvents) { + try { + // 发送到 Kafka + await this.eventPublisher.publishRaw({ + eventId: `outbox-${event.id}`, + eventType: event.eventType, + occurredAt: event.createdAt, + payload: event.payload, + }); + + // 标记为已发送 + await this.outboxRepo.markAsSent(event.id); + + this.logger.log( + `Published event ${event.id}: ${event.eventType} for ${event.aggregateType}:${event.aggregateId}`, + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + this.logger.error(`Failed to publish event ${event.id}: ${errorMessage}`); + + // 记录失败,安排重试 + await this.outboxRepo.recordFailure(event.id, errorMessage); + } + } + } catch (error) { + this.logger.error('Error in publishPendingEvents:', error); + } + } + + /** + * 定时检查超时未确认的事件(每分钟) + * 将 SENT 状态但超时的事件重置为 PENDING + */ + @Cron(CronExpression.EVERY_MINUTE) + async checkUnackedEvents(): Promise { + try { + const unackedEvents = await this.outboxRepo.findUnackedEvents( + this.SENT_TIMEOUT_SECONDS, + 50, + ); + + if (unackedEvents.length === 0) return; + + this.logger.warn(`Found ${unackedEvents.length} unacked events, will retry`); + + for (const event of unackedEvents) { + // 记录超时失败,触发重试逻辑 + await this.outboxRepo.recordFailure(event.id, 'ACK timeout'); + this.logger.warn( + `Event ${event.id} ACK timeout, scheduled for retry (attempt ${event.retryCount + 1})`, + ); + } + } catch (error) { + this.logger.error('Error in checkUnackedEvents:', error); + } + } + + /** + * 定时清理已确认的旧事件(每天凌晨3点) + */ + @Cron('0 3 * * *') + async cleanupOldEvents(): Promise { + try { + const count = await this.outboxRepo.cleanupAckedEvents(this.CLEANUP_DAYS); + if (count > 0) { + this.logger.log(`Cleaned up ${count} old ACKED events`); + } + } catch (error) { + this.logger.error('Error in cleanupOldEvents:', error); + } + } + + /** + * 处理 ACK 确认 + * 当收到 wallet-service 的确认事件时调用 + */ + async handleAck(aggregateType: string, aggregateId: string, eventType: string): Promise { + try { + await this.outboxRepo.markAsAckedByAggregateId(aggregateType, aggregateId, eventType); + + // 同时更新 deposit_transactions 表的 notified_at + if (aggregateType === 'DepositTransaction') { + const depositId = BigInt(aggregateId); + const deposit = await this.depositRepo.findById(depositId); + if (deposit) { + deposit.markAsNotified(); + await this.depositRepo.save(deposit); + this.logger.log(`Deposit ${aggregateId} marked as notified`); + } + } + } catch (error) { + this.logger.error(`Error handling ACK for ${aggregateType}:${aggregateId}:`, error); + } + } +} diff --git a/backend/services/mining-blockchain-service/src/config/app.config.ts b/backend/services/mining-blockchain-service/src/config/app.config.ts new file mode 100644 index 00000000..e655251c --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/app.config.ts @@ -0,0 +1,7 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('app', () => ({ + nodeEnv: process.env.NODE_ENV || 'development', + port: parseInt(process.env.PORT || '3012', 10), + serviceName: process.env.SERVICE_NAME || 'blockchain-service', +})); diff --git a/backend/services/mining-blockchain-service/src/config/blockchain.config.ts b/backend/services/mining-blockchain-service/src/config/blockchain.config.ts new file mode 100644 index 00000000..b7c7e012 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/blockchain.config.ts @@ -0,0 +1,65 @@ +import { registerAs } from '@nestjs/config'; + +/** + * 区块链配置 + * + * 支持主网和测试网切换,通过 NETWORK_MODE 环境变量控制: + * - NETWORK_MODE=mainnet (默认): 使用主网配置 + * - NETWORK_MODE=testnet: 使用测试网配置 + * + * 测试网配置: + * - BSC Testnet: Chain ID 97, 水龙头: https://testnet.bnbchain.org/faucet-smart + * - KAVA Testnet: Chain ID 2221, 水龙头: https://faucet.kava.io + */ +export default registerAs('blockchain', () => { + const networkMode = process.env.NETWORK_MODE || 'mainnet'; + const isTestnet = networkMode === 'testnet'; + + return { + // 网络模式 + networkMode, + isTestnet, + + // 通用配置 + scanIntervalMs: parseInt(process.env.BLOCK_SCAN_INTERVAL_MS || '5000', 10), + confirmationsRequired: parseInt(process.env.BLOCK_CONFIRMATIONS_REQUIRED || (isTestnet ? '3' : '12'), 10), + scanBatchSize: parseInt(process.env.BLOCK_SCAN_BATCH_SIZE || '100', 10), + + // KAVA 配置 + kava: isTestnet + ? { + // KAVA Testnet + rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.testnet.kava.io', + chainId: parseInt(process.env.KAVA_CHAIN_ID || '2221', 10), + // 测试网 USDT 合约 (自定义部署的 TestUSDT) + usdtContract: process.env.KAVA_USDT_CONTRACT || '0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF', + confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '3', 10), + } + : { + // KAVA Mainnet + rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.kava.io', + chainId: parseInt(process.env.KAVA_CHAIN_ID || '2222', 10), + // dUSDT (绿积分) 合约地址 - Durian USDT, 精度6位 + usdtContract: process.env.KAVA_USDT_CONTRACT || '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3', + confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '12', 10), + }, + + // BSC 配置 + bsc: isTestnet + ? { + // BSC Testnet (BNB Smart Chain Testnet) + rpcUrl: process.env.BSC_RPC_URL || 'https://data-seed-prebsc-1-s1.binance.org:8545', + chainId: parseInt(process.env.BSC_CHAIN_ID || '97', 10), + // BSC Testnet 官方测试 USDT 合约 + usdtContract: process.env.BSC_USDT_CONTRACT || '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd', + confirmations: parseInt(process.env.BSC_CONFIRMATIONS || '3', 10), + } + : { + // BSC Mainnet + rpcUrl: process.env.BSC_RPC_URL || 'https://bsc-dataseed.binance.org', + chainId: parseInt(process.env.BSC_CHAIN_ID || '56', 10), + usdtContract: process.env.BSC_USDT_CONTRACT || '0x55d398326f99059fF775485246999027B3197955', + confirmations: parseInt(process.env.BSC_CONFIRMATIONS || '15', 10), + }, + }; +}); diff --git a/backend/services/mining-blockchain-service/src/config/database.config.ts b/backend/services/mining-blockchain-service/src/config/database.config.ts new file mode 100644 index 00000000..4bc50472 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/database.config.ts @@ -0,0 +1,5 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('database', () => ({ + url: process.env.DATABASE_URL, +})); diff --git a/backend/services/mining-blockchain-service/src/config/index.ts b/backend/services/mining-blockchain-service/src/config/index.ts new file mode 100644 index 00000000..3a3ace47 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/index.ts @@ -0,0 +1,5 @@ +export { default as appConfig } from './app.config'; +export { default as databaseConfig } from './database.config'; +export { default as redisConfig } from './redis.config'; +export { default as kafkaConfig } from './kafka.config'; +export { default as blockchainConfig } from './blockchain.config'; diff --git a/backend/services/mining-blockchain-service/src/config/kafka.config.ts b/backend/services/mining-blockchain-service/src/config/kafka.config.ts new file mode 100644 index 00000000..6411acbc --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/kafka.config.ts @@ -0,0 +1,7 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('kafka', () => ({ + brokers: (process.env.KAFKA_BROKERS || 'localhost:9092').split(','), + clientId: process.env.KAFKA_CLIENT_ID || 'blockchain-service', + groupId: process.env.KAFKA_GROUP_ID || 'blockchain-service-group', +})); diff --git a/backend/services/mining-blockchain-service/src/config/redis.config.ts b/backend/services/mining-blockchain-service/src/config/redis.config.ts new file mode 100644 index 00000000..b3b8a1b0 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/config/redis.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('redis', () => ({ + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379', 10), + db: parseInt(process.env.REDIS_DB || '11', 10), + password: process.env.REDIS_PASSWORD || undefined, +})); diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/aggregate-root.base.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/aggregate-root.base.ts new file mode 100644 index 00000000..b6eea8df --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/aggregate-root.base.ts @@ -0,0 +1,44 @@ +import { DomainEvent } from '@/domain/events/domain-event.base'; + +/** + * 聚合根基类 + * + * 所有聚合根都应继承此基类,统一管理领域事件的收集和清理。 + */ +export abstract class AggregateRoot { + protected readonly _domainEvents: DomainEvent[] = []; + + /** + * 聚合根唯一标识 + */ + abstract get id(): TId | undefined; + + /** + * 获取所有待发布的领域事件 + */ + get domainEvents(): ReadonlyArray { + return [...this._domainEvents]; + } + + /** + * 添加领域事件 + * @param event 领域事件 + */ + protected addDomainEvent(event: DomainEvent): void { + this._domainEvents.push(event); + } + + /** + * 清空领域事件(在事件发布后调用) + */ + clearDomainEvents(): void { + this._domainEvents.length = 0; + } + + /** + * 检查是否有待发布的领域事件 + */ + hasDomainEvents(): boolean { + return this._domainEvents.length > 0; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts new file mode 100644 index 00000000..1d5ef625 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts @@ -0,0 +1,212 @@ +import { AggregateRoot } from '../aggregate-root.base'; +import { DepositDetectedEvent, DepositConfirmedEvent } from '@/domain/events'; +import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +export interface DepositTransactionProps { + id?: bigint; + chainType: ChainType; + txHash: TxHash; + fromAddress: EvmAddress; + toAddress: EvmAddress; + tokenContract: EvmAddress; + amount: TokenAmount; + blockNumber: BlockNumber; + blockTimestamp: Date; + logIndex: number; + confirmations: number; + status: DepositStatus; + addressId: bigint; + accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) + userId: bigint; // 保留兼容 + notifiedAt?: Date; + notifyAttempts: number; + lastNotifyError?: string; + createdAt?: Date; + updatedAt?: Date; +} + +export class DepositTransaction extends AggregateRoot { + private props: DepositTransactionProps; + + private constructor(props: DepositTransactionProps) { + super(); + this.props = props; + } + + // Getters + get id(): bigint | undefined { + return this.props.id; + } + get chainType(): ChainType { + return this.props.chainType; + } + get txHash(): TxHash { + return this.props.txHash; + } + get fromAddress(): EvmAddress { + return this.props.fromAddress; + } + get toAddress(): EvmAddress { + return this.props.toAddress; + } + get tokenContract(): EvmAddress { + return this.props.tokenContract; + } + get amount(): TokenAmount { + return this.props.amount; + } + get blockNumber(): BlockNumber { + return this.props.blockNumber; + } + get blockTimestamp(): Date { + return this.props.blockTimestamp; + } + get logIndex(): number { + return this.props.logIndex; + } + get confirmations(): number { + return this.props.confirmations; + } + get status(): DepositStatus { + return this.props.status; + } + get addressId(): bigint { + return this.props.addressId; + } + get accountSequence(): string { + return this.props.accountSequence; + } + get userId(): bigint { + return this.props.userId; + } + get notifiedAt(): Date | undefined { + return this.props.notifiedAt; + } + get notifyAttempts(): number { + return this.props.notifyAttempts; + } + get lastNotifyError(): string | undefined { + return this.props.lastNotifyError; + } + get createdAt(): Date | undefined { + return this.props.createdAt; + } + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + get isConfirmed(): boolean { + return this.props.status === DepositStatus.CONFIRMED; + } + get isNotified(): boolean { + return this.props.status === DepositStatus.NOTIFIED; + } + + /** + * 创建新的充值交易(检测到时) + */ + static create(params: { + chainType: ChainType; + txHash: TxHash; + fromAddress: EvmAddress; + toAddress: EvmAddress; + tokenContract: EvmAddress; + amount: TokenAmount; + blockNumber: BlockNumber; + blockTimestamp: Date; + logIndex: number; + addressId: bigint; + accountSequence: string; + userId: bigint; + }): DepositTransaction { + const deposit = new DepositTransaction({ + ...params, + confirmations: 0, + status: DepositStatus.DETECTED, + notifyAttempts: 0, + }); + + deposit.addDomainEvent( + new DepositDetectedEvent({ + depositId: '0', // Will be set after persistence + chainType: params.chainType.toString(), + txHash: params.txHash.toString(), + fromAddress: params.fromAddress.toString(), + toAddress: params.toAddress.toString(), + tokenContract: params.tokenContract.toString(), + amount: params.amount.raw.toString(), + amountFormatted: params.amount.toFixed(8), + blockNumber: params.blockNumber.toString(), + blockTimestamp: params.blockTimestamp.toISOString(), + accountSequence: params.accountSequence, + userId: params.userId.toString(), + }), + ); + + return deposit; + } + + /** + * 从持久化数据重建 + */ + static reconstitute(props: DepositTransactionProps): DepositTransaction { + return new DepositTransaction(props); + } + + /** + * 更新确认数 + */ + updateConfirmations(currentBlockNumber: BlockNumber, requiredConfirmations: number): void { + const confirmations = Number(currentBlockNumber.diff(this.props.blockNumber)); + this.props.confirmations = Math.max(0, confirmations); + + // 检查是否达到确认要求(状态为 DETECTED 或 CONFIRMING 都可以确认) + if ( + this.props.confirmations >= requiredConfirmations && + (this.props.status === DepositStatus.DETECTED || this.props.status === DepositStatus.CONFIRMING) + ) { + this.confirm(); + } else if (this.props.status === DepositStatus.DETECTED) { + // 首次检测但确认数不够,标记为确认中 + this.props.status = DepositStatus.CONFIRMING; + } + } + + /** + * 确认充值 + */ + private confirm(): void { + this.props.status = DepositStatus.CONFIRMED; + + this.addDomainEvent( + new DepositConfirmedEvent({ + depositId: this.props.id?.toString() ?? '0', + chainType: this.props.chainType.toString(), + txHash: this.props.txHash.toString(), + toAddress: this.props.toAddress.toString(), + amount: this.props.amount.raw.toString(), + amountFormatted: this.props.amount.toFixed(8), + confirmations: this.props.confirmations, + accountSequence: this.props.accountSequence, + userId: this.props.userId.toString(), + }), + ); + } + + /** + * 标记为已通知 + */ + markAsNotified(): void { + this.props.status = DepositStatus.NOTIFIED; + this.props.notifiedAt = new Date(); + } + + /** + * 记录通知失败 + */ + recordNotifyFailure(error: string): void { + this.props.notifyAttempts += 1; + this.props.lastNotifyError = error; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/index.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/index.ts new file mode 100644 index 00000000..0c6b7caf --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/deposit-transaction/index.ts @@ -0,0 +1 @@ +export * from './deposit-transaction.aggregate'; diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/index.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/index.ts new file mode 100644 index 00000000..635b6896 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/index.ts @@ -0,0 +1,4 @@ +export * from './aggregate-root.base'; +export * from './deposit-transaction'; +export * from './monitored-address'; +export * from './transaction-request'; diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/index.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/index.ts new file mode 100644 index 00000000..88433646 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/index.ts @@ -0,0 +1 @@ +export * from './monitored-address.aggregate'; diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts new file mode 100644 index 00000000..d79c3406 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts @@ -0,0 +1,84 @@ +import { AggregateRoot } from '../aggregate-root.base'; +import { ChainType, EvmAddress } from '@/domain/value-objects'; + +export interface MonitoredAddressProps { + id?: bigint; + chainType: ChainType; + address: EvmAddress; + accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) + userId: bigint; // 保留兼容 + isActive: boolean; + createdAt?: Date; + updatedAt?: Date; +} + +export class MonitoredAddress extends AggregateRoot { + private props: MonitoredAddressProps; + + private constructor(props: MonitoredAddressProps) { + super(); + this.props = props; + } + + // Getters + get id(): bigint | undefined { + return this.props.id; + } + get chainType(): ChainType { + return this.props.chainType; + } + get address(): EvmAddress { + return this.props.address; + } + get accountSequence(): string { + return this.props.accountSequence; + } + get userId(): bigint { + return this.props.userId; + } + get isActive(): boolean { + return this.props.isActive; + } + get createdAt(): Date | undefined { + return this.props.createdAt; + } + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + /** + * 创建新的监控地址 + */ + static create(params: { + chainType: ChainType; + address: EvmAddress; + accountSequence: string; + userId: bigint; + }): MonitoredAddress { + return new MonitoredAddress({ + ...params, + isActive: true, + }); + } + + /** + * 从持久化数据重建 + */ + static reconstitute(props: MonitoredAddressProps): MonitoredAddress { + return new MonitoredAddress(props); + } + + /** + * 激活地址监控 + */ + activate(): void { + this.props.isActive = true; + } + + /** + * 停用地址监控 + */ + deactivate(): void { + this.props.isActive = false; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/index.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/index.ts new file mode 100644 index 00000000..a415cff7 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/index.ts @@ -0,0 +1 @@ +export * from './transaction-request.aggregate'; diff --git a/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/transaction-request.aggregate.ts b/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/transaction-request.aggregate.ts new file mode 100644 index 00000000..a5fae62c --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/aggregates/transaction-request/transaction-request.aggregate.ts @@ -0,0 +1,185 @@ +import { AggregateRoot } from '../aggregate-root.base'; +import { TransactionBroadcastedEvent } from '@/domain/events'; +import { ChainType, TxHash, EvmAddress, TokenAmount } from '@/domain/value-objects'; +import { TransactionStatus } from '@/domain/enums'; +import { Decimal } from '@prisma/client/runtime/library'; + +export interface TransactionRequestProps { + id?: bigint; + chainType: ChainType; + sourceService: string; + sourceOrderId: string; + fromAddress: EvmAddress; + toAddress: EvmAddress; + value: TokenAmount; + data?: string; + signedTx?: string; + txHash?: TxHash; + status: TransactionStatus; + gasLimit?: bigint; + gasPrice?: Decimal; + nonce?: number; + errorMessage?: string; + retryCount: number; + createdAt?: Date; + updatedAt?: Date; +} + +export class TransactionRequest extends AggregateRoot { + private props: TransactionRequestProps; + + private constructor(props: TransactionRequestProps) { + super(); + this.props = props; + } + + // Getters + get id(): bigint | undefined { + return this.props.id; + } + get chainType(): ChainType { + return this.props.chainType; + } + get sourceService(): string { + return this.props.sourceService; + } + get sourceOrderId(): string { + return this.props.sourceOrderId; + } + get fromAddress(): EvmAddress { + return this.props.fromAddress; + } + get toAddress(): EvmAddress { + return this.props.toAddress; + } + get value(): TokenAmount { + return this.props.value; + } + get data(): string | undefined { + return this.props.data; + } + get signedTx(): string | undefined { + return this.props.signedTx; + } + get txHash(): TxHash | undefined { + return this.props.txHash; + } + get status(): TransactionStatus { + return this.props.status; + } + get gasLimit(): bigint | undefined { + return this.props.gasLimit; + } + get gasPrice(): Decimal | undefined { + return this.props.gasPrice; + } + get nonce(): number | undefined { + return this.props.nonce; + } + get errorMessage(): string | undefined { + return this.props.errorMessage; + } + get retryCount(): number { + return this.props.retryCount; + } + get createdAt(): Date | undefined { + return this.props.createdAt; + } + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + get isPending(): boolean { + return this.props.status === TransactionStatus.PENDING; + } + get isBroadcasted(): boolean { + return this.props.status === TransactionStatus.BROADCASTED; + } + get isFailed(): boolean { + return this.props.status === TransactionStatus.FAILED; + } + + /** + * 创建新的交易请求 + */ + static create(params: { + chainType: ChainType; + sourceService: string; + sourceOrderId: string; + fromAddress: EvmAddress; + toAddress: EvmAddress; + value: TokenAmount; + data?: string; + }): TransactionRequest { + return new TransactionRequest({ + ...params, + status: TransactionStatus.PENDING, + retryCount: 0, + }); + } + + /** + * 从持久化数据重建 + */ + static reconstitute(props: TransactionRequestProps): TransactionRequest { + return new TransactionRequest(props); + } + + /** + * 设置签名交易 + */ + setSignedTransaction(signedTx: string, gasLimit: bigint, gasPrice: Decimal, nonce: number): void { + this.props.signedTx = signedTx; + this.props.gasLimit = gasLimit; + this.props.gasPrice = gasPrice; + this.props.nonce = nonce; + this.props.status = TransactionStatus.SIGNED; + } + + /** + * 标记为已广播 + */ + markAsBroadcasted(txHash: TxHash): void { + this.props.txHash = txHash; + this.props.status = TransactionStatus.BROADCASTED; + + this.addDomainEvent( + new TransactionBroadcastedEvent({ + requestId: this.props.id?.toString() ?? '0', + chainType: this.props.chainType.toString(), + txHash: txHash.toString(), + fromAddress: this.props.fromAddress.toString(), + toAddress: this.props.toAddress.toString(), + value: this.props.value.raw.toString(), + sourceService: this.props.sourceService, + sourceOrderId: this.props.sourceOrderId, + }), + ); + } + + /** + * 标记为已确认 + */ + markAsConfirmed(): void { + this.props.status = TransactionStatus.CONFIRMED; + } + + /** + * 标记为失败 + */ + markAsFailed(errorMessage: string): void { + this.props.status = TransactionStatus.FAILED; + this.props.errorMessage = errorMessage; + this.props.retryCount += 1; + } + + /** + * 重试 + */ + retry(): void { + this.props.status = TransactionStatus.PENDING; + this.props.errorMessage = undefined; + this.props.signedTx = undefined; + this.props.txHash = undefined; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/domain.module.ts b/backend/services/mining-blockchain-service/src/domain/domain.module.ts new file mode 100644 index 00000000..758adab7 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/domain.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ConfirmationPolicyService, ChainConfigService } from './services'; +import { Erc20TransferService } from './services/erc20-transfer.service'; + +@Module({ + providers: [ConfirmationPolicyService, ChainConfigService, Erc20TransferService], + exports: [ConfirmationPolicyService, ChainConfigService, Erc20TransferService], +}) +export class DomainModule {} diff --git a/backend/services/mining-blockchain-service/src/domain/enums/chain-type.enum.ts b/backend/services/mining-blockchain-service/src/domain/enums/chain-type.enum.ts new file mode 100644 index 00000000..13a9c4ef --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/chain-type.enum.ts @@ -0,0 +1,8 @@ +/** + * 支持的区块链类型 + */ +export enum ChainTypeEnum { + KAVA = 'KAVA', + DST = 'DST', + BSC = 'BSC', +} diff --git a/backend/services/mining-blockchain-service/src/domain/enums/deposit-status.enum.ts b/backend/services/mining-blockchain-service/src/domain/enums/deposit-status.enum.ts new file mode 100644 index 00000000..67a30273 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/deposit-status.enum.ts @@ -0,0 +1,13 @@ +/** + * 充值交易状态 + */ +export enum DepositStatus { + /** 已检测到 */ + DETECTED = 'DETECTED', + /** 确认中 */ + CONFIRMING = 'CONFIRMING', + /** 已确认 */ + CONFIRMED = 'CONFIRMED', + /** 已通知 */ + NOTIFIED = 'NOTIFIED', +} diff --git a/backend/services/mining-blockchain-service/src/domain/enums/index.ts b/backend/services/mining-blockchain-service/src/domain/enums/index.ts new file mode 100644 index 00000000..1248ea7d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/index.ts @@ -0,0 +1,3 @@ +export * from './chain-type.enum'; +export * from './deposit-status.enum'; +export * from './transaction-status.enum'; diff --git a/backend/services/mining-blockchain-service/src/domain/enums/transaction-status.enum.ts b/backend/services/mining-blockchain-service/src/domain/enums/transaction-status.enum.ts new file mode 100644 index 00000000..e6ef6a87 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/transaction-status.enum.ts @@ -0,0 +1,15 @@ +/** + * 交易请求状态 + */ +export enum TransactionStatus { + /** 待处理 */ + PENDING = 'PENDING', + /** 已签名 */ + SIGNED = 'SIGNED', + /** 已广播 */ + BROADCASTED = 'BROADCASTED', + /** 已确认 */ + CONFIRMED = 'CONFIRMED', + /** 失败 */ + FAILED = 'FAILED', +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/deposit-confirmed.event.ts b/backend/services/mining-blockchain-service/src/domain/events/deposit-confirmed.event.ts new file mode 100644 index 00000000..b3b44b12 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/deposit-confirmed.event.ts @@ -0,0 +1,30 @@ +import { DomainEvent } from './domain-event.base'; + +export interface DepositConfirmedPayload { + depositId: string; + chainType: string; + txHash: string; + toAddress: string; + amount: string; + amountFormatted: string; + confirmations: number; + accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) + userId: string; // 保留兼容 + [key: string]: unknown; +} + +/** + * 充值确认事件 + * 当充值交易达到确认数时触发 + */ +export class DepositConfirmedEvent extends DomainEvent { + readonly eventType = 'blockchain.deposit.confirmed'; + + constructor(private readonly payload: DepositConfirmedPayload) { + super(); + } + + toPayload(): DepositConfirmedPayload { + return this.payload; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/deposit-detected.event.ts b/backend/services/mining-blockchain-service/src/domain/events/deposit-detected.event.ts new file mode 100644 index 00000000..0a7b8e2b --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/deposit-detected.event.ts @@ -0,0 +1,33 @@ +import { DomainEvent } from './domain-event.base'; + +export interface DepositDetectedPayload { + depositId: string; + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + tokenContract: string; + amount: string; + amountFormatted: string; + blockNumber: string; + blockTimestamp: string; + accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) + userId: string; // 保留兼容 + [key: string]: unknown; +} + +/** + * 充值检测事件 + * 当检测到新的充值交易时触发 + */ +export class DepositDetectedEvent extends DomainEvent { + readonly eventType = 'blockchain.deposit.detected'; + + constructor(private readonly payload: DepositDetectedPayload) { + super(); + } + + toPayload(): DepositDetectedPayload { + return this.payload; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/domain-event.base.ts b/backend/services/mining-blockchain-service/src/domain/events/domain-event.base.ts new file mode 100644 index 00000000..acc4dcc6 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/domain-event.base.ts @@ -0,0 +1,17 @@ +import { v4 as uuidv4 } from 'uuid'; + +/** + * 领域事件基类 + */ +export abstract class DomainEvent { + readonly eventId: string; + readonly occurredAt: Date; + abstract readonly eventType: string; + + constructor() { + this.eventId = uuidv4(); + this.occurredAt = new Date(); + } + + abstract toPayload(): Record; +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/index.ts b/backend/services/mining-blockchain-service/src/domain/events/index.ts new file mode 100644 index 00000000..b69c1096 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/index.ts @@ -0,0 +1,5 @@ +export * from './domain-event.base'; +export * from './deposit-detected.event'; +export * from './deposit-confirmed.event'; +export * from './wallet-address-created.event'; +export * from './transaction-broadcasted.event'; diff --git a/backend/services/mining-blockchain-service/src/domain/events/transaction-broadcasted.event.ts b/backend/services/mining-blockchain-service/src/domain/events/transaction-broadcasted.event.ts new file mode 100644 index 00000000..72d0e101 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/transaction-broadcasted.event.ts @@ -0,0 +1,29 @@ +import { DomainEvent } from './domain-event.base'; + +export interface TransactionBroadcastedPayload { + requestId: string; + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + value: string; + sourceService: string; + sourceOrderId: string; + [key: string]: unknown; +} + +/** + * 交易广播事件 + * 当交易成功广播到链上时触发 + */ +export class TransactionBroadcastedEvent extends DomainEvent { + readonly eventType = 'blockchain.transaction.broadcasted'; + + constructor(private readonly payload: TransactionBroadcastedPayload) { + super(); + } + + toPayload(): TransactionBroadcastedPayload { + return this.payload; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/wallet-address-created.event.ts b/backend/services/mining-blockchain-service/src/domain/events/wallet-address-created.event.ts new file mode 100644 index 00000000..8157a613 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/wallet-address-created.event.ts @@ -0,0 +1,32 @@ +import { DomainEvent } from './domain-event.base'; + +export interface WalletAddressCreatedPayload { + userId: string; + accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号) + publicKey: string; + addresses: { + chainType: string; + address: string; + }[]; + // 恢复助记词相关 + mnemonic?: string; // 12词助记词 (明文) + encryptedMnemonic?: string; // 加密的助记词 + mnemonicHash?: string; // 助记词哈希 + [key: string]: unknown; +} + +/** + * 钱包地址创建事件 + * 当从公钥派生出钱包地址后触发 + */ +export class WalletAddressCreatedEvent extends DomainEvent { + readonly eventType = 'blockchain.wallet.address.created'; + + constructor(private readonly payload: WalletAddressCreatedPayload) { + super(); + } + + toPayload(): WalletAddressCreatedPayload { + return this.payload; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/block-checkpoint.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/block-checkpoint.repository.interface.ts new file mode 100644 index 00000000..f27e0539 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/block-checkpoint.repository.interface.ts @@ -0,0 +1,43 @@ +import { ChainType, BlockNumber } from '@/domain/value-objects'; + +export const BLOCK_CHECKPOINT_REPOSITORY = Symbol('BLOCK_CHECKPOINT_REPOSITORY'); + +export interface BlockCheckpointData { + chainType: string; + lastScannedBlock: bigint; + lastScannedAt: Date; + isHealthy: boolean; + lastError?: string; +} + +export interface IBlockCheckpointRepository { + /** + * 获取最后扫描的区块 + */ + getLastScannedBlock(chainType: ChainType): Promise; + + /** + * 更新扫描进度 + */ + updateCheckpoint(chainType: ChainType, blockNumber: BlockNumber): Promise; + + /** + * 记录扫描错误 + */ + recordError(chainType: ChainType, error: string): Promise; + + /** + * 标记为健康 + */ + markHealthy(chainType: ChainType): Promise; + + /** + * 获取检查点状态 + */ + getCheckpoint(chainType: ChainType): Promise; + + /** + * 初始化检查点(如果不存在) + */ + initializeIfNotExists(chainType: ChainType, startBlock: BlockNumber): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/deposit-transaction.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/deposit-transaction.repository.interface.ts new file mode 100644 index 00000000..bb8e083d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/deposit-transaction.repository.interface.ts @@ -0,0 +1,47 @@ +import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +export const DEPOSIT_TRANSACTION_REPOSITORY = Symbol('DEPOSIT_TRANSACTION_REPOSITORY'); + +export interface IDepositTransactionRepository { + /** + * 保存充值交易 + */ + save(deposit: DepositTransaction): Promise; + + /** + * 根据ID查找 + */ + findById(id: bigint): Promise; + + /** + * 根据交易哈希查找 + */ + findByTxHash(txHash: TxHash): Promise; + + /** + * 根据状态查找 + */ + findByStatus(chainType: ChainType, status: DepositStatus): Promise; + + /** + * 查找待确认的充值 + */ + findPendingConfirmation(chainType: ChainType): Promise; + + /** + * 查找待通知的充值 + */ + findPendingNotification(): Promise; + + /** + * 根据用户ID查找 + */ + findByUserId(userId: bigint, limit?: number): Promise; + + /** + * 检查交易是否存在 + */ + existsByTxHash(txHash: TxHash): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/index.ts b/backend/services/mining-blockchain-service/src/domain/repositories/index.ts new file mode 100644 index 00000000..17e87bdc --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/index.ts @@ -0,0 +1,5 @@ +export * from './deposit-transaction.repository.interface'; +export * from './monitored-address.repository.interface'; +export * from './block-checkpoint.repository.interface'; +export * from './transaction-request.repository.interface'; +export * from './outbox-event.repository.interface'; diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/monitored-address.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/monitored-address.repository.interface.ts new file mode 100644 index 00000000..bbf76932 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/monitored-address.repository.interface.ts @@ -0,0 +1,44 @@ +import { MonitoredAddress } from '@/domain/aggregates/monitored-address'; +import { ChainType, EvmAddress } from '@/domain/value-objects'; + +export const MONITORED_ADDRESS_REPOSITORY = Symbol('MONITORED_ADDRESS_REPOSITORY'); + +export interface IMonitoredAddressRepository { + /** + * 保存监控地址 + */ + save(address: MonitoredAddress): Promise; + + /** + * 根据ID查找 + */ + findById(id: bigint): Promise; + + /** + * 根据链类型和地址查找 + */ + findByChainAndAddress( + chainType: ChainType, + address: EvmAddress, + ): Promise; + + /** + * 查找链上所有活跃地址 + */ + findActiveByChain(chainType: ChainType): Promise; + + /** + * 根据用户ID查找 + */ + findByUserId(userId: bigint): Promise; + + /** + * 检查地址是否已存在 + */ + existsByChainAndAddress(chainType: ChainType, address: EvmAddress): Promise; + + /** + * 获取所有活跃地址(用于构建地址集合) + */ + getAllActiveAddresses(chainType: ChainType): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/outbox-event.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/outbox-event.repository.interface.ts new file mode 100644 index 00000000..0494ae5e --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/outbox-event.repository.interface.ts @@ -0,0 +1,99 @@ +/** + * Outbox Event Repository Interface + * + * 发件箱模式 - 保证事件发布的可靠性 + */ + +export const OUTBOX_EVENT_REPOSITORY = Symbol('OUTBOX_EVENT_REPOSITORY'); + +export enum OutboxEventStatus { + PENDING = 'PENDING', // 待发送 + SENT = 'SENT', // 已发送,等待确认 + ACKED = 'ACKED', // 已确认 + FAILED = 'FAILED', // 发送失败(超过最大重试次数) +} + +export interface OutboxEventData { + eventType: string; + aggregateId: string; + aggregateType: string; + payload: Record; +} + +export interface OutboxEvent { + id: bigint; + eventType: string; + aggregateId: string; + aggregateType: string; + payload: Record; + status: OutboxEventStatus; + retryCount: number; + maxRetries: number; + lastError: string | null; + nextRetryAt: Date | null; + createdAt: Date; + sentAt: Date | null; + ackedAt: Date | null; +} + +export interface IOutboxEventRepository { + /** + * 创建新的 outbox 事件 + */ + create(data: OutboxEventData): Promise; + + /** + * 批量创建 outbox 事件 + */ + createMany(data: OutboxEventData[]): Promise; + + /** + * 根据 ID 查找 + */ + findById(id: bigint): Promise; + + /** + * 查找待发送的事件(PENDING 状态且到达重试时间) + */ + findPendingEvents(limit?: number): Promise; + + /** + * 查找已发送但未确认的事件(SENT 状态且超时) + */ + findUnackedEvents(timeoutSeconds: number, limit?: number): Promise; + + /** + * 标记为已发送 + */ + markAsSent(id: bigint): Promise; + + /** + * 标记为已确认 + */ + markAsAcked(id: bigint): Promise; + + /** + * 根据聚合ID标记为已确认(用于接收 ACK 时) + */ + markAsAckedByAggregateId(aggregateType: string, aggregateId: string, eventType: string): Promise; + + /** + * 记录发送失败,增加重试计数 + */ + recordFailure(id: bigint, error: string): Promise; + + /** + * 标记为最终失败(超过最大重试次数) + */ + markAsFailed(id: bigint, error: string): Promise; + + /** + * 重置为待发送状态(用于手动重试) + */ + resetToPending(id: bigint): Promise; + + /** + * 清理已确认的旧事件 + */ + cleanupAckedEvents(olderThanDays: number): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/transaction-request.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/transaction-request.repository.interface.ts new file mode 100644 index 00000000..36b30a49 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/transaction-request.repository.interface.ts @@ -0,0 +1,41 @@ +import { TransactionRequest } from '@/domain/aggregates/transaction-request'; +import { ChainType, TxHash } from '@/domain/value-objects'; + +export const TRANSACTION_REQUEST_REPOSITORY = Symbol('TRANSACTION_REQUEST_REPOSITORY'); + +export interface ITransactionRequestRepository { + /** + * 保存交易请求 + */ + save(request: TransactionRequest): Promise; + + /** + * 根据ID查找 + */ + findById(id: bigint): Promise; + + /** + * 根据交易哈希查找 + */ + findByTxHash(txHash: TxHash): Promise; + + /** + * 根据来源查找 + */ + findBySource(sourceService: string, sourceOrderId: string): Promise; + + /** + * 查找待处理的请求 + */ + findPending(chainType: ChainType): Promise; + + /** + * 查找已广播待确认的请求 + */ + findBroadcasted(chainType: ChainType): Promise; + + /** + * 查找失败可重试的请求 + */ + findRetryable(chainType: ChainType, maxRetries: number): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts b/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts new file mode 100644 index 00000000..3c303a81 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts @@ -0,0 +1,117 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ChainType } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface ChainConfig { + chainType: ChainTypeEnum; + chainId: number; + rpcUrl: string; + usdtContract: string; + nativeSymbol: string; + blockTime: number; // 平均出块时间(秒) + isTestnet: boolean; +} + +/** + * 链配置服务 + * + * 支持主网/测试网切换,通过 NETWORK_MODE 环境变量控制 + */ +@Injectable() +export class ChainConfigService { + private readonly logger = new Logger(ChainConfigService.name); + private readonly configs: Map; + private readonly isTestnet: boolean; + private readonly networkMode: string; + + constructor(private readonly configService: ConfigService) { + this.configs = new Map(); + this.networkMode = this.configService.get('blockchain.networkMode', 'mainnet'); + this.isTestnet = this.networkMode === 'testnet'; + this.initializeConfigs(); + this.logger.log(`[INIT] Network mode: ${this.networkMode} (testnet=${this.isTestnet})`); + } + + private initializeConfigs(): void { + // KAVA 配置 + this.configs.set(ChainTypeEnum.KAVA, { + chainType: ChainTypeEnum.KAVA, + chainId: this.configService.get('blockchain.kava.chainId', this.isTestnet ? 2221 : 2222), + rpcUrl: this.configService.get( + 'blockchain.kava.rpcUrl', + this.isTestnet ? 'https://evm.testnet.kava.io' : 'https://evm.kava.io', + ), + // dUSDT (绿积分) 合约地址 - Durian USDT, 精度6位 + usdtContract: this.configService.get( + 'blockchain.kava.usdtContract', + this.isTestnet ? '0x0000000000000000000000000000000000000000' : '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3', + ), + nativeSymbol: 'KAVA', + blockTime: 6, + isTestnet: this.isTestnet, + }); + this.logger.log(`[INIT] KAVA: chainId=${this.configs.get(ChainTypeEnum.KAVA)?.chainId}, rpc=${this.configs.get(ChainTypeEnum.KAVA)?.rpcUrl}`); + + // BSC 配置 + this.configs.set(ChainTypeEnum.BSC, { + chainType: ChainTypeEnum.BSC, + chainId: this.configService.get('blockchain.bsc.chainId', this.isTestnet ? 97 : 56), + rpcUrl: this.configService.get( + 'blockchain.bsc.rpcUrl', + this.isTestnet ? 'https://data-seed-prebsc-1-s1.binance.org:8545' : 'https://bsc-dataseed.binance.org', + ), + usdtContract: this.configService.get( + 'blockchain.bsc.usdtContract', + this.isTestnet ? '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd' : '0x55d398326f99059fF775485246999027B3197955', + ), + nativeSymbol: 'BNB', + blockTime: 3, + isTestnet: this.isTestnet, + }); + this.logger.log(`[INIT] BSC: chainId=${this.configs.get(ChainTypeEnum.BSC)?.chainId}, rpc=${this.configs.get(ChainTypeEnum.BSC)?.rpcUrl}`); + } + + /** + * 获取当前网络模式 + */ + getNetworkMode(): string { + return this.networkMode; + } + + /** + * 是否为测试网 + */ + isTestnetMode(): boolean { + return this.isTestnet; + } + + /** + * 获取链配置 + */ + getConfig(chainType: ChainType): ChainConfig { + const config = this.configs.get(chainType.value); + if (!config) { + throw new Error(`Unsupported chain type: ${chainType.toString()}`); + } + return config; + } + + /** + * 获取所有支持的链 + * + * 注意:暂时只支持 KAVA 链,BSC 链暂未启用 + */ + getSupportedChains(): ChainTypeEnum[] { + // TODO: BSC 链暂时禁用,未来需要时可以恢复 + // return Array.from(this.configs.keys()); + return [ChainTypeEnum.KAVA]; + } + + /** + * 检查链是否支持 + */ + isSupported(chainType: ChainType): boolean { + return this.configs.has(chainType.value); + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/services/confirmation-policy.service.ts b/backend/services/mining-blockchain-service/src/domain/services/confirmation-policy.service.ts new file mode 100644 index 00000000..6c081347 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/services/confirmation-policy.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ChainType } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +/** + * 确认策略服务 + * 定义各链的确认数要求 + */ +@Injectable() +export class ConfirmationPolicyService { + private readonly defaultConfirmations: number; + + constructor(private readonly configService: ConfigService) { + this.defaultConfirmations = this.configService.get( + 'blockchain.confirmationsRequired', + 12, + ); + } + + /** + * 获取所需确认数 + */ + getRequiredConfirmations(chainType: ChainType): number { + // 可以根据不同链配置不同的确认数 + switch (chainType.value) { + case ChainTypeEnum.KAVA: + return this.configService.get('blockchain.kava.confirmations', 12); + case ChainTypeEnum.BSC: + return this.configService.get('blockchain.bsc.confirmations', 15); + default: + return this.defaultConfirmations; + } + } + + /** + * 检查是否已确认 + */ + isConfirmed(chainType: ChainType, confirmations: number): boolean { + return confirmations >= this.getRequiredConfirmations(chainType); + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts new file mode 100644 index 00000000..51cd37ba --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts @@ -0,0 +1,327 @@ +import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + JsonRpcProvider, + Contract, + parseUnits, + formatUnits, + Transaction, + Signature, + recoverAddress, +} from 'ethers'; +import { ChainConfigService } from './chain-config.service'; +import { ChainType } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +// ERC20 ABI for transfer +const ERC20_TRANSFER_ABI = [ + 'function transfer(address to, uint256 amount) returns (bool)', + 'function balanceOf(address owner) view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function symbol() view returns (string)', +]; + +export interface TransferResult { + success: boolean; + txHash?: string; + error?: string; + gasUsed?: string; + blockNumber?: number; +} + +// MPC 签名客户端接口(避免循环依赖) +export interface IMpcSigningClient { + isConfigured(): boolean; + getHotWalletAddress(): string; + signMessage(messageHash: string): Promise; +} + +export const MPC_SIGNING_CLIENT = Symbol('MPC_SIGNING_CLIENT'); + +/** + * ERC20 转账服务 + * + * 用于从系统热钱包发送 ERC20 代币到用户指定地址 + * 使用 MPC 签名替代私钥签名,提高安全性 + */ +@Injectable() +export class Erc20TransferService { + private readonly logger = new Logger(Erc20TransferService.name); + private readonly providers: Map = new Map(); + private readonly hotWalletAddress: string; + private mpcSigningClient: IMpcSigningClient | null = null; + + constructor( + private readonly configService: ConfigService, + private readonly chainConfig: ChainConfigService, + ) { + this.hotWalletAddress = this.configService.get('HOT_WALLET_ADDRESS', ''); + this.initializeProviders(); + } + + /** + * 设置 MPC 签名客户端(用于延迟注入,避免循环依赖) + */ + setMpcSigningClient(client: IMpcSigningClient): void { + this.mpcSigningClient = client; + this.logger.log(`[INIT] MPC Signing Client injected`); + } + + private initializeProviders(): void { + // 为每条支持的链创建 Provider + for (const chainType of this.chainConfig.getSupportedChains()) { + try { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const provider = new JsonRpcProvider(config.rpcUrl, config.chainId); + this.providers.set(chainType, provider); + this.logger.log(`[INIT] Provider initialized for ${chainType}: ${config.rpcUrl}`); + } catch (error) { + this.logger.error(`[INIT] Failed to initialize provider for ${chainType}`, error); + } + } + + // 检查热钱包地址配置 + if (this.hotWalletAddress) { + this.logger.log(`[INIT] Hot wallet address configured: ${this.hotWalletAddress}`); + } else { + this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured, transfers will fail'); + } + } + + /** + * 获取热钱包地址 + */ + getHotWalletAddress(chainType: ChainTypeEnum): string | null { + // MPC 钱包地址在所有 EVM 链上相同 + return this.hotWalletAddress || null; + } + + /** + * 获取热钱包 USDT 余额 + */ + async getHotWalletBalance(chainType: ChainTypeEnum): Promise { + const provider = this.providers.get(chainType); + if (!provider) { + throw new Error(`Provider not configured for chain: ${chainType}`); + } + + if (!this.hotWalletAddress) { + throw new Error('Hot wallet address not configured'); + } + + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, provider); + + const balance = await contract.balanceOf(this.hotWalletAddress); + const decimals = await contract.decimals(); + + return formatUnits(balance, decimals); + } + + /** + * 执行 ERC20 转账(使用 MPC 签名) + * + * @param chainType 链类型 (KAVA, BSC) + * @param toAddress 接收地址 + * @param amount 转账金额 (人类可读格式,如 "100.5") + * @returns 转账结果 + */ + async transferUsdt( + chainType: ChainTypeEnum, + toAddress: string, + amount: string, + ): Promise { + this.logger.log(`[TRANSFER] Starting USDT transfer with MPC signing`); + this.logger.log(`[TRANSFER] Chain: ${chainType}`); + this.logger.log(`[TRANSFER] To: ${toAddress}`); + this.logger.log(`[TRANSFER] Amount: ${amount} USDT`); + + const provider = this.providers.get(chainType); + if (!provider) { + const error = `Provider not configured for chain: ${chainType}`; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.mpcSigningClient || !this.mpcSigningClient.isConfigured()) { + const error = 'MPC signing client not configured'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.hotWalletAddress) { + const error = 'Hot wallet address not configured'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + try { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, provider); + + // 获取代币精度 + const decimals = await contract.decimals(); + this.logger.log(`[TRANSFER] Token decimals: ${decimals}`); + + // 转换金额 + const amountInWei = parseUnits(amount, decimals); + this.logger.log(`[TRANSFER] Amount in wei: ${amountInWei.toString()}`); + + // 检查余额 + const balance = await contract.balanceOf(this.hotWalletAddress); + this.logger.log(`[TRANSFER] Hot wallet balance: ${formatUnits(balance, decimals)} USDT`); + + if (balance < amountInWei) { + const error = 'Insufficient USDT balance in hot wallet'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + // 构建交易 + this.logger.log(`[TRANSFER] Building transaction...`); + const nonce = await provider.getTransactionCount(this.hotWalletAddress); + const feeData = await provider.getFeeData(); + + // ERC20 transfer 的 calldata + const transferData = contract.interface.encodeFunctionData('transfer', [toAddress, amountInWei]); + + // 估算 gas + const gasEstimate = await provider.estimateGas({ + from: this.hotWalletAddress, + to: config.usdtContract, + data: transferData, + }); + + const gasLimit = gasEstimate * BigInt(120) / BigInt(100); // 增加 20% buffer + + // 检测链是否支持 EIP-1559 + // 如果 maxFeePerGas 为 null 或 0,则使用 legacy 交易 + const supportsEip1559 = feeData.maxFeePerGas && feeData.maxFeePerGas > BigInt(0); + this.logger.log(`[TRANSFER] Chain supports EIP-1559: ${supportsEip1559}`); + this.logger.log(`[TRANSFER] Fee data: gasPrice=${feeData.gasPrice}, maxFeePerGas=${feeData.maxFeePerGas}`); + + let tx: Transaction; + if (supportsEip1559) { + // EIP-1559 交易 (type 2) + tx = Transaction.from({ + type: 2, + chainId: config.chainId, + nonce, + to: config.usdtContract, + data: transferData, + gasLimit, + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + }); + this.logger.log(`[TRANSFER] Built EIP-1559 transaction`); + } else { + // Legacy 交易 (type 0) + const gasPrice = feeData.gasPrice || BigInt(1000000000); // 默认 1 gwei + tx = Transaction.from({ + type: 0, + chainId: config.chainId, + nonce, + to: config.usdtContract, + data: transferData, + gasLimit, + gasPrice, + }); + this.logger.log(`[TRANSFER] Built legacy transaction with gasPrice=${gasPrice}`); + } + + this.logger.log(`[TRANSFER] Transaction built: nonce=${nonce}, gasLimit=${tx.gasLimit}`); + + // 获取交易哈希用于签名 + const unsignedTxHash = tx.unsignedHash; + this.logger.log(`[TRANSFER] Unsigned tx hash: ${unsignedTxHash}`); + + // 使用 MPC 签名 + this.logger.log(`[TRANSFER] Requesting MPC signature...`); + const signatureHex = await this.mpcSigningClient.signMessage(unsignedTxHash); + this.logger.log(`[TRANSFER] MPC signature obtained: ${signatureHex.slice(0, 20)}...`); + + // 解析签名 - MPC 返回 64 字节 (r+s),需要转换为 ethers.js 格式 + // 确保有 0x 前缀 + const normalizedSig = signatureHex.startsWith('0x') ? signatureHex : `0x${signatureHex}`; + this.logger.log(`[TRANSFER] Normalized signature: ${normalizedSig.slice(0, 22)}...`); + + // MPC 签名是 64 字节 (r: 32 bytes + s: 32 bytes),需要添加 v (recovery id) + // 对于 EIP-1559 交易,v = 0 或 1 (yParity) + // 我们需要尝试两个值来恢复正确的地址 + const sigBytes = normalizedSig.slice(2); // 去掉 0x + const r = `0x${sigBytes.slice(0, 64)}`; + const s = `0x${sigBytes.slice(64, 128)}`; + + this.logger.log(`[TRANSFER] Signature r: ${r.slice(0, 20)}...`); + this.logger.log(`[TRANSFER] Signature s: ${s.slice(0, 20)}...`); + + // 尝试 yParity 0 和 1 来找到正确的 recovery id + let signature: Signature | null = null; + for (const yParity of [0, 1] as const) { + try { + const testSig = Signature.from({ r, s, yParity }); + // 使用 recoverAddress 验证签名 + const recoveredAddress = recoverAddress(unsignedTxHash, testSig); + this.logger.log(`[TRANSFER] Recovered address with yParity=${yParity}: ${recoveredAddress}`); + + if (recoveredAddress.toLowerCase() === this.hotWalletAddress.toLowerCase()) { + this.logger.log(`[TRANSFER] Found correct yParity: ${yParity}`); + signature = testSig; + break; + } + } catch (e) { + this.logger.debug(`[TRANSFER] yParity=${yParity} failed: ${e}`); + } + } + + if (!signature) { + throw new Error('Failed to recover correct signature - address mismatch'); + } + + // 创建已签名交易 + const signedTx = tx.clone(); + signedTx.signature = signature; + + // 广播交易 + this.logger.log(`[TRANSFER] Broadcasting transaction...`); + const txResponse = await provider.broadcastTransaction(signedTx.serialized); + this.logger.log(`[TRANSFER] Transaction sent: ${txResponse.hash}`); + + // 等待确认 + this.logger.log(`[TRANSFER] Waiting for confirmation...`); + const receipt = await txResponse.wait(); + + if (receipt && receipt.status === 1) { + this.logger.log(`[TRANSFER] Transaction confirmed!`); + this.logger.log(`[TRANSFER] Block: ${receipt.blockNumber}`); + this.logger.log(`[TRANSFER] Gas used: ${receipt.gasUsed.toString()}`); + + return { + success: true, + txHash: txResponse.hash, + gasUsed: receipt.gasUsed.toString(), + blockNumber: receipt.blockNumber, + }; + } else { + const error = 'Transaction failed (reverted)'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, txHash: txResponse.hash, error }; + } + } catch (error: any) { + this.logger.error(`[TRANSFER] Transfer failed:`, error); + return { + success: false, + error: error.message || 'Unknown error during transfer', + }; + } + } + + /** + * 检查热钱包是否已配置 + */ + isConfigured(chainType: ChainTypeEnum): boolean { + return this.providers.has(chainType) && + !!this.hotWalletAddress && + !!this.mpcSigningClient?.isConfigured(); + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/services/index.ts b/backend/services/mining-blockchain-service/src/domain/services/index.ts new file mode 100644 index 00000000..7bf6b5cd --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/services/index.ts @@ -0,0 +1,2 @@ +export * from './confirmation-policy.service'; +export * from './chain-config.service'; diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/block-number.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/block-number.vo.ts new file mode 100644 index 00000000..073df990 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/block-number.vo.ts @@ -0,0 +1,61 @@ +/** + * 区块号值对象 + */ +export class BlockNumber { + private readonly _value: bigint; + + private constructor(value: bigint) { + this._value = value; + } + + static create(value: bigint | number | string): BlockNumber { + const num = BigInt(value); + if (num < 0n) { + throw new Error(`Block number cannot be negative: ${value}`); + } + return new BlockNumber(num); + } + + get value(): bigint { + return this._value; + } + + get asNumber(): number { + return Number(this._value); + } + + equals(other: BlockNumber): boolean { + return this._value === other._value; + } + + isGreaterThan(other: BlockNumber): boolean { + return this._value > other._value; + } + + isLessThan(other: BlockNumber): boolean { + return this._value < other._value; + } + + add(blocks: number): BlockNumber { + return new BlockNumber(this._value + BigInt(blocks)); + } + + subtract(blocks: number): BlockNumber { + const result = this._value - BigInt(blocks); + if (result < 0n) { + throw new Error('Block number cannot be negative'); + } + return new BlockNumber(result); + } + + /** + * 计算与另一个区块的差距 + */ + diff(other: BlockNumber): bigint { + return this._value - other._value; + } + + toString(): string { + return this._value.toString(); + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/chain-type.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/chain-type.vo.ts new file mode 100644 index 00000000..7c6c8faa --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/chain-type.vo.ts @@ -0,0 +1,48 @@ +import { ChainTypeEnum } from '../enums'; + +/** + * 链类型值对象 + */ +export class ChainType { + private readonly _value: ChainTypeEnum; + + private constructor(value: ChainTypeEnum) { + this._value = value; + } + + static create(value: string): ChainType { + const normalized = value.toUpperCase() as ChainTypeEnum; + if (!Object.values(ChainTypeEnum).includes(normalized)) { + throw new Error(`Invalid chain type: ${value}`); + } + return new ChainType(normalized); + } + + static fromEnum(value: ChainTypeEnum): ChainType { + return new ChainType(value); + } + + static KAVA(): ChainType { + return new ChainType(ChainTypeEnum.KAVA); + } + + static BSC(): ChainType { + return new ChainType(ChainTypeEnum.BSC); + } + + get value(): ChainTypeEnum { + return this._value; + } + + equals(other: ChainType): boolean { + return this._value === other._value; + } + + toString(): string { + return this._value; + } + + isEVM(): boolean { + return [ChainTypeEnum.KAVA, ChainTypeEnum.BSC].includes(this._value); + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/cosmos-address.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/cosmos-address.vo.ts new file mode 100644 index 00000000..80ffb06f --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/cosmos-address.vo.ts @@ -0,0 +1,50 @@ +import { bech32 } from 'bech32'; + +/** + * Cosmos 地址值对象 (bech32 格式) + * 支持 kava1..., dst1... 等地址格式 + */ +export class CosmosAddress { + private readonly _value: string; + private readonly _prefix: string; + + private constructor(value: string, prefix: string) { + this._value = value; + this._prefix = prefix; + } + + static create(value: string): CosmosAddress { + try { + const decoded = bech32.decode(value); + return new CosmosAddress(value, decoded.prefix); + } catch { + throw new Error(`Invalid Cosmos address: ${value}`); + } + } + + static fromPrefixAndHash(prefix: string, hash20Bytes: Uint8Array): CosmosAddress { + const words = bech32.toWords(Buffer.from(hash20Bytes)); + const address = bech32.encode(prefix, words); + return new CosmosAddress(address, prefix); + } + + get value(): string { + return this._value; + } + + get prefix(): string { + return this._prefix; + } + + get lowercase(): string { + return this._value.toLowerCase(); + } + + equals(other: CosmosAddress): boolean { + return this._value.toLowerCase() === other._value.toLowerCase(); + } + + toString(): string { + return this._value; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/evm-address.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/evm-address.vo.ts new file mode 100644 index 00000000..bb5355e1 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/evm-address.vo.ts @@ -0,0 +1,41 @@ +import { getAddress, isAddress } from 'ethers'; + +/** + * EVM 地址值对象 + */ +export class EvmAddress { + private readonly _value: string; + + private constructor(value: string) { + this._value = value; + } + + static create(value: string): EvmAddress { + if (!isAddress(value)) { + throw new Error(`Invalid EVM address: ${value}`); + } + // 使用 checksum 格式 + const checksumAddress = getAddress(value); + return new EvmAddress(checksumAddress); + } + + static fromUnchecked(value: string): EvmAddress { + return new EvmAddress(value.toLowerCase()); + } + + get value(): string { + return this._value; + } + + get lowercase(): string { + return this._value.toLowerCase(); + } + + equals(other: EvmAddress): boolean { + return this._value.toLowerCase() === other._value.toLowerCase(); + } + + toString(): string { + return this._value; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/index.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/index.ts new file mode 100644 index 00000000..ac31c374 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/index.ts @@ -0,0 +1,6 @@ +export * from './chain-type.vo'; +export * from './evm-address.vo'; +export * from './cosmos-address.vo'; +export * from './tx-hash.vo'; +export * from './token-amount.vo'; +export * from './block-number.vo'; diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/token-amount.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/token-amount.vo.ts new file mode 100644 index 00000000..6070748d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/token-amount.vo.ts @@ -0,0 +1,99 @@ +import { Decimal } from '@prisma/client/runtime/library'; + +/** + * 代币金额值对象 + */ +export class TokenAmount { + private readonly _raw: bigint; + private readonly _decimals: number; + + private constructor(raw: bigint, decimals: number) { + this._raw = raw; + this._decimals = decimals; + } + + /** + * 从原始值创建(链上格式) + */ + static fromRaw(raw: bigint, decimals: number = 18): TokenAmount { + return new TokenAmount(raw, decimals); + } + + /** + * 从格式化值创建(人类可读格式) + */ + static fromFormatted(formatted: string, decimals: number = 18): TokenAmount { + const [intPart, decPart = ''] = formatted.split('.'); + const paddedDec = decPart.padEnd(decimals, '0').slice(0, decimals); + const raw = BigInt(intPart + paddedDec); + return new TokenAmount(raw, decimals); + } + + /** + * 从 Decimal 创建 + */ + static fromDecimal(decimal: Decimal, decimals: number = 18): TokenAmount { + const raw = BigInt(decimal.toFixed(0)); + return new TokenAmount(raw, decimals); + } + + get raw(): bigint { + return this._raw; + } + + get decimals(): number { + return this._decimals; + } + + /** + * 获取格式化值 + */ + get formatted(): string { + const rawStr = this._raw.toString().padStart(this._decimals + 1, '0'); + const intPart = rawStr.slice(0, -this._decimals) || '0'; + const decPart = rawStr.slice(-this._decimals); + // 移除尾随零 + const trimmedDec = decPart.replace(/0+$/, ''); + return trimmedDec ? `${intPart}.${trimmedDec}` : intPart; + } + + /** + * 获取固定小数位格式 + */ + toFixed(places: number = 8): string { + const rawStr = this._raw.toString().padStart(this._decimals + 1, '0'); + const intPart = rawStr.slice(0, -this._decimals) || '0'; + const decPart = rawStr.slice(-this._decimals).padEnd(places, '0').slice(0, places); + return `${intPart}.${decPart}`; + } + + /** + * 转为 Decimal(用于数据库存储) + */ + toDecimal(): Decimal { + return new Decimal(this._raw.toString()); + } + + /** + * 转为格式化 Decimal + */ + toFormattedDecimal(): Decimal { + return new Decimal(this.toFixed(8)); + } + + equals(other: TokenAmount): boolean { + return this._raw === other._raw && this._decimals === other._decimals; + } + + isZero(): boolean { + return this._raw === 0n; + } + + isPositive(): boolean { + return this._raw > 0n; + } + + toString(): string { + return this.formatted; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/value-objects/tx-hash.vo.ts b/backend/services/mining-blockchain-service/src/domain/value-objects/tx-hash.vo.ts new file mode 100644 index 00000000..555849a5 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/value-objects/tx-hash.vo.ts @@ -0,0 +1,42 @@ +/** + * 交易哈希值对象 + */ +export class TxHash { + private readonly _value: string; + + private constructor(value: string) { + this._value = value; + } + + static create(value: string): TxHash { + // 验证格式: 0x 开头,64位十六进制 + const normalized = value.toLowerCase(); + if (!/^0x[a-f0-9]{64}$/.test(normalized)) { + throw new Error(`Invalid transaction hash: ${value}`); + } + return new TxHash(normalized); + } + + static fromUnchecked(value: string): TxHash { + return new TxHash(value.toLowerCase()); + } + + get value(): string { + return this._value; + } + + equals(other: TxHash): boolean { + return this._value === other._value; + } + + toString(): string { + return this._value; + } + + /** + * 获取短格式(用于显示) + */ + toShort(): string { + return `${this._value.slice(0, 10)}...${this._value.slice(-8)}`; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts new file mode 100644 index 00000000..e673d0c6 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts @@ -0,0 +1,188 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { keccak256, getBytes, sha256, ripemd160 } from 'ethers'; +import { bech32 } from 'bech32'; +import { EvmAddress } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface DerivedAddress { + chainType: ChainTypeEnum; + address: string; +} + +/** + * 地址派生适配器 + * 从 MPC 公钥派生多链钱包地址 + */ +@Injectable() +export class AddressDerivationAdapter { + private readonly logger = new Logger(AddressDerivationAdapter.name); + + /** + * 从压缩公钥派生 EVM 地址 + * + * @param compressedPublicKey 压缩格式的公钥 (33 bytes, 0x02/0x03 开头) + * @returns EVM 地址 + */ + deriveEvmAddress(compressedPublicKey: string): string { + // 移除 0x 前缀 + const pubKeyHex = compressedPublicKey.replace('0x', ''); + + // 验证压缩公钥格式 + if (pubKeyHex.length !== 66) { + throw new Error(`Invalid compressed public key length: ${pubKeyHex.length}, expected 66`); + } + + const prefix = pubKeyHex.slice(0, 2); + if (prefix !== '02' && prefix !== '03') { + throw new Error(`Invalid compressed public key prefix: ${prefix}, expected 02 or 03`); + } + + // 解压缩公钥 + const uncompressedPubKey = this.decompressPublicKey(pubKeyHex); + + // 移除 04 前缀(非压缩公钥标识) + const pubKeyWithoutPrefix = uncompressedPubKey.slice(2); + + // Keccak256 哈希 + const hash = keccak256(getBytes('0x' + pubKeyWithoutPrefix)); + + // 取最后 20 bytes 作为地址 + const address = '0x' + hash.slice(-40); + + return address; + } + + /** + * 解压缩公钥 + * + * 使用 secp256k1 曲线解压缩 + */ + private decompressPublicKey(compressedPubKeyHex: string): string { + const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'); + + const prefix = parseInt(compressedPubKeyHex.slice(0, 2), 16); + const x = BigInt('0x' + compressedPubKeyHex.slice(2)); + + // y² = x³ + 7 (mod p) + const ySquared = (x ** 3n + 7n) % p; + + // 计算模平方根 + let y = this.modPow(ySquared, (p + 1n) / 4n, p); + + // 根据前缀选择 y 的奇偶性 + const isYOdd = y % 2n === 1n; + const shouldBeOdd = prefix === 0x03; + + if (isYOdd !== shouldBeOdd) { + y = p - y; + } + + // 返回非压缩格式 (04 + x + y) + const xHex = x.toString(16).padStart(64, '0'); + const yHex = y.toString(16).padStart(64, '0'); + + return '04' + xHex + yHex; + } + + /** + * 模幂运算 + */ + private modPow(base: bigint, exp: bigint, mod: bigint): bigint { + let result = 1n; + base = base % mod; + while (exp > 0n) { + if (exp % 2n === 1n) { + result = (result * base) % mod; + } + exp = exp / 2n; + base = (base * base) % mod; + } + return result; + } + + /** + * 从压缩公钥派生 Cosmos 地址 (bech32 格式) + * + * @param compressedPublicKey 压缩格式的公钥 (33 bytes, 0x02/0x03 开头) + * @param prefix bech32 地址前缀 (如 'kava', 'dst') + * @returns bech32 格式的地址 + */ + deriveCosmosAddress(compressedPublicKey: string, prefix: string): string { + // 移除 0x 前缀 + const pubKeyHex = compressedPublicKey.replace('0x', ''); + + // 验证压缩公钥格式 + if (pubKeyHex.length !== 66) { + throw new Error(`Invalid compressed public key length: ${pubKeyHex.length}, expected 66`); + } + + // SHA256 哈希 + const pubKeyBytes = getBytes('0x' + pubKeyHex); + const sha256Hash = sha256(pubKeyBytes); + + // RIPEMD160 哈希 (得到 20 bytes) + const ripemd160Hash = ripemd160(sha256Hash); + + // 转换为 5-bit words 用于 bech32 编码 + const hashBytes = getBytes(ripemd160Hash); + const words = bech32.toWords(Buffer.from(hashBytes)); + + // bech32 编码 + const address = bech32.encode(prefix, words); + + this.logger.debug(`Derived Cosmos address with prefix '${prefix}': ${address}`); + + return address; + } + + /** + * 从公钥派生所有支持链的地址 + */ + deriveAllAddresses(compressedPublicKey: string): DerivedAddress[] { + const addresses: DerivedAddress[] = []; + + this.logger.log(`[DERIVE] Starting address derivation for public key: ${compressedPublicKey.slice(0, 20)}...`); + + // EVM 链共用同一个地址 + const evmAddress = this.deriveEvmAddress(compressedPublicKey); + this.logger.log(`[DERIVE] EVM address derived: ${evmAddress}`); + + // KAVA (EVM 格式 - 0x...) - Kava EVM 兼容链 + addresses.push({ + chainType: ChainTypeEnum.KAVA, + address: evmAddress, + }); + this.logger.log(`[DERIVE] KAVA address (EVM): ${evmAddress}`); + + // DST (Cosmos bech32 格式 - dst1...) + const dstAddress = this.deriveCosmosAddress(compressedPublicKey, 'dst'); + addresses.push({ + chainType: ChainTypeEnum.DST, + address: dstAddress, + }); + this.logger.log(`[DERIVE] DST address (Cosmos): ${dstAddress}`); + + // BSC (EVM 格式 - 0x...) + addresses.push({ + chainType: ChainTypeEnum.BSC, + address: evmAddress, + }); + this.logger.log(`[DERIVE] BSC address (EVM): ${evmAddress}`); + + this.logger.log(`[DERIVE] Successfully derived ${addresses.length} addresses from public key`); + + return addresses; + } + + /** + * 验证地址格式 + */ + validateEvmAddress(address: string): boolean { + try { + EvmAddress.create(address); + return true; + } catch { + return false; + } + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/block-scanner.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/block-scanner.service.ts new file mode 100644 index 00000000..529faaaa --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/block-scanner.service.ts @@ -0,0 +1,124 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EvmProviderAdapter, TransferEvent } from './evm-provider.adapter'; +import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { ChainType, BlockNumber } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface DepositEvent extends TransferEvent { + chainType: ChainTypeEnum; +} + +export type DepositHandler = (deposits: DepositEvent[]) => Promise; + +/** + * 区块扫描服务 + * 定期扫描区块,检测充值交易 + */ +@Injectable() +export class BlockScannerService implements OnModuleInit { + private readonly logger = new Logger(BlockScannerService.name); + private readonly scanBatchSize: number; + private depositHandler?: DepositHandler; + private isScanning: Map = new Map(); + + constructor( + private readonly configService: ConfigService, + private readonly evmProvider: EvmProviderAdapter, + private readonly addressCache: AddressCacheService, + private readonly chainConfig: ChainConfigService, + ) { + this.scanBatchSize = this.configService.get('blockchain.scanBatchSize', 100); + } + + async onModuleInit() { + // 初始化扫描状态 + for (const chainType of this.chainConfig.getSupportedChains()) { + this.isScanning.set(chainType, false); + } + this.logger.log('BlockScannerService initialized'); + } + + /** + * 注册充值处理器 + */ + registerDepositHandler(handler: DepositHandler): void { + this.depositHandler = handler; + this.logger.log('Deposit handler registered'); + } + + /** + * 扫描指定链的区块 + */ + async scanChain( + chainType: ChainType, + fromBlock: BlockNumber, + toBlock: BlockNumber, + ): Promise { + const config = this.chainConfig.getConfig(chainType); + const deposits: DepositEvent[] = []; + + // 获取所有监控地址 + const monitoredAddresses = await this.addressCache.getAllAddresses(chainType); + const addressSet = new Set(monitoredAddresses.map((a) => a.toLowerCase())); + + if (addressSet.size === 0) { + this.logger.debug(`No monitored addresses for ${chainType}, skipping scan`); + return deposits; + } + + // 扫描 USDT Transfer 事件 + const events = await this.evmProvider.scanTransferEvents( + chainType, + fromBlock, + toBlock, + config.usdtContract, + ); + + // 过滤出充值到监控地址的交易 + for (const event of events) { + if (addressSet.has(event.to.toLowerCase())) { + deposits.push({ + ...event, + chainType: chainType.value, + }); + this.logger.log( + `Deposit detected: ${event.txHash} -> ${event.to} (${event.value.toString()})`, + ); + } + } + + return deposits; + } + + /** + * 执行单次扫描(供应用层调用) + */ + async executeScan( + chainType: ChainType, + lastScannedBlock: BlockNumber, + ): Promise<{ deposits: DepositEvent[]; newLastBlock: BlockNumber }> { + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + + // 计算扫描范围 + const fromBlock = lastScannedBlock.add(1); + let toBlock = fromBlock.add(this.scanBatchSize - 1); + + // 不超过当前区块 + if (toBlock.isGreaterThan(currentBlock)) { + toBlock = currentBlock; + } + + // 如果没有新区块,返回空 + if (fromBlock.isGreaterThan(currentBlock)) { + return { deposits: [], newLastBlock: lastScannedBlock }; + } + + this.logger.debug(`Scanning ${chainType}: blocks ${fromBlock} to ${toBlock}`); + + const deposits = await this.scanChain(chainType, fromBlock, toBlock); + + return { deposits, newLastBlock: toBlock }; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/evm-provider.adapter.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/evm-provider.adapter.ts new file mode 100644 index 00000000..4d247786 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/evm-provider.adapter.ts @@ -0,0 +1,198 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { JsonRpcProvider, Contract } from 'ethers'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { ChainType, BlockNumber, TokenAmount } from '@/domain/value-objects'; +import { ChainTypeEnum } from '@/domain/enums'; + +// ERC20 Transfer 事件 ABI +const ERC20_TRANSFER_EVENT_ABI = [ + 'event Transfer(address indexed from, address indexed to, uint256 value)', +]; + +// ERC20 balanceOf ABI +const ERC20_BALANCE_ABI = [ + 'function balanceOf(address owner) view returns (uint256)', + 'function decimals() view returns (uint8)', +]; + +export interface TransferEvent { + txHash: string; + logIndex: number; + blockNumber: bigint; + blockTimestamp: Date; + from: string; + to: string; + value: bigint; + tokenContract: string; +} + +/** + * EVM 区块链提供者适配器 + * 封装与 EVM 链的交互 + */ +@Injectable() +export class EvmProviderAdapter { + private readonly logger = new Logger(EvmProviderAdapter.name); + private readonly providers: Map = new Map(); + + constructor(private readonly chainConfig: ChainConfigService) { + this.initializeProviders(); + } + + private initializeProviders(): void { + for (const chainType of this.chainConfig.getSupportedChains()) { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const provider = new JsonRpcProvider(config.rpcUrl, config.chainId); + this.providers.set(chainType, provider); + this.logger.log(`Initialized provider for ${chainType}: ${config.rpcUrl}`); + } + } + + private getProvider(chainType: ChainType): JsonRpcProvider { + const provider = this.providers.get(chainType.value); + if (!provider) { + throw new Error(`No provider for chain: ${chainType.toString()}`); + } + return provider; + } + + /** + * 获取当前区块号 + */ + async getCurrentBlockNumber(chainType: ChainType): Promise { + const provider = this.getProvider(chainType); + const blockNumber = await provider.getBlockNumber(); + return BlockNumber.create(blockNumber); + } + + /** + * 获取区块时间戳 + */ + async getBlockTimestamp(chainType: ChainType, blockNumber: BlockNumber): Promise { + const provider = this.getProvider(chainType); + const block = await provider.getBlock(blockNumber.asNumber); + if (!block) { + throw new Error(`Block not found: ${blockNumber.toString()}`); + } + return new Date(block.timestamp * 1000); + } + + /** + * 扫描指定区块范围内的 ERC20 Transfer 事件 + */ + async scanTransferEvents( + chainType: ChainType, + fromBlock: BlockNumber, + toBlock: BlockNumber, + tokenContract: string, + ): Promise { + const provider = this.getProvider(chainType); + const contract = new Contract(tokenContract, ERC20_TRANSFER_EVENT_ABI, provider); + + const filter = contract.filters.Transfer(); + const logs = await contract.queryFilter(filter, fromBlock.asNumber, toBlock.asNumber); + + const events: TransferEvent[] = []; + + for (const log of logs) { + const block = await provider.getBlock(log.blockNumber); + if (!block) continue; + + const parsedLog = contract.interface.parseLog({ + topics: log.topics as string[], + data: log.data, + }); + + if (parsedLog) { + events.push({ + txHash: log.transactionHash, + logIndex: log.index, + blockNumber: BigInt(log.blockNumber), + blockTimestamp: new Date(block.timestamp * 1000), + from: parsedLog.args[0], + to: parsedLog.args[1], + value: parsedLog.args[2], + tokenContract, + }); + } + } + + return events; + } + + /** + * 查询 ERC20 代币余额 + */ + async getTokenBalance( + chainType: ChainType, + tokenContract: string, + address: string, + ): Promise { + const provider = this.getProvider(chainType); + const contract = new Contract(tokenContract, ERC20_BALANCE_ABI, provider); + const [balance, decimals] = await Promise.all([ + contract.balanceOf(address), + contract.decimals(), + ]); + return TokenAmount.fromRaw(balance, Number(decimals)); + } + + /** + * 查询 ERC20 代币的 decimals + */ + async getTokenDecimals(chainType: ChainType, tokenContract: string): Promise { + const provider = this.getProvider(chainType); + const contract = new Contract(tokenContract, ERC20_BALANCE_ABI, provider); + const decimals = await contract.decimals(); + return Number(decimals); + } + + /** + * 查询原生代币余额 + */ + async getNativeBalance(chainType: ChainType, address: string): Promise { + const provider = this.getProvider(chainType); + const balance = await provider.getBalance(address); + return TokenAmount.fromRaw(balance, 18); + } + + /** + * 广播签名交易 + */ + async broadcastTransaction(chainType: ChainType, signedTx: string): Promise { + const provider = this.getProvider(chainType); + const txResponse = await provider.broadcastTransaction(signedTx); + this.logger.log(`Transaction broadcasted: ${txResponse.hash}`); + return txResponse.hash; + } + + /** + * 等待交易确认 + */ + async waitForTransaction( + chainType: ChainType, + txHash: string, + confirmations: number = 1, + ): Promise { + const provider = this.getProvider(chainType); + const receipt = await provider.waitForTransaction(txHash, confirmations); + return receipt !== null && receipt.status === 1; + } + + /** + * 检查交易是否确认 + */ + async isTransactionConfirmed( + chainType: ChainType, + txHash: string, + requiredConfirmations: number, + ): Promise { + const provider = this.getProvider(chainType); + const receipt = await provider.getTransactionReceipt(txHash); + if (!receipt) return false; + + const currentBlock = await provider.getBlockNumber(); + const confirmations = currentBlock - receipt.blockNumber; + return confirmations >= requiredConfirmations; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/index.ts new file mode 100644 index 00000000..66fc41a9 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/index.ts @@ -0,0 +1,5 @@ +export * from './evm-provider.adapter'; +export * from './address-derivation.adapter'; +export * from './mnemonic-derivation.adapter'; +export * from './recovery-mnemonic.adapter'; +export * from './block-scanner.service'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/mnemonic-derivation.adapter.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/mnemonic-derivation.adapter.ts new file mode 100644 index 00000000..20462a79 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/mnemonic-derivation.adapter.ts @@ -0,0 +1,148 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HDKey } from '@scure/bip32'; +import { mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { createHash } from 'crypto'; +import { bech32 } from 'bech32'; +import { Wallet } from 'ethers'; +import { ChainTypeEnum } from '@/domain/enums'; + +export interface MnemonicDerivedAddress { + chainType: ChainTypeEnum; + address: string; +} + +/** + * 链配置 + */ +const CHAIN_CONFIG: Record = { + [ChainTypeEnum.KAVA]: { + derivationPath: "m/44'/459'/0'/0/0", + prefix: 'kava', + }, + [ChainTypeEnum.DST]: { + derivationPath: "m/44'/118'/0'/0/0", + prefix: 'dst', + }, + [ChainTypeEnum.BSC]: { + derivationPath: "m/44'/60'/0'/0/0", + }, +}; + +/** + * 助记词派生适配器 + * 从 BIP39 助记词派生多链钱包地址 + */ +@Injectable() +export class MnemonicDerivationAdapter { + private readonly logger = new Logger(MnemonicDerivationAdapter.name); + + /** + * 验证助记词格式 + */ + validateMnemonic(mnemonic: string): boolean { + return validateMnemonic(mnemonic, wordlist); + } + + /** + * 从助记词派生所有支持链的地址 + */ + deriveAllAddresses(mnemonic: string): MnemonicDerivedAddress[] { + if (!this.validateMnemonic(mnemonic)) { + throw new Error('Invalid mnemonic'); + } + + const seed = mnemonicToSeedSync(mnemonic); + const addresses: MnemonicDerivedAddress[] = []; + + this.logger.log('[DERIVE] Starting mnemonic address derivation'); + + // KAVA + const kavaAddress = this.deriveCosmosAddress(seed, CHAIN_CONFIG[ChainTypeEnum.KAVA]); + addresses.push({ chainType: ChainTypeEnum.KAVA, address: kavaAddress }); + this.logger.log(`[DERIVE] KAVA address: ${kavaAddress}`); + + // DST + const dstAddress = this.deriveCosmosAddress(seed, CHAIN_CONFIG[ChainTypeEnum.DST]); + addresses.push({ chainType: ChainTypeEnum.DST, address: dstAddress }); + this.logger.log(`[DERIVE] DST address: ${dstAddress}`); + + // BSC + const bscAddress = this.deriveEvmAddress(seed, CHAIN_CONFIG[ChainTypeEnum.BSC]); + addresses.push({ chainType: ChainTypeEnum.BSC, address: bscAddress }); + this.logger.log(`[DERIVE] BSC address: ${bscAddress}`); + + return addresses; + } + + /** + * 验证助记词是否对应指定的地址 + */ + verifyMnemonic( + mnemonic: string, + expectedAddresses: Array<{ chainType: string; address: string }>, + ): { valid: boolean; matchedAddresses: string[]; mismatchedAddresses: string[] } { + if (!this.validateMnemonic(mnemonic)) { + return { valid: false, matchedAddresses: [], mismatchedAddresses: [] }; + } + + const derivedAddresses = this.deriveAllAddresses(mnemonic); + const derivedMap = new Map(derivedAddresses.map((a) => [a.chainType, a.address.toLowerCase()])); + + const matchedAddresses: string[] = []; + const mismatchedAddresses: string[] = []; + + for (const expected of expectedAddresses) { + const chainType = expected.chainType as ChainTypeEnum; + const derivedAddress = derivedMap.get(chainType); + + if (derivedAddress && derivedAddress === expected.address.toLowerCase()) { + matchedAddresses.push(expected.chainType); + } else { + mismatchedAddresses.push(expected.chainType); + } + } + + return { + valid: mismatchedAddresses.length === 0 && matchedAddresses.length > 0, + matchedAddresses, + mismatchedAddresses, + }; + } + + /** + * 派生 Cosmos 地址 (bech32 格式) + */ + private deriveCosmosAddress( + seed: Uint8Array, + config: { derivationPath: string; prefix?: string }, + ): string { + const hdkey = HDKey.fromMasterSeed(seed); + const childKey = hdkey.derive(config.derivationPath); + + if (!childKey.publicKey) { + throw new Error('Failed to derive public key'); + } + + const hash = createHash('sha256').update(childKey.publicKey).digest(); + const addressHash = createHash('ripemd160').update(hash).digest(); + const words = bech32.toWords(addressHash); + + return bech32.encode(config.prefix!, words); + } + + /** + * 派生 EVM 地址 + */ + private deriveEvmAddress(seed: Uint8Array, config: { derivationPath: string }): string { + const hdkey = HDKey.fromMasterSeed(seed); + const childKey = hdkey.derive(config.derivationPath); + + if (!childKey.privateKey) { + throw new Error('Failed to derive private key'); + } + + const wallet = new Wallet(Buffer.from(childKey.privateKey).toString('hex')); + return wallet.address; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts new file mode 100644 index 00000000..49580269 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts @@ -0,0 +1,176 @@ +/** + * Recovery Mnemonic Adapter + * + * 生成与钱包公钥关联的恢复助记词 + * 支持挂失和更换功能 + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { validateMnemonic, entropyToMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; +import { ConfigService } from '@nestjs/config'; +import * as bcrypt from 'bcrypt'; + +export interface GenerateMnemonicParams { + userId: string; + publicKey: string; // 钱包公钥 (hex) +} + +export interface GenerateMnemonicResult { + mnemonic: string; // 12词助记词 (明文,仅首次返回) + encryptedMnemonic: string; // 加密的助记词 + mnemonicHash: string; // 助记词哈希 (用于验证) + publicKey: string; // 关联的公钥 +} + +export interface VerifyMnemonicResult { + valid: boolean; + message?: string; +} + +@Injectable() +export class RecoveryMnemonicAdapter { + private readonly logger = new Logger(RecoveryMnemonicAdapter.name); + private readonly encryptionKey: Buffer; + + constructor(private readonly configService: ConfigService) { + // 从环境变量获取加密密钥 + const key = this.configService.get('MNEMONIC_ENCRYPTION_KEY'); + if (key) { + this.encryptionKey = createHash('sha256').update(key).digest(); + } else { + this.logger.warn('MNEMONIC_ENCRYPTION_KEY not set, using development key'); + this.encryptionKey = createHash('sha256').update('dev-mnemonic-key-do-not-use-in-production').digest(); + } + } + + /** + * 生成与钱包公钥关联的恢复助记词 + * + * 生成逻辑: + * 1. 用公钥 + 随机数生成熵 + * 2. 从熵生成 12 词 BIP39 助记词 + * 3. 加密存储 + */ + async generateMnemonic(params: GenerateMnemonicParams): Promise { + const { userId, publicKey } = params; + this.logger.log(`Generating recovery mnemonic for user=${userId}, publicKey=${publicKey.slice(0, 16)}...`); + + // 生成随机熵 (128 bits = 16 bytes for 12 words) + const randomEntropy = randomBytes(16); + const publicKeyBytes = Buffer.from(publicKey.replace('0x', ''), 'hex'); + + // 混合熵: SHA256(randomEntropy + publicKey + timestamp) + const timestampBuffer = Buffer.alloc(8); + timestampBuffer.writeBigInt64BE(BigInt(Date.now())); + + const mixedEntropyFull = createHash('sha256') + .update(randomEntropy) + .update(publicKeyBytes) + .update(timestampBuffer) + .digest(); + + // 取前 16 bytes (128 bits) 作为 BIP39 熵 + const entropy = mixedEntropyFull.slice(0, 16); + + // 生成 12 词助记词 + const mnemonic = entropyToMnemonic(entropy, wordlist); + + if (!validateMnemonic(mnemonic, wordlist)) { + throw new Error('Generated mnemonic validation failed'); + } + + // 加密助记词 + const encryptedMnemonic = this.encryptMnemonic(mnemonic); + + // 计算助记词哈希 (用于验证,不可逆,使用 bcrypt) + const mnemonicHash = await this.hashMnemonic(mnemonic); + + this.logger.log(`Recovery mnemonic generated: hash=${mnemonicHash.slice(0, 16)}...`); + + return { + mnemonic, + encryptedMnemonic, + mnemonicHash, + publicKey, + }; + } + + /** + * 验证助记词是否正确 (异步,使用 bcrypt) + */ + async verifyMnemonic(mnemonic: string, expectedHash: string): Promise { + if (!validateMnemonic(mnemonic, wordlist)) { + return { valid: false, message: 'Invalid mnemonic format' }; + } + + // 兼容旧的 SHA256 hash 和新的 bcrypt hash + if (expectedHash.startsWith('$2')) { + // bcrypt hash + const isValid = await bcrypt.compare(mnemonic, expectedHash); + if (!isValid) { + return { valid: false, message: 'Mnemonic does not match' }; + } + } else { + // 旧的 SHA256 hash (兼容性) + const hash = this.hashMnemonicSha256(mnemonic); + if (hash !== expectedHash) { + return { valid: false, message: 'Mnemonic does not match' }; + } + } + + return { valid: true }; + } + + /** + * 解密助记词 + */ + decryptMnemonic(encryptedMnemonic: string): string { + try { + const data = Buffer.from(encryptedMnemonic, 'base64'); + const iv = data.slice(0, 16); + const encrypted = data.slice(16); + + const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); + let decrypted = decipher.update(encrypted); + decrypted = Buffer.concat([decrypted, decipher.final()]); + + return decrypted.toString('utf8'); + } catch (error) { + this.logger.error(`Failed to decrypt mnemonic: ${error.message}`); + throw new Error('Failed to decrypt mnemonic'); + } + } + + /** + * 加密助记词 + */ + private encryptMnemonic(mnemonic: string): string { + const iv = randomBytes(16); + const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); + + let encrypted = cipher.update(mnemonic, 'utf8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + + return Buffer.concat([iv, encrypted]).toString('base64'); + } + + /** + * 计算助记词哈希 (使用 bcrypt,抗暴力破解) + */ + private async hashMnemonic(mnemonic: string): Promise { + // bcrypt rounds = 12, 足够安全且不会太慢 + const hash = await bcrypt.hash(mnemonic, 12); + return hash; + } + + /** + * SHA256 哈希 (兼容旧数据) + */ + private hashMnemonicSha256(mnemonic: string): string { + const hash1 = createHash('sha256').update(mnemonic).digest(); + const hash2 = createHash('sha256').update(hash1).digest(); + return hash2.toString('hex'); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts b/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts new file mode 100644 index 00000000..00266b78 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts @@ -0,0 +1,88 @@ +import { Global, Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { PrismaService } from './persistence/prisma/prisma.service'; +import { RedisService, AddressCacheService } from './redis'; +import { EventPublisherService, MpcEventConsumerService, WithdrawalEventConsumerService } from './kafka'; +import { EvmProviderAdapter, AddressDerivationAdapter, MnemonicDerivationAdapter, RecoveryMnemonicAdapter, BlockScannerService } from './blockchain'; +import { MpcSigningClient } from './mpc'; +import { DomainModule } from '@/domain/domain.module'; +import { + DEPOSIT_TRANSACTION_REPOSITORY, + MONITORED_ADDRESS_REPOSITORY, + BLOCK_CHECKPOINT_REPOSITORY, + TRANSACTION_REQUEST_REPOSITORY, + OUTBOX_EVENT_REPOSITORY, +} from '@/domain/repositories'; +import { + DepositTransactionRepositoryImpl, + MonitoredAddressRepositoryImpl, + BlockCheckpointRepositoryImpl, + TransactionRequestRepositoryImpl, + OutboxEventRepositoryImpl, +} from './persistence/repositories'; + +@Global() +@Module({ + imports: [DomainModule, HttpModule], + providers: [ + // 核心服务 + PrismaService, + RedisService, + EventPublisherService, + MpcEventConsumerService, + WithdrawalEventConsumerService, + MpcSigningClient, + + // 区块链适配器 + EvmProviderAdapter, + AddressDerivationAdapter, + MnemonicDerivationAdapter, + RecoveryMnemonicAdapter, + BlockScannerService, + + // 缓存服务 + AddressCacheService, + + // 仓储实现 (依赖倒置) + { + provide: DEPOSIT_TRANSACTION_REPOSITORY, + useClass: DepositTransactionRepositoryImpl, + }, + { + provide: MONITORED_ADDRESS_REPOSITORY, + useClass: MonitoredAddressRepositoryImpl, + }, + { + provide: BLOCK_CHECKPOINT_REPOSITORY, + useClass: BlockCheckpointRepositoryImpl, + }, + { + provide: TRANSACTION_REQUEST_REPOSITORY, + useClass: TransactionRequestRepositoryImpl, + }, + { + provide: OUTBOX_EVENT_REPOSITORY, + useClass: OutboxEventRepositoryImpl, + }, + ], + exports: [ + PrismaService, + RedisService, + EventPublisherService, + MpcEventConsumerService, + WithdrawalEventConsumerService, + MpcSigningClient, + EvmProviderAdapter, + AddressDerivationAdapter, + MnemonicDerivationAdapter, + RecoveryMnemonicAdapter, + BlockScannerService, + AddressCacheService, + DEPOSIT_TRANSACTION_REPOSITORY, + MONITORED_ADDRESS_REPOSITORY, + BLOCK_CHECKPOINT_REPOSITORY, + TRANSACTION_REQUEST_REPOSITORY, + OUTBOX_EVENT_REPOSITORY, + ], +}) +export class InfrastructureModule {} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/deposit-ack-consumer.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/deposit-ack-consumer.service.ts new file mode 100644 index 00000000..16ff9aab --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/deposit-ack-consumer.service.ts @@ -0,0 +1,151 @@ +/** + * Deposit ACK Consumer Service + * + * 监听 wallet-service 发送的充值确认事件。 + * 当 wallet-service 成功处理充值后,会发送 ACK 事件。 + */ + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs'; +import { OutboxPublisherService } from '@/application/services/outbox-publisher.service'; + +export const ACK_TOPICS = { + WALLET_ACKS: 'wallet.acks', +} as const; + +export interface DepositCreditedPayload { + depositId: string; + txHash: string; + userId: string; + accountSequence: string; + amount: string; + creditedAt: string; +} + +@Injectable() +export class DepositAckConsumerService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(DepositAckConsumerService.name); + private kafka: Kafka; + private consumer: Consumer; + private isConnected = false; + + constructor( + private readonly configService: ConfigService, + private readonly outboxPublisher: OutboxPublisherService, + ) {} + + async onModuleInit() { + const brokersEnv = this.configService.get('KAFKA_BROKERS'); + const brokersConfig = this.configService.get('kafka.brokers'); + const brokers: string[] = brokersEnv?.split(',') || brokersConfig || ['localhost:9092']; + const clientId = this.configService.get('kafka.clientId') || 'blockchain-service'; + const groupId = 'blockchain-service-deposit-acks'; + + this.logger.log(`[INIT] Deposit ACK Consumer initializing...`); + this.logger.log(`[INIT] ClientId: ${clientId}`); + this.logger.log(`[INIT] GroupId: ${groupId}`); + this.logger.log(`[INIT] Brokers: ${brokers}`); + this.logger.log(`[INIT] Topics: ${Object.values(ACK_TOPICS).join(', ')}`); + + // 企业级重试配置:指数退避,最多重试约 2.5 小时 + this.kafka = new Kafka({ + clientId, + brokers, + logLevel: logLevel.WARN, + retry: { + initialRetryTime: 1000, // 1 秒 + maxRetryTime: 300000, // 最大 5 分钟 + retries: 15, // 最多 15 次 + multiplier: 2, // 指数退避因子 + restartOnFailure: async () => true, + }, + }); + + this.consumer = this.kafka.consumer({ + groupId, + sessionTimeout: 30000, + heartbeatInterval: 3000, + }); + + try { + this.logger.log(`[CONNECT] Connecting Deposit ACK consumer...`); + await this.consumer.connect(); + this.isConnected = true; + this.logger.log(`[CONNECT] Deposit ACK consumer connected successfully`); + + await this.consumer.subscribe({ + topics: Object.values(ACK_TOPICS), + fromBeginning: false, + }); + this.logger.log(`[SUBSCRIBE] Subscribed to ACK topics`); + + await this.startConsuming(); + } catch (error) { + this.logger.error(`[ERROR] Failed to connect Deposit ACK consumer`, error); + } + } + + async onModuleDestroy() { + if (this.isConnected) { + await this.consumer.disconnect(); + this.logger.log('Deposit ACK consumer disconnected'); + } + } + + private async startConsuming(): Promise { + await this.consumer.run({ + eachMessage: async ({ topic, partition, message }: EachMessagePayload) => { + const offset = message.offset; + this.logger.log(`[RECEIVE] ACK message received: topic=${topic}, partition=${partition}, offset=${offset}`); + + try { + const value = message.value?.toString(); + if (!value) { + this.logger.warn(`[RECEIVE] Empty ACK message received on ${topic}`); + return; + } + + this.logger.debug(`[RECEIVE] Raw ACK message: ${value.substring(0, 500)}`); + + const parsed = JSON.parse(value); + const eventType = parsed.eventType; + const payload = parsed.payload || parsed; + + this.logger.log(`[RECEIVE] ACK event type: ${eventType}`); + + if (eventType === 'wallet.deposit.credited') { + await this.handleDepositCredited(payload as DepositCreditedPayload); + } else { + this.logger.debug(`[RECEIVE] Unknown ACK event type: ${eventType}`); + } + } catch (error) { + this.logger.error(`[ERROR] Error processing ACK message from ${topic}`, error); + } + }, + }); + + this.logger.log(`[START] Started consuming ACK events`); + } + + private async handleDepositCredited(payload: DepositCreditedPayload): Promise { + this.logger.log(`[ACK] Processing deposit credited ACK`); + this.logger.log(`[ACK] depositId: ${payload.depositId}`); + this.logger.log(`[ACK] txHash: ${payload.txHash}`); + this.logger.log(`[ACK] userId: ${payload.userId}`); + this.logger.log(`[ACK] amount: ${payload.amount}`); + + try { + // 通知 OutboxPublisher 处理 ACK + await this.outboxPublisher.handleAck( + 'DepositTransaction', + payload.depositId, + 'blockchain.deposit.confirmed', + ); + + this.logger.log(`[ACK] Deposit ${payload.depositId} ACK processed successfully`); + } catch (error) { + this.logger.error(`[ACK] Error processing deposit ACK for ${payload.depositId}:`, error); + } + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-consumer.controller.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-consumer.controller.ts new file mode 100644 index 00000000..310afe23 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-consumer.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; + +interface MpcKeygenCompletedEvent { + eventId: string; + eventType: string; + occurredAt: string; + payload: { + userId: string; + deviceId: string; + publicKey: string; + keyType: string; + }; +} + +/** + * Kafka 事件消费者 + * 监听来自其他服务的事件 + */ +@Controller() +export class EventConsumerController { + private readonly logger = new Logger(EventConsumerController.name); + + /** + * 处理 MPC 密钥生成完成事件 + * 从 mpc-service 接收公钥,派生钱包地址 + */ + @EventPattern('mpc.keygen.completed') + async handleMpcKeygenCompleted(@Payload() event: MpcKeygenCompletedEvent): Promise { + this.logger.log(`Received MPC keygen completed event: ${event.eventId}`); + this.logger.debug(`User: ${event.payload.userId}, PublicKey: ${event.payload.publicKey}`); + + // TODO: 调用 AddressDerivationService 派生地址 + // 这将在应用层实现 + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts new file mode 100644 index 00000000..dd4b4723 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts @@ -0,0 +1,111 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Kafka, Producer, logLevel } from 'kafkajs'; +import { DomainEvent } from '@/domain/events/domain-event.base'; + +@Injectable() +export class EventPublisherService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(EventPublisherService.name); + private readonly kafka: Kafka; + private readonly producer: Producer; + + constructor(private readonly configService: ConfigService) { + this.kafka = new Kafka({ + clientId: this.configService.get('kafka.clientId'), + brokers: this.configService.get('kafka.brokers') || ['localhost:9092'], + logLevel: logLevel.WARN, + }); + this.producer = this.kafka.producer(); + } + + async onModuleInit() { + await this.producer.connect(); + this.logger.log('Kafka producer connected'); + } + + async onModuleDestroy() { + await this.producer.disconnect(); + this.logger.log('Kafka producer disconnected'); + } + + /** + * 发布领域事件 + */ + async publish(event: DomainEvent): Promise { + const topic = this.getTopicForEvent(event.eventType); + const message = { + key: event.eventId, + value: JSON.stringify({ + eventId: event.eventId, + eventType: event.eventType, + occurredAt: event.occurredAt.toISOString(), + payload: event.toPayload(), + }), + headers: { + eventType: event.eventType, + source: 'blockchain-service', + }, + }; + + await this.producer.send({ + topic, + messages: [message], + }); + + this.logger.debug(`Published event: ${event.eventType} to topic: ${topic}`); + } + + /** + * 批量发布事件 + */ + async publishAll(events: DomainEvent[]): Promise { + for (const event of events) { + await this.publish(event); + } + } + + /** + * 发布原始事件数据(用于 Outbox 模式) + */ + async publishRaw(event: { + eventId: string; + eventType: string; + occurredAt: Date; + payload: Record; + }): Promise { + const topic = this.getTopicForEvent(event.eventType); + const message = { + key: event.eventId, + value: JSON.stringify({ + eventId: event.eventId, + eventType: event.eventType, + occurredAt: event.occurredAt.toISOString(), + payload: event.payload, + }), + headers: { + eventType: event.eventType, + source: 'blockchain-service', + }, + }; + + await this.producer.send({ + topic, + messages: [message], + }); + + this.logger.debug(`Published raw event: ${event.eventType} to topic: ${topic}`); + } + + private getTopicForEvent(eventType: string): string { + // 事件类型到 topic 的映射 + const topicMap: Record = { + 'blockchain.deposit.detected': 'blockchain.deposits', + 'blockchain.deposit.confirmed': 'blockchain.deposits', + 'blockchain.wallet.address.created': 'blockchain.wallets', + 'blockchain.transaction.broadcasted': 'blockchain.transactions', + // MPC 签名请求 - 发送到 mpc-service 消费的 topic + 'blockchain.mpc.signing.requested': 'mpc.SigningRequested', + }; + return topicMap[eventType] || 'blockchain.events'; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/index.ts new file mode 100644 index 00000000..e78978c3 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/index.ts @@ -0,0 +1,5 @@ +export * from './event-publisher.service'; +export * from './event-consumer.controller'; +export * from './mpc-event-consumer.service'; +export * from './withdrawal-event-consumer.service'; +export * from './deposit-ack-consumer.service'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts new file mode 100644 index 00000000..cff98b82 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts @@ -0,0 +1,247 @@ +/** + * MPC Event Consumer Service for Blockchain Service + * + * Consumes MPC events from mpc-service via Kafka: + * - KeygenCompleted: derives wallet addresses from public keys + * - SigningCompleted: returns signature for hot wallet transfers + * - SessionFailed: handles keygen/signing failures + */ + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs'; + +// MPC Event Topics (events from mpc-service) +export const MPC_TOPICS = { + KEYGEN_COMPLETED: 'mpc.KeygenCompleted', + SIGNING_COMPLETED: 'mpc.SigningCompleted', + SESSION_FAILED: 'mpc.SessionFailed', +} as const; + +export interface KeygenCompletedPayload { + sessionId: string; + partyId: string; + publicKey: string; + shareId: string; + threshold: string; + extraPayload?: { + userId: string; + accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号) + username: string; + delegateShare?: { + partyId: string; + partyIndex: number; + encryptedShare: string; + }; + serverParties?: string[]; + }; +} + +export interface SigningCompletedPayload { + sessionId: string; + partyId: string; + messageHash: string; + signature: string; + publicKey: string; + extraPayload?: { + userId: string; + username: string; + mpcSessionId: string; + source?: string; // 'blockchain-service' | 'identity-service' + }; +} + +export interface SessionFailedPayload { + sessionId: string; + partyId: string; + sessionType: string; // 'keygen' | 'sign' + errorMessage: string; + errorCode?: string; + extraPayload?: { + userId: string; + username: string; + source?: string; + }; +} + +export type MpcEventHandler = (payload: T) => Promise; + +@Injectable() +export class MpcEventConsumerService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(MpcEventConsumerService.name); + private kafka: Kafka; + private consumer: Consumer; + private isConnected = false; + + private keygenCompletedHandler?: MpcEventHandler; + private signingCompletedHandler?: MpcEventHandler; + private sessionFailedHandler?: MpcEventHandler; + private signingFailedHandler?: MpcEventHandler; + + constructor(private readonly configService: ConfigService) {} + + async onModuleInit() { + const brokers = this.configService.get('KAFKA_BROKERS')?.split(',') || ['localhost:9092']; + const clientId = this.configService.get('KAFKA_CLIENT_ID') || 'blockchain-service'; + const groupId = 'blockchain-service-mpc-events'; + + this.logger.log(`[INIT] MPC Event Consumer for blockchain-service initializing...`); + this.logger.log(`[INIT] ClientId: ${clientId}`); + this.logger.log(`[INIT] GroupId: ${groupId}`); + this.logger.log(`[INIT] Brokers: ${brokers.join(', ')}`); + this.logger.log(`[INIT] Topics to subscribe: ${Object.values(MPC_TOPICS).join(', ')}`); + + // 企业级重试配置:指数退避,最多重试约 2.5 小时 + this.kafka = new Kafka({ + clientId, + brokers, + logLevel: logLevel.WARN, + retry: { + initialRetryTime: 1000, // 1 秒 + maxRetryTime: 300000, // 最大 5 分钟 + retries: 15, // 最多 15 次 + multiplier: 2, // 指数退避因子 + restartOnFailure: async () => true, + }, + }); + + this.consumer = this.kafka.consumer({ + groupId, + sessionTimeout: 30000, + heartbeatInterval: 3000, + }); + + try { + this.logger.log(`[CONNECT] Connecting MPC Event consumer...`); + await this.consumer.connect(); + this.isConnected = true; + this.logger.log(`[CONNECT] MPC Event Kafka consumer connected successfully`); + + // Subscribe to MPC topics + await this.consumer.subscribe({ topics: Object.values(MPC_TOPICS), fromBeginning: false }); + this.logger.log(`[SUBSCRIBE] Subscribed to MPC topics: ${Object.values(MPC_TOPICS).join(', ')}`); + + // Start consuming + await this.startConsuming(); + } catch (error) { + this.logger.error(`[ERROR] Failed to connect MPC Event Kafka consumer`, error); + } + } + + async onModuleDestroy() { + if (this.isConnected) { + await this.consumer.disconnect(); + this.logger.log('MPC Event Kafka consumer disconnected'); + } + } + + /** + * Register handler for keygen completed events + */ + onKeygenCompleted(handler: MpcEventHandler): void { + this.keygenCompletedHandler = handler; + this.logger.log(`[REGISTER] KeygenCompleted handler registered`); + } + + /** + * Register handler for signing completed events + */ + onSigningCompleted(handler: MpcEventHandler): void { + this.signingCompletedHandler = handler; + this.logger.log(`[REGISTER] SigningCompleted handler registered`); + } + + /** + * Register handler for session failed events (keygen) + */ + onSessionFailed(handler: MpcEventHandler): void { + this.sessionFailedHandler = handler; + this.logger.log(`[REGISTER] SessionFailed handler registered`); + } + + /** + * Register handler for signing failed events + */ + onSigningFailed(handler: MpcEventHandler): void { + this.signingFailedHandler = handler; + this.logger.log(`[REGISTER] SigningFailed handler registered`); + } + + private async startConsuming(): Promise { + await this.consumer.run({ + eachMessage: async ({ topic, partition, message }: EachMessagePayload) => { + const offset = message.offset; + this.logger.log(`[RECEIVE] Message received: topic=${topic}, partition=${partition}, offset=${offset}`); + + try { + const value = message.value?.toString(); + if (!value) { + this.logger.warn(`[RECEIVE] Empty message received on ${topic}`); + return; + } + + this.logger.log(`[RECEIVE] Raw message value: ${value.substring(0, 500)}...`); + + const parsed = JSON.parse(value); + const payload = parsed.payload || parsed; + + this.logger.log(`[RECEIVE] Parsed event: eventType=${parsed.eventType || 'unknown'}`); + this.logger.log(`[RECEIVE] Payload keys: ${Object.keys(payload).join(', ')}`); + + switch (topic) { + case MPC_TOPICS.KEYGEN_COMPLETED: + this.logger.log(`[HANDLE] Processing KeygenCompleted event for blockchain-service`); + this.logger.log(`[HANDLE] publicKey: ${(payload as KeygenCompletedPayload).publicKey?.substring(0, 20)}...`); + this.logger.log(`[HANDLE] extraPayload.userId: ${(payload as KeygenCompletedPayload).extraPayload?.userId}`); + if (this.keygenCompletedHandler) { + await this.keygenCompletedHandler(payload as KeygenCompletedPayload); + this.logger.log(`[HANDLE] KeygenCompleted handler completed successfully`); + } else { + this.logger.warn(`[HANDLE] No handler registered for KeygenCompleted`); + } + break; + + case MPC_TOPICS.SIGNING_COMPLETED: + this.logger.log(`[HANDLE] Processing SigningCompleted event`); + this.logger.log(`[HANDLE] sessionId: ${(payload as SigningCompletedPayload).sessionId}`); + this.logger.log(`[HANDLE] signature: ${(payload as SigningCompletedPayload).signature?.substring(0, 20)}...`); + if (this.signingCompletedHandler) { + await this.signingCompletedHandler(payload as SigningCompletedPayload); + this.logger.log(`[HANDLE] SigningCompleted handler completed successfully`); + } else { + this.logger.warn(`[HANDLE] No handler registered for SigningCompleted`); + } + break; + + case MPC_TOPICS.SESSION_FAILED: + this.logger.log(`[HANDLE] Processing SessionFailed event`); + this.logger.log(`[HANDLE] sessionType: ${(payload as SessionFailedPayload).sessionType}`); + this.logger.log(`[HANDLE] errorMessage: ${(payload as SessionFailedPayload).errorMessage}`); + + const failedPayload = payload as SessionFailedPayload; + // Route to appropriate handler based on session type + if (failedPayload.sessionType === 'sign') { + if (this.signingFailedHandler) { + await this.signingFailedHandler(failedPayload); + this.logger.log(`[HANDLE] SigningFailed handler completed`); + } + } else { + if (this.sessionFailedHandler) { + await this.sessionFailedHandler(failedPayload); + this.logger.log(`[HANDLE] SessionFailed handler completed`); + } + } + break; + + default: + this.logger.warn(`[RECEIVE] Unknown MPC topic: ${topic}`); + } + } catch (error) { + this.logger.error(`[ERROR] Error processing MPC event from ${topic}`, error); + } + }, + }); + + this.logger.log(`[START] Started consuming MPC events for address derivation`); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts new file mode 100644 index 00000000..01bc249b --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts @@ -0,0 +1,186 @@ +/** + * Withdrawal Event Consumer Service for Blockchain Service + * + * Consumes withdrawal request events from wallet-service via Kafka. + * Creates transaction requests for MPC signing and blockchain broadcasting. + */ + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs'; + +export const WITHDRAWAL_TOPICS = { + WITHDRAWAL_REQUESTED: 'wallet.withdrawals', + SYSTEM_WITHDRAWAL_REQUESTED: 'wallet.system-withdrawals', +} as const; + +export interface WithdrawalRequestedPayload { + orderNo: string; + accountSequence: string; + userId: string; + walletId: string; + amount: string; + fee: string; + netAmount: string; + assetType: string; + chainType: string; + toAddress: string; +} + +export interface SystemWithdrawalRequestedPayload { + orderNo: string; + fromAccountSequence: string; + fromAccountName: string; + toAccountSequence: string; + toAddress: string; + amount: string; + chainType: string; +} + +export type WithdrawalEventHandler = (payload: WithdrawalRequestedPayload) => Promise; +export type SystemWithdrawalEventHandler = (payload: SystemWithdrawalRequestedPayload) => Promise; + +@Injectable() +export class WithdrawalEventConsumerService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(WithdrawalEventConsumerService.name); + private kafka: Kafka; + private consumer: Consumer; + private isConnected = false; + + private withdrawalRequestedHandler?: WithdrawalEventHandler; + private systemWithdrawalRequestedHandler?: SystemWithdrawalEventHandler; + + constructor(private readonly configService: ConfigService) {} + + async onModuleInit() { + const brokers = this.configService.get('KAFKA_BROKERS')?.split(',') || ['localhost:9092']; + const clientId = this.configService.get('KAFKA_CLIENT_ID') || 'blockchain-service'; + const groupId = 'blockchain-service-withdrawal-events'; + + this.logger.log(`[INIT] Withdrawal Event Consumer initializing...`); + this.logger.log(`[INIT] ClientId: ${clientId}`); + this.logger.log(`[INIT] GroupId: ${groupId}`); + this.logger.log(`[INIT] Brokers: ${brokers.join(', ')}`); + this.logger.log(`[INIT] Topics: ${Object.values(WITHDRAWAL_TOPICS).join(', ')}`); + + // 企业级重试配置:指数退避,最多重试约 2.5 小时 + this.kafka = new Kafka({ + clientId, + brokers, + logLevel: logLevel.WARN, + retry: { + initialRetryTime: 1000, // 1 秒 + maxRetryTime: 300000, // 最大 5 分钟 + retries: 15, // 最多 15 次 + multiplier: 2, // 指数退避因子 + restartOnFailure: async () => true, + }, + }); + + this.consumer = this.kafka.consumer({ + groupId, + sessionTimeout: 30000, + heartbeatInterval: 3000, + }); + + try { + this.logger.log(`[CONNECT] Connecting Withdrawal Event consumer...`); + await this.consumer.connect(); + this.isConnected = true; + this.logger.log(`[CONNECT] Withdrawal Event consumer connected successfully`); + + await this.consumer.subscribe({ + topics: Object.values(WITHDRAWAL_TOPICS), + fromBeginning: false, + }); + this.logger.log(`[SUBSCRIBE] Subscribed to withdrawal topics`); + + await this.startConsuming(); + } catch (error) { + this.logger.error(`[ERROR] Failed to connect Withdrawal Event consumer`, error); + } + } + + async onModuleDestroy() { + if (this.isConnected) { + await this.consumer.disconnect(); + this.logger.log('Withdrawal Event consumer disconnected'); + } + } + + /** + * Register handler for withdrawal requested events + */ + onWithdrawalRequested(handler: WithdrawalEventHandler): void { + this.withdrawalRequestedHandler = handler; + this.logger.log(`[REGISTER] WithdrawalRequested handler registered`); + } + + /** + * Register handler for system withdrawal requested events + */ + onSystemWithdrawalRequested(handler: SystemWithdrawalEventHandler): void { + this.systemWithdrawalRequestedHandler = handler; + this.logger.log(`[REGISTER] SystemWithdrawalRequested handler registered`); + } + + private async startConsuming(): Promise { + await this.consumer.run({ + eachMessage: async ({ topic, partition, message }: EachMessagePayload) => { + const offset = message.offset; + this.logger.log(`[RECEIVE] Message received: topic=${topic}, partition=${partition}, offset=${offset}`); + + try { + const value = message.value?.toString(); + if (!value) { + this.logger.warn(`[RECEIVE] Empty message received on ${topic}`); + return; + } + + this.logger.log(`[RECEIVE] Raw message: ${value.substring(0, 500)}...`); + + const parsed = JSON.parse(value); + const eventType = parsed.eventType; + const payload = parsed.payload || parsed; + + this.logger.log(`[RECEIVE] Event type: ${eventType}`); + + if (eventType === 'wallet.withdrawal.requested') { + this.logger.log(`[HANDLE] Processing WithdrawalRequested event`); + this.logger.log(`[HANDLE] orderNo: ${payload.orderNo}`); + this.logger.log(`[HANDLE] chainType: ${payload.chainType}`); + this.logger.log(`[HANDLE] toAddress: ${payload.toAddress}`); + this.logger.log(`[HANDLE] amount: ${payload.amount}`); + + if (this.withdrawalRequestedHandler) { + await this.withdrawalRequestedHandler(payload as WithdrawalRequestedPayload); + this.logger.log(`[HANDLE] WithdrawalRequested handler completed`); + } else { + this.logger.warn(`[HANDLE] No handler registered for WithdrawalRequested`); + } + } else if (eventType === 'wallet.system-withdrawal.requested') { + this.logger.log(`[HANDLE] Processing SystemWithdrawalRequested event`); + this.logger.log(`[HANDLE] orderNo: ${payload.orderNo}`); + this.logger.log(`[HANDLE] fromAccountSequence: ${payload.fromAccountSequence}`); + this.logger.log(`[HANDLE] toAccountSequence: ${payload.toAccountSequence}`); + this.logger.log(`[HANDLE] toAddress: ${payload.toAddress}`); + this.logger.log(`[HANDLE] amount: ${payload.amount}`); + + if (this.systemWithdrawalRequestedHandler) { + await this.systemWithdrawalRequestedHandler(payload as SystemWithdrawalRequestedPayload); + this.logger.log(`[HANDLE] SystemWithdrawalRequested handler completed`); + } else { + this.logger.warn(`[HANDLE] No handler registered for SystemWithdrawalRequested`); + } + } else { + this.logger.warn(`[RECEIVE] Unknown event type: ${eventType}`); + } + } catch (error) { + this.logger.error(`[ERROR] Error processing withdrawal event from ${topic}`, error); + } + }, + }); + + this.logger.log(`[START] Started consuming withdrawal events`); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/mpc/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/mpc/index.ts new file mode 100644 index 00000000..6c568d04 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/mpc/index.ts @@ -0,0 +1 @@ +export * from './mpc-signing.client'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts new file mode 100644 index 00000000..7f4fcea2 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts @@ -0,0 +1,197 @@ +/** + * MPC Signing Client + * + * 通过 Kafka 事件与 mpc-service 通信进行 MPC 签名 + * 用于热钱包的 ERC20 转账签名 + * + * 事件流: + * blockchain-service → Kafka(mpc.SigningRequested) → mpc-service + * mpc-service → Kafka(mpc.SigningCompleted) → blockchain-service + */ + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { randomUUID } from 'crypto'; +import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; +import { + MpcEventConsumerService, + SigningCompletedPayload, + SessionFailedPayload, +} from '@/infrastructure/kafka/mpc-event-consumer.service'; + +export interface CreateSigningInput { + username: string; + messageHash: string; +} + +export interface SigningResult { + sessionId: string; + status: string; + signature?: string; +} + +// 签名结果回调 +type SigningCallback = (signature: string | null, error?: string) => void; + +// MPC 签名请求 Topic +export const MPC_SIGNING_TOPIC = 'mpc.SigningRequested'; + +@Injectable() +export class MpcSigningClient implements OnModuleInit { + private readonly logger = new Logger(MpcSigningClient.name); + private readonly hotWalletUsername: string; + private readonly hotWalletAddress: string; + private readonly signingTimeoutMs: number = 300000; // 5 minutes + + // 待处理的签名请求回调 Map + private pendingRequests: Map void; + reject: (error: Error) => void; + timeout: NodeJS.Timeout; + }> = new Map(); + + constructor( + private readonly configService: ConfigService, + private readonly eventPublisher: EventPublisherService, + private readonly mpcEventConsumer: MpcEventConsumerService, + ) { + this.hotWalletUsername = this.configService.get('HOT_WALLET_USERNAME', ''); + this.hotWalletAddress = this.configService.get('HOT_WALLET_ADDRESS', ''); + + if (!this.hotWalletUsername) { + this.logger.warn('[INIT] HOT_WALLET_USERNAME not configured'); + } + if (!this.hotWalletAddress) { + this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured'); + } + + this.logger.log(`[INIT] Hot Wallet Username: ${this.hotWalletUsername || '(not configured)'}`); + this.logger.log(`[INIT] Hot Wallet Address: ${this.hotWalletAddress || '(not configured)'}`); + this.logger.log(`[INIT] Using Kafka event-driven signing`); + } + + async onModuleInit() { + // 注册签名完成和失败事件处理器 + this.mpcEventConsumer.onSigningCompleted(this.handleSigningCompleted.bind(this)); + this.mpcEventConsumer.onSigningFailed(this.handleSigningFailed.bind(this)); + this.logger.log('[INIT] MPC signing event handlers registered'); + } + + /** + * 检查热钱包是否已配置 + */ + isConfigured(): boolean { + return !!this.hotWalletUsername && !!this.hotWalletAddress; + } + + /** + * 获取热钱包地址 + */ + getHotWalletAddress(): string { + return this.hotWalletAddress; + } + + /** + * 获取热钱包用户名 + */ + getHotWalletUsername(): string { + return this.hotWalletUsername; + } + + /** + * 签名消息(通过 Kafka 事件驱动) + * + * @param messageHash 要签名的消息哈希 (hex string with 0x prefix) + * @returns 签名结果 (hex string) + */ + async signMessage(messageHash: string): Promise { + this.logger.log(`[SIGN] Starting MPC signing for: ${messageHash.slice(0, 16)}...`); + + if (!this.hotWalletUsername) { + throw new Error('Hot wallet username not configured'); + } + + const sessionId = randomUUID(); + this.logger.log(`[SIGN] Session ID: ${sessionId}`); + + // 创建 Promise 等待签名结果 + const signaturePromise = new Promise((resolve, reject) => { + // 设置超时 + const timeout = setTimeout(() => { + this.pendingRequests.delete(sessionId); + reject(new Error(`MPC signing timeout after ${this.signingTimeoutMs}ms`)); + }, this.signingTimeoutMs); + + // 保存到待处理队列 + this.pendingRequests.set(sessionId, { resolve, reject, timeout }); + }); + + // 发布签名请求事件到 Kafka + try { + await this.eventPublisher.publish({ + eventType: 'blockchain.mpc.signing.requested', + toPayload: () => ({ + sessionId, + userId: 'system', // 系统热钱包 + username: this.hotWalletUsername, + messageHash, + source: 'blockchain-service', + }), + eventId: sessionId, + occurredAt: new Date(), + }); + + this.logger.log(`[SIGN] Signing request published to Kafka: sessionId=${sessionId}`); + } catch (error) { + // 发布失败,清理待处理队列 + const pending = this.pendingRequests.get(sessionId); + if (pending) { + clearTimeout(pending.timeout); + this.pendingRequests.delete(sessionId); + } + throw error; + } + + // 等待签名结果 + const signature = await signaturePromise; + this.logger.log(`[SIGN] Signature obtained: ${signature.slice(0, 20)}...`); + return signature; + } + + /** + * 处理签名完成事件 + */ + private async handleSigningCompleted(payload: SigningCompletedPayload): Promise { + const sessionId = payload.sessionId; + this.logger.log(`[EVENT] Signing completed: sessionId=${sessionId}`); + + const pending = this.pendingRequests.get(sessionId); + if (pending) { + clearTimeout(pending.timeout); + this.pendingRequests.delete(sessionId); + + if (payload.signature) { + pending.resolve(payload.signature); + } else { + pending.reject(new Error('Signing completed but no signature returned')); + } + } else { + this.logger.warn(`[EVENT] No pending request for sessionId=${sessionId}`); + } + } + + /** + * 处理签名失败事件 + */ + private async handleSigningFailed(payload: SessionFailedPayload): Promise { + const sessionId = payload.sessionId; + this.logger.warn(`[EVENT] Signing failed: sessionId=${sessionId}, error=${payload.errorMessage}`); + + const pending = this.pendingRequests.get(sessionId); + if (pending) { + clearTimeout(pending.timeout); + this.pendingRequests.delete(sessionId); + pending.reject(new Error(`MPC signing failed: ${payload.errorMessage}`)); + } + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts new file mode 100644 index 00000000..3f0a1d35 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts @@ -0,0 +1,79 @@ +import { DepositTransaction as PrismaDepositTransaction } from '@prisma/client'; +import { + DepositTransaction, + DepositTransactionProps, +} from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +export class DepositTransactionMapper { + /** + * Map from Prisma to Domain (only for USER deposits) + * System account deposits are handled separately + */ + static toDomain(prisma: PrismaDepositTransaction): DepositTransaction { + // For USER deposits, accountSequence and userId must exist + if (!prisma.accountSequence || !prisma.userId) { + throw new Error(`DepositTransaction ${prisma.id} missing accountSequence or userId`); + } + + // 使用 amountFormatted 重建 TokenAmount + // amountFormatted 已经是正确的人类可读格式(如 1000000.00000000) + // 用 decimals=8 与 toFixed(8) 保持一致,确保 toFixed(8) 返回相同的值 + const amount = TokenAmount.fromFormatted(prisma.amountFormatted.toString(), 8); + + const props: DepositTransactionProps = { + id: prisma.id, + chainType: ChainType.create(prisma.chainType), + txHash: TxHash.fromUnchecked(prisma.txHash), + fromAddress: EvmAddress.fromUnchecked(prisma.fromAddress), + toAddress: EvmAddress.fromUnchecked(prisma.toAddress), + tokenContract: EvmAddress.fromUnchecked(prisma.tokenContract), + amount, + blockNumber: BlockNumber.create(prisma.blockNumber), + blockTimestamp: prisma.blockTimestamp, + logIndex: prisma.logIndex, + confirmations: prisma.confirmations, + status: prisma.status as DepositStatus, + addressId: prisma.addressId, + accountSequence: prisma.accountSequence, + userId: prisma.userId, + notifiedAt: prisma.notifiedAt ?? undefined, + notifyAttempts: prisma.notifyAttempts, + lastNotifyError: prisma.lastNotifyError ?? undefined, + createdAt: prisma.createdAt, + updatedAt: prisma.updatedAt, + }; + + return DepositTransaction.reconstitute(props); + } + + static toPersistence( + domain: DepositTransaction, + ): Omit & { id?: bigint } { + return { + id: domain.id, + chainType: domain.chainType.toString(), + txHash: domain.txHash.toString(), + fromAddress: domain.fromAddress.toString(), + toAddress: domain.toAddress.toString(), + tokenContract: domain.tokenContract.toString(), + amount: domain.amount.toDecimal(), + amountFormatted: domain.amount.toFormattedDecimal(), + blockNumber: domain.blockNumber.value, + blockTimestamp: domain.blockTimestamp, + logIndex: domain.logIndex, + confirmations: domain.confirmations, + status: domain.status, + addressId: domain.addressId, + addressType: 'USER', + accountSequence: domain.accountSequence, + userId: domain.userId, + systemAccountType: null, + systemAccountId: null, + notifiedAt: domain.notifiedAt ?? null, + notifyAttempts: domain.notifyAttempts, + lastNotifyError: domain.lastNotifyError ?? null, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/index.ts new file mode 100644 index 00000000..29f870e6 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/index.ts @@ -0,0 +1,3 @@ +export * from './deposit-transaction.mapper'; +export * from './monitored-address.mapper'; +export * from './transaction-request.mapper'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts new file mode 100644 index 00000000..4c765087 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts @@ -0,0 +1,46 @@ +import { MonitoredAddress as PrismaMonitoredAddress } from '@prisma/client'; +import { MonitoredAddress, MonitoredAddressProps } from '@/domain/aggregates/monitored-address'; +import { ChainType, EvmAddress } from '@/domain/value-objects'; + +export class MonitoredAddressMapper { + /** + * Map from Prisma to Domain (only for USER addresses) + * System addresses are handled separately + */ + static toDomain(prisma: PrismaMonitoredAddress): MonitoredAddress { + // For USER addresses, accountSequence and userId must exist + if (!prisma.accountSequence || !prisma.userId) { + throw new Error(`MonitoredAddress ${prisma.id} missing accountSequence or userId`); + } + + const props: MonitoredAddressProps = { + id: prisma.id, + chainType: ChainType.create(prisma.chainType), + address: EvmAddress.fromUnchecked(prisma.address), + accountSequence: prisma.accountSequence, + userId: prisma.userId, + isActive: prisma.isActive, + createdAt: prisma.createdAt, + updatedAt: prisma.updatedAt, + }; + + return MonitoredAddress.reconstitute(props); + } + + static toPersistence( + domain: MonitoredAddress, + ): Omit & { id?: bigint } { + return { + id: domain.id, + chainType: domain.chainType.toString(), + address: domain.address.lowercase, + addressType: 'USER', + accountSequence: domain.accountSequence, + userId: domain.userId, + systemAccountType: null, + systemAccountId: null, + regionCode: null, + isActive: domain.isActive, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/transaction-request.mapper.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/transaction-request.mapper.ts new file mode 100644 index 00000000..8dcce4d2 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/mappers/transaction-request.mapper.ts @@ -0,0 +1,57 @@ +import { TransactionRequest as PrismaTransactionRequest } from '@prisma/client'; +import { + TransactionRequest, + TransactionRequestProps, +} from '@/domain/aggregates/transaction-request'; +import { ChainType, TxHash, EvmAddress, TokenAmount } from '@/domain/value-objects'; +import { TransactionStatus } from '@/domain/enums'; + +export class TransactionRequestMapper { + static toDomain(prisma: PrismaTransactionRequest): TransactionRequest { + const props: TransactionRequestProps = { + id: prisma.id, + chainType: ChainType.create(prisma.chainType), + sourceService: prisma.sourceService, + sourceOrderId: prisma.sourceOrderId, + fromAddress: EvmAddress.fromUnchecked(prisma.fromAddress), + toAddress: EvmAddress.fromUnchecked(prisma.toAddress), + value: TokenAmount.fromDecimal(prisma.value, 18), + data: prisma.data ?? undefined, + signedTx: prisma.signedTx ?? undefined, + txHash: prisma.txHash ? TxHash.fromUnchecked(prisma.txHash) : undefined, + status: prisma.status as TransactionStatus, + gasLimit: prisma.gasLimit ?? undefined, + gasPrice: prisma.gasPrice ?? undefined, + nonce: prisma.nonce ?? undefined, + errorMessage: prisma.errorMessage ?? undefined, + retryCount: prisma.retryCount, + createdAt: prisma.createdAt, + updatedAt: prisma.updatedAt, + }; + + return TransactionRequest.reconstitute(props); + } + + static toPersistence( + domain: TransactionRequest, + ): Omit & { id?: bigint } { + return { + id: domain.id, + chainType: domain.chainType.toString(), + sourceService: domain.sourceService, + sourceOrderId: domain.sourceOrderId, + fromAddress: domain.fromAddress.toString(), + toAddress: domain.toAddress.toString(), + value: domain.value.toDecimal(), + data: domain.data ?? null, + signedTx: domain.signedTx ?? null, + txHash: domain.txHash?.toString() ?? null, + status: domain.status, + gasLimit: domain.gasLimit ?? null, + gasPrice: domain.gasPrice ?? null, + nonce: domain.nonce ?? null, + errorMessage: domain.errorMessage ?? null, + retryCount: domain.retryCount, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/prisma/prisma.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/prisma/prisma.service.ts new file mode 100644 index 00000000..6226b225 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/prisma/prisma.service.ts @@ -0,0 +1,17 @@ +import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(PrismaService.name); + + async onModuleInit() { + await this.$connect(); + this.logger.log('Prisma connected to database'); + } + + async onModuleDestroy() { + await this.$disconnect(); + this.logger.log('Prisma disconnected from database'); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/block-checkpoint.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/block-checkpoint.repository.impl.ts new file mode 100644 index 00000000..638aaba5 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/block-checkpoint.repository.impl.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { + IBlockCheckpointRepository, + BlockCheckpointData, +} from '@/domain/repositories/block-checkpoint.repository.interface'; +import { ChainType, BlockNumber } from '@/domain/value-objects'; + +@Injectable() +export class BlockCheckpointRepositoryImpl implements IBlockCheckpointRepository { + constructor(private readonly prisma: PrismaService) {} + + async getLastScannedBlock(chainType: ChainType): Promise { + const record = await this.prisma.blockCheckpoint.findUnique({ + where: { chainType: chainType.toString() }, + }); + return record ? BlockNumber.create(record.lastScannedBlock) : null; + } + + async updateCheckpoint(chainType: ChainType, blockNumber: BlockNumber): Promise { + await this.prisma.blockCheckpoint.upsert({ + where: { chainType: chainType.toString() }, + update: { + lastScannedBlock: blockNumber.value, + lastScannedAt: new Date(), + isHealthy: true, + lastError: null, + }, + create: { + chainType: chainType.toString(), + lastScannedBlock: blockNumber.value, + lastScannedAt: new Date(), + isHealthy: true, + }, + }); + } + + async recordError(chainType: ChainType, error: string): Promise { + await this.prisma.blockCheckpoint.update({ + where: { chainType: chainType.toString() }, + data: { + isHealthy: false, + lastError: error, + }, + }); + } + + async markHealthy(chainType: ChainType): Promise { + await this.prisma.blockCheckpoint.update({ + where: { chainType: chainType.toString() }, + data: { + isHealthy: true, + lastError: null, + }, + }); + } + + async getCheckpoint(chainType: ChainType): Promise { + const record = await this.prisma.blockCheckpoint.findUnique({ + where: { chainType: chainType.toString() }, + }); + + if (!record) return null; + + return { + chainType: record.chainType, + lastScannedBlock: record.lastScannedBlock, + lastScannedAt: record.lastScannedAt, + isHealthy: record.isHealthy, + lastError: record.lastError ?? undefined, + }; + } + + async initializeIfNotExists(chainType: ChainType, startBlock: BlockNumber): Promise { + const existing = await this.prisma.blockCheckpoint.findUnique({ + where: { chainType: chainType.toString() }, + }); + + if (!existing) { + await this.prisma.blockCheckpoint.create({ + data: { + chainType: chainType.toString(), + lastScannedBlock: startBlock.value, + lastScannedAt: new Date(), + isHealthy: true, + }, + }); + } + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/deposit-transaction.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/deposit-transaction.repository.impl.ts new file mode 100644 index 00000000..81363dff --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/deposit-transaction.repository.impl.ts @@ -0,0 +1,97 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { DepositTransactionMapper } from '../mappers/deposit-transaction.mapper'; +import { IDepositTransactionRepository } from '@/domain/repositories/deposit-transaction.repository.interface'; +import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; +import { ChainType, TxHash } from '@/domain/value-objects'; +import { DepositStatus } from '@/domain/enums'; + +@Injectable() +export class DepositTransactionRepositoryImpl implements IDepositTransactionRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(deposit: DepositTransaction): Promise { + const data = DepositTransactionMapper.toPersistence(deposit); + + if (deposit.id) { + const updated = await this.prisma.depositTransaction.update({ + where: { id: deposit.id }, + data, + }); + return DepositTransactionMapper.toDomain(updated); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _, ...createData } = data; + const created = await this.prisma.depositTransaction.create({ + data: createData, + }); + return DepositTransactionMapper.toDomain(created); + } + } + + async findById(id: bigint): Promise { + const record = await this.prisma.depositTransaction.findUnique({ + where: { id }, + }); + return record ? DepositTransactionMapper.toDomain(record) : null; + } + + async findByTxHash(txHash: TxHash): Promise { + const record = await this.prisma.depositTransaction.findUnique({ + where: { txHash: txHash.toString() }, + }); + return record ? DepositTransactionMapper.toDomain(record) : null; + } + + async findByStatus(chainType: ChainType, status: DepositStatus): Promise { + const records = await this.prisma.depositTransaction.findMany({ + where: { + chainType: chainType.toString(), + status, + }, + orderBy: { blockNumber: 'asc' }, + }); + return records.map(DepositTransactionMapper.toDomain); + } + + async findPendingConfirmation(chainType: ChainType): Promise { + const records = await this.prisma.depositTransaction.findMany({ + where: { + chainType: chainType.toString(), + status: { + in: [DepositStatus.DETECTED, DepositStatus.CONFIRMING], + }, + }, + orderBy: { blockNumber: 'asc' }, + }); + return records.map(DepositTransactionMapper.toDomain); + } + + async findPendingNotification(): Promise { + const records = await this.prisma.depositTransaction.findMany({ + where: { + status: DepositStatus.CONFIRMED, + notifiedAt: null, + }, + orderBy: { createdAt: 'asc' }, + take: 100, + }); + return records.map(DepositTransactionMapper.toDomain); + } + + async findByUserId(userId: bigint, limit: number = 50): Promise { + const records = await this.prisma.depositTransaction.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); + return records.map(DepositTransactionMapper.toDomain); + } + + async existsByTxHash(txHash: TxHash): Promise { + const count = await this.prisma.depositTransaction.count({ + where: { txHash: txHash.toString() }, + }); + return count > 0; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts new file mode 100644 index 00000000..8fc5c7a3 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts @@ -0,0 +1,5 @@ +export * from './deposit-transaction.repository.impl'; +export * from './monitored-address.repository.impl'; +export * from './block-checkpoint.repository.impl'; +export * from './transaction-request.repository.impl'; +export * from './outbox-event.repository.impl'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/monitored-address.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/monitored-address.repository.impl.ts new file mode 100644 index 00000000..ef79b938 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/monitored-address.repository.impl.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { MonitoredAddressMapper } from '../mappers/monitored-address.mapper'; +import { IMonitoredAddressRepository } from '@/domain/repositories/monitored-address.repository.interface'; +import { MonitoredAddress } from '@/domain/aggregates/monitored-address'; +import { ChainType, EvmAddress } from '@/domain/value-objects'; + +@Injectable() +export class MonitoredAddressRepositoryImpl implements IMonitoredAddressRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(address: MonitoredAddress): Promise { + const data = MonitoredAddressMapper.toPersistence(address); + + if (address.id) { + const updated = await this.prisma.monitoredAddress.update({ + where: { id: address.id }, + data, + }); + return MonitoredAddressMapper.toDomain(updated); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _, ...createData } = data; + const created = await this.prisma.monitoredAddress.create({ + data: createData, + }); + return MonitoredAddressMapper.toDomain(created); + } + } + + async findById(id: bigint): Promise { + const record = await this.prisma.monitoredAddress.findUnique({ + where: { id }, + }); + return record ? MonitoredAddressMapper.toDomain(record) : null; + } + + async findByChainAndAddress( + chainType: ChainType, + address: EvmAddress, + ): Promise { + const record = await this.prisma.monitoredAddress.findUnique({ + where: { + uk_chain_address: { + chainType: chainType.toString(), + address: address.lowercase, + }, + }, + }); + return record ? MonitoredAddressMapper.toDomain(record) : null; + } + + async findActiveByChain(chainType: ChainType): Promise { + const records = await this.prisma.monitoredAddress.findMany({ + where: { + chainType: chainType.toString(), + isActive: true, + }, + }); + return records.map(MonitoredAddressMapper.toDomain); + } + + async findByUserId(userId: bigint): Promise { + const records = await this.prisma.monitoredAddress.findMany({ + where: { userId }, + }); + return records.map(MonitoredAddressMapper.toDomain); + } + + async existsByChainAndAddress(chainType: ChainType, address: EvmAddress): Promise { + const count = await this.prisma.monitoredAddress.count({ + where: { + chainType: chainType.toString(), + address: address.lowercase, + }, + }); + return count > 0; + } + + async getAllActiveAddresses(chainType: ChainType): Promise { + const records = await this.prisma.monitoredAddress.findMany({ + where: { + chainType: chainType.toString(), + isActive: true, + }, + select: { address: true }, + }); + return records.map((r) => r.address.toLowerCase()); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/outbox-event.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/outbox-event.repository.impl.ts new file mode 100644 index 00000000..6079251e --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/outbox-event.repository.impl.ts @@ -0,0 +1,219 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + IOutboxEventRepository, + OutboxEvent, + OutboxEventData, + OutboxEventStatus, +} from '@/domain/repositories/outbox-event.repository.interface'; + +@Injectable() +export class OutboxEventRepositoryImpl implements IOutboxEventRepository { + private readonly logger = new Logger(OutboxEventRepositoryImpl.name); + + constructor(private readonly prisma: PrismaService) {} + + async create(data: OutboxEventData): Promise { + const record = await this.prisma.outboxEvent.create({ + data: { + eventType: data.eventType, + aggregateId: data.aggregateId, + aggregateType: data.aggregateType, + payload: data.payload as Prisma.InputJsonValue, + status: OutboxEventStatus.PENDING, + retryCount: 0, + maxRetries: 10, + }, + }); + return this.mapToOutboxEvent(record); + } + + async createMany(data: OutboxEventData[]): Promise { + await this.prisma.outboxEvent.createMany({ + data: data.map((d) => ({ + eventType: d.eventType, + aggregateId: d.aggregateId, + aggregateType: d.aggregateType, + payload: d.payload as Prisma.InputJsonValue, + status: OutboxEventStatus.PENDING, + retryCount: 0, + maxRetries: 10, + })), + }); + } + + async findById(id: bigint): Promise { + const record = await this.prisma.outboxEvent.findUnique({ + where: { id }, + }); + return record ? this.mapToOutboxEvent(record) : null; + } + + async findPendingEvents(limit: number = 100): Promise { + const now = new Date(); + const records = await this.prisma.outboxEvent.findMany({ + where: { + status: OutboxEventStatus.PENDING, + OR: [ + { nextRetryAt: null }, + { nextRetryAt: { lte: now } }, + ], + }, + orderBy: { createdAt: 'asc' }, + take: limit, + }); + return records.map((r) => this.mapToOutboxEvent(r)); + } + + async findUnackedEvents(timeoutSeconds: number, limit: number = 100): Promise { + const cutoff = new Date(Date.now() - timeoutSeconds * 1000); + const records = await this.prisma.outboxEvent.findMany({ + where: { + status: OutboxEventStatus.SENT, + sentAt: { lte: cutoff }, + }, + orderBy: { sentAt: 'asc' }, + take: limit, + }); + return records.map((r) => this.mapToOutboxEvent(r)); + } + + async markAsSent(id: bigint): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxEventStatus.SENT, + sentAt: new Date(), + }, + }); + } + + async markAsAcked(id: bigint): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxEventStatus.ACKED, + ackedAt: new Date(), + }, + }); + } + + async markAsAckedByAggregateId( + aggregateType: string, + aggregateId: string, + eventType: string, + ): Promise { + const result = await this.prisma.outboxEvent.updateMany({ + where: { + aggregateType, + aggregateId, + eventType, + status: OutboxEventStatus.SENT, + }, + data: { + status: OutboxEventStatus.ACKED, + ackedAt: new Date(), + }, + }); + this.logger.debug( + `Marked ${result.count} events as ACKED for ${aggregateType}:${aggregateId}:${eventType}`, + ); + } + + async recordFailure(id: bigint, error: string): Promise { + const event = await this.prisma.outboxEvent.findUnique({ + where: { id }, + }); + + if (!event) return; + + const newRetryCount = event.retryCount + 1; + // 指数退避: 2^retryCount 秒,最大 5 分钟 + const backoffSeconds = Math.min(Math.pow(2, newRetryCount), 300); + const nextRetryAt = new Date(Date.now() + backoffSeconds * 1000); + + if (newRetryCount >= event.maxRetries) { + await this.markAsFailed(id, error); + } else { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxEventStatus.PENDING, + retryCount: newRetryCount, + lastError: error, + nextRetryAt, + }, + }); + } + } + + async markAsFailed(id: bigint, error: string): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxEventStatus.FAILED, + lastError: error, + }, + }); + this.logger.warn(`Event ${id} marked as FAILED: ${error}`); + } + + async resetToPending(id: bigint): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxEventStatus.PENDING, + retryCount: 0, + lastError: null, + nextRetryAt: null, + sentAt: null, + ackedAt: null, + }, + }); + } + + async cleanupAckedEvents(olderThanDays: number): Promise { + const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000); + const result = await this.prisma.outboxEvent.deleteMany({ + where: { + status: OutboxEventStatus.ACKED, + ackedAt: { lte: cutoff }, + }, + }); + this.logger.log(`Cleaned up ${result.count} old ACKED events`); + return result.count; + } + + private mapToOutboxEvent(record: { + id: bigint; + eventType: string; + aggregateId: string; + aggregateType: string; + payload: unknown; + status: string; + retryCount: number; + maxRetries: number; + lastError: string | null; + nextRetryAt: Date | null; + createdAt: Date; + sentAt: Date | null; + ackedAt: Date | null; + }): OutboxEvent { + return { + id: record.id, + eventType: record.eventType, + aggregateId: record.aggregateId, + aggregateType: record.aggregateType, + payload: record.payload as Record, + status: record.status as OutboxEventStatus, + retryCount: record.retryCount, + maxRetries: record.maxRetries, + lastError: record.lastError, + nextRetryAt: record.nextRetryAt, + createdAt: record.createdAt, + sentAt: record.sentAt, + ackedAt: record.ackedAt, + }; + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/transaction-request.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/transaction-request.repository.impl.ts new file mode 100644 index 00000000..ac8e15e1 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/transaction-request.repository.impl.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { TransactionRequestMapper } from '../mappers/transaction-request.mapper'; +import { ITransactionRequestRepository } from '@/domain/repositories/transaction-request.repository.interface'; +import { TransactionRequest } from '@/domain/aggregates/transaction-request'; +import { ChainType, TxHash } from '@/domain/value-objects'; +import { TransactionStatus } from '@/domain/enums'; + +@Injectable() +export class TransactionRequestRepositoryImpl implements ITransactionRequestRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(request: TransactionRequest): Promise { + const data = TransactionRequestMapper.toPersistence(request); + + if (request.id) { + const updated = await this.prisma.transactionRequest.update({ + where: { id: request.id }, + data, + }); + return TransactionRequestMapper.toDomain(updated); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _, ...createData } = data; + const created = await this.prisma.transactionRequest.create({ + data: createData, + }); + return TransactionRequestMapper.toDomain(created); + } + } + + async findById(id: bigint): Promise { + const record = await this.prisma.transactionRequest.findUnique({ + where: { id }, + }); + return record ? TransactionRequestMapper.toDomain(record) : null; + } + + async findByTxHash(txHash: TxHash): Promise { + const record = await this.prisma.transactionRequest.findFirst({ + where: { txHash: txHash.toString() }, + }); + return record ? TransactionRequestMapper.toDomain(record) : null; + } + + async findBySource( + sourceService: string, + sourceOrderId: string, + ): Promise { + const record = await this.prisma.transactionRequest.findUnique({ + where: { + uk_source_order: { + sourceService, + sourceOrderId, + }, + }, + }); + return record ? TransactionRequestMapper.toDomain(record) : null; + } + + async findPending(chainType: ChainType): Promise { + const records = await this.prisma.transactionRequest.findMany({ + where: { + chainType: chainType.toString(), + status: TransactionStatus.PENDING, + }, + orderBy: { createdAt: 'asc' }, + }); + return records.map(TransactionRequestMapper.toDomain); + } + + async findBroadcasted(chainType: ChainType): Promise { + const records = await this.prisma.transactionRequest.findMany({ + where: { + chainType: chainType.toString(), + status: TransactionStatus.BROADCASTED, + }, + orderBy: { createdAt: 'asc' }, + }); + return records.map(TransactionRequestMapper.toDomain); + } + + async findRetryable(chainType: ChainType, maxRetries: number): Promise { + const records = await this.prisma.transactionRequest.findMany({ + where: { + chainType: chainType.toString(), + status: TransactionStatus.FAILED, + retryCount: { lt: maxRetries }, + }, + orderBy: { createdAt: 'asc' }, + }); + return records.map(TransactionRequestMapper.toDomain); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/redis/address-cache.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/redis/address-cache.service.ts new file mode 100644 index 00000000..d2d7dccd --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/redis/address-cache.service.ts @@ -0,0 +1,92 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { RedisService } from './redis.service'; +import { ChainType } from '@/domain/value-objects'; + +/** + * 地址缓存服务 + * 缓存需要监控的地址集合,用于快速判断是否需要处理某个交易 + */ +@Injectable() +export class AddressCacheService implements OnModuleInit { + private readonly logger = new Logger(AddressCacheService.name); + private readonly CACHE_KEY_PREFIX = 'blockchain:monitored_addresses:'; + + constructor(private readonly redis: RedisService) {} + + async onModuleInit() { + this.logger.log('AddressCacheService initialized'); + } + + private getCacheKey(chainType: ChainType): string { + return `${this.CACHE_KEY_PREFIX}${chainType.toString().toLowerCase()}`; + } + + /** + * 检查地址是否在监控列表中 + */ + async isMonitored(chainType: ChainType, address: string): Promise { + const key = this.getCacheKey(chainType); + return this.redis.sismember(key, address.toLowerCase()); + } + + /** + * 添加地址到监控列表 + */ + async addAddress(chainType: ChainType, address: string): Promise { + const key = this.getCacheKey(chainType); + await this.redis.sadd(key, address.toLowerCase()); + this.logger.debug(`Added address to cache: ${chainType} - ${address}`); + } + + /** + * 从监控列表移除地址 + */ + async removeAddress(chainType: ChainType, address: string): Promise { + const key = this.getCacheKey(chainType); + await this.redis.srem(key, address.toLowerCase()); + this.logger.debug(`Removed address from cache: ${chainType} - ${address}`); + } + + /** + * 批量添加地址 + */ + async addAddresses(chainType: ChainType, addresses: string[]): Promise { + if (addresses.length === 0) return; + const key = this.getCacheKey(chainType); + const lowercased = addresses.map((a) => a.toLowerCase()); + await this.redis.sadd(key, ...lowercased); + this.logger.debug(`Added ${addresses.length} addresses to cache: ${chainType}`); + } + + /** + * 获取所有监控地址 + */ + async getAllAddresses(chainType: ChainType): Promise { + const key = this.getCacheKey(chainType); + return this.redis.smembers(key); + } + + /** + * 获取监控地址数量 + */ + async getCount(chainType: ChainType): Promise { + const key = this.getCacheKey(chainType); + const client = this.redis.getClient(); + return client.scard(key); + } + + /** + * 重新加载缓存(从数据库) + */ + async reloadCache(chainType: ChainType, addresses: string[]): Promise { + const key = this.getCacheKey(chainType); + // 先删除旧缓存 + await this.redis.del(key); + // 重新添加 + if (addresses.length > 0) { + const lowercased = addresses.map((a) => a.toLowerCase()); + await this.redis.sadd(key, ...lowercased); + } + this.logger.log(`Reloaded cache for ${chainType}: ${addresses.length} addresses`); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/redis/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/redis/index.ts new file mode 100644 index 00000000..a0236eb2 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/redis/index.ts @@ -0,0 +1,2 @@ +export * from './redis.service'; +export * from './address-cache.service'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/redis/redis.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/redis/redis.service.ts new file mode 100644 index 00000000..c54624e7 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/redis/redis.service.ts @@ -0,0 +1,67 @@ +import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService implements OnModuleDestroy { + private readonly logger = new Logger(RedisService.name); + private readonly client: Redis; + + constructor(private readonly configService: ConfigService) { + this.client = new Redis({ + host: this.configService.get('redis.host'), + port: this.configService.get('redis.port'), + db: this.configService.get('redis.db'), + password: this.configService.get('redis.password') || undefined, + }); + + this.client.on('connect', () => { + this.logger.log('Redis connected'); + }); + + this.client.on('error', (err) => { + this.logger.error('Redis error', err); + }); + } + + onModuleDestroy() { + this.client.disconnect(); + } + + getClient(): Redis { + return this.client; + } + + async get(key: string): Promise { + return this.client.get(key); + } + + async set(key: string, value: string, ttlSeconds?: number): Promise { + if (ttlSeconds) { + await this.client.set(key, value, 'EX', ttlSeconds); + } else { + await this.client.set(key, value); + } + } + + async del(key: string): Promise { + await this.client.del(key); + } + + async sismember(key: string, member: string): Promise { + const result = await this.client.sismember(key, member); + return result === 1; + } + + async sadd(key: string, ...members: string[]): Promise { + return this.client.sadd(key, ...members); + } + + async srem(key: string, ...members: string[]): Promise { + return this.client.srem(key, ...members); + } + + async smembers(key: string): Promise { + return this.client.smembers(key); + } +} diff --git a/backend/services/mining-blockchain-service/src/main.ts b/backend/services/mining-blockchain-service/src/main.ts new file mode 100644 index 00000000..cf903db4 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/main.ts @@ -0,0 +1,72 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const logger = new Logger('Bootstrap'); + + const app = await NestFactory.create(AppModule); + + const configService = app.get(ConfigService); + + // 全局验证管道 + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + // CORS + app.enableCors(); + + // 全局 API 前缀 + const apiPrefix = configService.get('app.apiPrefix', 'api/v1'); + app.setGlobalPrefix(apiPrefix); + + // Swagger 文档 + const swaggerConfig = new DocumentBuilder() + .setTitle('Mining Blockchain Service API') + .setDescription('Mining 区块链服务 - C2C Bot dUSDT 转账') + .setVersion('1.0') + .addTag('Health', '健康检查') + .addTag('Transfer', 'dUSDT 转账') + .build(); + + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api', app, document); + + // Kafka 微服务(用于 MPC 签名通信) + const kafkaBrokers = configService.get('kafka.brokers') || ['localhost:9092']; + const kafkaGroupId = configService.get('kafka.groupId') || 'mining-blockchain-service-group'; + + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: 'mining-blockchain-service', + brokers: kafkaBrokers, + }, + consumer: { + groupId: kafkaGroupId, + }, + }, + }); + + // 启动微服务 + await app.startAllMicroservices(); + logger.log('Kafka microservice started for MPC signing'); + + // 启动 HTTP 服务 + const port = configService.get('app.port', 3020); + await app.listen(port); + + logger.log(`Mining Blockchain service is running on port ${port}`); + logger.log(`Swagger docs available at http://localhost:${port}/api`); +} + +bootstrap(); diff --git a/backend/services/mining-blockchain-service/src/shared/decorators/index.ts b/backend/services/mining-blockchain-service/src/shared/decorators/index.ts new file mode 100644 index 00000000..3f75d993 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/decorators/index.ts @@ -0,0 +1 @@ +export * from './public.decorator'; diff --git a/backend/services/mining-blockchain-service/src/shared/decorators/public.decorator.ts b/backend/services/mining-blockchain-service/src/shared/decorators/public.decorator.ts new file mode 100644 index 00000000..b3845e12 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/backend/services/mining-blockchain-service/src/shared/exceptions/blockchain.exception.ts b/backend/services/mining-blockchain-service/src/shared/exceptions/blockchain.exception.ts new file mode 100644 index 00000000..c7fd9dd0 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/exceptions/blockchain.exception.ts @@ -0,0 +1,39 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class BlockchainException extends HttpException { + constructor( + message: string, + public readonly chainType?: string, + public readonly txHash?: string, + status: HttpStatus = HttpStatus.SERVICE_UNAVAILABLE, + ) { + super( + { + statusCode: status, + error: 'Blockchain Error', + message, + chainType, + txHash, + }, + status, + ); + } +} + +export class RpcConnectionException extends BlockchainException { + constructor(chainType: string, message: string = 'RPC connection failed') { + super(message, chainType, undefined, HttpStatus.SERVICE_UNAVAILABLE); + } +} + +export class TransactionFailedException extends BlockchainException { + constructor(chainType: string, txHash: string, message: string = 'Transaction failed') { + super(message, chainType, txHash, HttpStatus.UNPROCESSABLE_ENTITY); + } +} + +export class InvalidAddressException extends BlockchainException { + constructor(address: string) { + super(`Invalid address: ${address}`, undefined, undefined, HttpStatus.BAD_REQUEST); + } +} diff --git a/backend/services/mining-blockchain-service/src/shared/exceptions/domain.exception.ts b/backend/services/mining-blockchain-service/src/shared/exceptions/domain.exception.ts new file mode 100644 index 00000000..fecd2bcc --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/exceptions/domain.exception.ts @@ -0,0 +1,26 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class DomainException extends HttpException { + constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { + super( + { + statusCode: status, + error: 'Domain Error', + message, + }, + status, + ); + } +} + +export class EntityNotFoundException extends DomainException { + constructor(entity: string, id: string | bigint) { + super(`${entity} with id ${id} not found`, HttpStatus.NOT_FOUND); + } +} + +export class InvalidOperationException extends DomainException { + constructor(message: string) { + super(message, HttpStatus.UNPROCESSABLE_ENTITY); + } +} diff --git a/backend/services/mining-blockchain-service/src/shared/exceptions/index.ts b/backend/services/mining-blockchain-service/src/shared/exceptions/index.ts new file mode 100644 index 00000000..ae49eda1 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from './domain.exception'; +export * from './blockchain.exception'; diff --git a/backend/services/mining-blockchain-service/src/shared/filters/global-exception.filter.ts b/backend/services/mining-blockchain-service/src/shared/filters/global-exception.filter.ts new file mode 100644 index 00000000..e62a72b2 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/filters/global-exception.filter.ts @@ -0,0 +1,45 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + let error = 'Internal Server Error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + + if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { + message = ((exceptionResponse as Record).message as string) || message; + error = ((exceptionResponse as Record).error as string) || error; + } else { + message = exceptionResponse as string; + } + } else if (exception instanceof Error) { + message = exception.message; + this.logger.error(`Unhandled exception: ${exception.message}`, exception.stack); + } + + response.status(status).json({ + statusCode: status, + error, + message, + timestamp: new Date().toISOString(), + }); + } +} diff --git a/backend/services/mining-blockchain-service/src/shared/filters/index.ts b/backend/services/mining-blockchain-service/src/shared/filters/index.ts new file mode 100644 index 00000000..c3ec44dc --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/filters/index.ts @@ -0,0 +1 @@ +export * from './global-exception.filter'; diff --git a/backend/services/mining-blockchain-service/src/shared/guards/jwt-auth.guard.ts b/backend/services/mining-blockchain-service/src/shared/guards/jwt-auth.guard.ts new file mode 100644 index 00000000..20797af4 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/guards/jwt-auth.guard.ts @@ -0,0 +1,22 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_KEY } from '@/shared/decorators'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + return super.canActivate(context); + } +} diff --git a/backend/services/mining-blockchain-service/src/shared/index.ts b/backend/services/mining-blockchain-service/src/shared/index.ts new file mode 100644 index 00000000..1951c69d --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/index.ts @@ -0,0 +1,3 @@ +export * from './exceptions'; +export * from './filters'; +export * from './decorators'; diff --git a/backend/services/mining-blockchain-service/src/shared/strategies/jwt.strategy.ts b/backend/services/mining-blockchain-service/src/shared/strategies/jwt.strategy.ts new file mode 100644 index 00000000..1bd12a12 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/shared/strategies/jwt.strategy.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; + +interface JwtPayload { + userId: string; + accountSequence: string; + deviceId: string; + type: 'access' | 'refresh'; + iat: number; + exp: number; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET') || 'default-secret', + }); + } + + async validate(payload: JwtPayload) { + return { + userId: payload.userId, + accountSequence: payload.accountSequence, + deviceId: payload.deviceId, + }; + } +} diff --git a/backend/services/mining-blockchain-service/tsconfig.json b/backend/services/mining-blockchain-service/tsconfig.json new file mode 100644 index 00000000..bd3c3946 --- /dev/null +++ b/backend/services/mining-blockchain-service/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/backend/services/trading-service/package-lock.json b/backend/services/trading-service/package-lock.json index 6acc7b0b..7bba6cc5 100644 --- a/backend/services/trading-service/package-lock.json +++ b/backend/services/trading-service/package-lock.json @@ -8,6 +8,7 @@ "name": "trading-service", "version": "1.0.0", "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", @@ -1600,6 +1601,17 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -3208,6 +3220,24 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -3922,6 +3952,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4207,6 +4249,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4474,6 +4525,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5146,6 +5212,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -5216,6 +5302,22 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5563,6 +5665,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7929,6 +8046,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/services/trading-service/package.json b/backend/services/trading-service/package.json index 538495d4..94426002 100644 --- a/backend/services/trading-service/package.json +++ b/backend/services/trading-service/package.json @@ -22,6 +22,7 @@ "seed": "ts-node prisma/seed.ts" }, "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", diff --git a/backend/services/trading-service/prisma/schema.prisma b/backend/services/trading-service/prisma/schema.prisma index 0064e356..ed50994a 100644 --- a/backend/services/trading-service/prisma/schema.prisma +++ b/backend/services/trading-service/prisma/schema.prisma @@ -676,6 +676,14 @@ model C2cOrder { paymentDeadline DateTime? @map("payment_deadline") // 付款截止时间 confirmDeadline DateTime? @map("confirm_deadline") // 确认收款截止时间 + // ============ Bot 自动购买相关 ============ + // 卖家 Kava 地址(从 SyncedUser 表获取,用于接收 dUSDT) + sellerKavaAddress String? @map("seller_kava_address") + // 是否被 Bot 购买 + botPurchased Boolean @default(false) @map("bot_purchased") + // dUSDT 支付交易哈希 + paymentTxHash String? @map("payment_tx_hash") + // 时间戳 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -692,5 +700,6 @@ model C2cOrder { @@index([createdAt(sort: Desc)]) @@index([paymentDeadline]) @@index([confirmDeadline]) + @@index([botPurchased]) @@map("c2c_orders") } diff --git a/backend/services/trading-service/src/application/application.module.ts b/backend/services/trading-service/src/application/application.module.ts index f7277053..fa58d42b 100644 --- a/backend/services/trading-service/src/application/application.module.ts +++ b/backend/services/trading-service/src/application/application.module.ts @@ -10,10 +10,12 @@ import { BurnService } from './services/burn.service'; import { AssetService } from './services/asset.service'; import { MarketMakerService } from './services/market-maker.service'; import { C2cService } from './services/c2c.service'; +import { C2cBotService } from './services/c2c-bot.service'; import { OutboxScheduler } from './schedulers/outbox.scheduler'; import { BurnScheduler } from './schedulers/burn.scheduler'; import { PriceBroadcastScheduler } from './schedulers/price-broadcast.scheduler'; import { C2cExpiryScheduler } from './schedulers/c2c-expiry.scheduler'; +import { C2cBotScheduler } from './schedulers/c2c-bot.scheduler'; @Module({ imports: [ @@ -31,12 +33,14 @@ import { C2cExpiryScheduler } from './schedulers/c2c-expiry.scheduler'; P2pTransferService, MarketMakerService, C2cService, + C2cBotService, // Schedulers OutboxScheduler, BurnScheduler, PriceBroadcastScheduler, C2cExpiryScheduler, + C2cBotScheduler, ], - exports: [OrderService, TransferService, P2pTransferService, PriceService, BurnService, AssetService, MarketMakerService, C2cService], + exports: [OrderService, TransferService, P2pTransferService, PriceService, BurnService, AssetService, MarketMakerService, C2cService, C2cBotService], }) export class ApplicationModule {} diff --git a/backend/services/trading-service/src/application/schedulers/c2c-bot.scheduler.ts b/backend/services/trading-service/src/application/schedulers/c2c-bot.scheduler.ts new file mode 100644 index 00000000..3d913e2b --- /dev/null +++ b/backend/services/trading-service/src/application/schedulers/c2c-bot.scheduler.ts @@ -0,0 +1,117 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { ConfigService } from '@nestjs/config'; +import { C2cOrderRepository } from '../../infrastructure/persistence/repositories/c2c-order.repository'; +import { C2cBotService } from '../services/c2c-bot.service'; +import { RedisService } from '../../infrastructure/redis/redis.service'; + +/** + * C2C Bot 定时任务 + * 定期扫描待处理的卖单并自动购买 + */ +@Injectable() +export class C2cBotScheduler implements OnModuleInit { + private readonly logger = new Logger(C2cBotScheduler.name); + private readonly LOCK_KEY = 'c2c:bot:scheduler:lock'; + private readonly enabled: boolean; + + constructor( + private readonly c2cOrderRepository: C2cOrderRepository, + private readonly c2cBotService: C2cBotService, + private readonly redis: RedisService, + private readonly configService: ConfigService, + ) { + this.enabled = this.configService.get('C2C_BOT_ENABLED', false); + } + + async onModuleInit() { + this.logger.log(`C2C Bot Scheduler initialized, enabled: ${this.enabled}`); + + if (this.enabled) { + const isAvailable = await this.c2cBotService.isAvailable(); + if (isAvailable) { + const balance = await this.c2cBotService.getHotWalletBalance(); + this.logger.log(`Hot wallet balance: ${balance} dUSDT`); + } else { + this.logger.warn('Mining blockchain service not available, Bot will not process orders'); + } + } + } + + /** + * 每10秒扫描待处理的卖单 + */ + @Cron('*/10 * * * * *') + async processPendingSellOrders(): Promise { + if (!this.enabled) { + return; + } + + const lockValue = await this.redis.acquireLock(this.LOCK_KEY, 8); // 8秒锁 + if (!lockValue) { + return; // 其他实例正在处理 + } + + try { + // 查询待处理的卖单 + const orders = await this.c2cOrderRepository.findPendingSellOrdersForBot(10); + + if (orders.length === 0) { + return; + } + + this.logger.log(`[SCHEDULER] Found ${orders.length} pending sell orders`); + + let successCount = 0; + let errorCount = 0; + + // 逐个处理 + for (const order of orders) { + try { + const success = await this.c2cBotService.purchaseOrder(order); + if (success) { + successCount++; + } else { + errorCount++; + } + } catch (error: any) { + this.logger.error(`[SCHEDULER] Error processing order ${order.orderNo}: ${error.message}`); + errorCount++; + } + } + + if (successCount > 0 || errorCount > 0) { + this.logger.log(`[SCHEDULER] Processed: ${successCount} success, ${errorCount} errors`); + } + } catch (error: any) { + this.logger.error(`[SCHEDULER] Error in processPendingSellOrders: ${error.message}`); + } finally { + await this.redis.releaseLock(this.LOCK_KEY, lockValue); + } + } + + /** + * 每分钟检查热钱包余额 + */ + @Cron('0 * * * * *') + async checkHotWalletBalance(): Promise { + if (!this.enabled) { + return; + } + + try { + const balance = await this.c2cBotService.getHotWalletBalance(); + if (balance) { + // 如果余额低于阈值,记录警告 + const threshold = this.configService.get('C2C_BOT_BALANCE_THRESHOLD', 1000); + const balanceNum = parseFloat(balance); + + if (balanceNum < threshold) { + this.logger.warn(`[SCHEDULER] Hot wallet balance (${balance}) is below threshold (${threshold})`); + } + } + } catch (error: any) { + this.logger.error(`[SCHEDULER] Error checking hot wallet balance: ${error.message}`); + } + } +} diff --git a/backend/services/trading-service/src/application/services/c2c-bot.service.ts b/backend/services/trading-service/src/application/services/c2c-bot.service.ts new file mode 100644 index 00000000..6d9dc06a --- /dev/null +++ b/backend/services/trading-service/src/application/services/c2c-bot.service.ts @@ -0,0 +1,122 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { C2cOrderRepository, C2cOrderEntity } from '../../infrastructure/persistence/repositories/c2c-order.repository'; +import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository'; +import { BlockchainClient } from '../../infrastructure/blockchain/blockchain.client'; +import { IdentityClient } from '../../infrastructure/identity/identity.client'; +import { RedisService } from '../../infrastructure/redis/redis.service'; +import Decimal from 'decimal.js'; + +/** + * C2C Bot 服务 + * 自动购买用户发布的卖单,并通过 dUSDT 支付 + */ +@Injectable() +export class C2cBotService { + private readonly logger = new Logger(C2cBotService.name); + + constructor( + private readonly c2cOrderRepository: C2cOrderRepository, + private readonly tradingAccountRepository: TradingAccountRepository, + private readonly blockchainClient: BlockchainClient, + private readonly identityClient: IdentityClient, + private readonly redis: RedisService, + ) {} + + /** + * 处理单个卖单 + * @param order 卖单 + */ + async purchaseOrder(order: C2cOrderEntity): Promise { + const lockKey = `c2c:bot:order:${order.orderNo}`; + const lockValue = await this.redis.acquireLock(lockKey, 60); // 60秒锁 + if (!lockValue) { + this.logger.debug(`Order ${order.orderNo} is being processed by another instance`); + return false; + } + + try { + this.logger.log(`[BOT] Processing order ${order.orderNo}`); + this.logger.log(`[BOT] Seller: ${order.makerAccountSequence}`); + this.logger.log(`[BOT] Amount: ${order.totalAmount}`); + + // 1. 获取卖家的 Kava 地址 + const kavaAddress = await this.identityClient.getUserKavaAddress(order.makerAccountSequence); + if (!kavaAddress) { + this.logger.error(`[BOT] Seller ${order.makerAccountSequence} has no Kava address`); + return false; + } + this.logger.log(`[BOT] Seller Kava address: ${kavaAddress}`); + + // 2. 计算 dUSDT 支付金额(积分值 = dUSDT,1:1 兑换) + const paymentAmount = order.totalAmount; + this.logger.log(`[BOT] Payment amount: ${paymentAmount} dUSDT`); + + // 3. 调用 mining-blockchain-service 转账 dUSDT + const transferResult = await this.blockchainClient.transferDusdt(kavaAddress, paymentAmount); + + if (!transferResult.success) { + this.logger.error(`[BOT] Transfer failed: ${transferResult.error}`); + return false; + } + + this.logger.log(`[BOT] Transfer successful: txHash=${transferResult.txHash}`); + + // 4. 更新订单状态 + await this.c2cOrderRepository.updateBotPurchase(order.orderNo, { + sellerKavaAddress: kavaAddress, + paymentTxHash: transferResult.txHash!, + }); + + // 5. 扣减卖家的积分值余额 + await this.deductSellerBalance(order.makerAccountSequence, order.totalAmount); + + this.logger.log(`[BOT] Order ${order.orderNo} completed successfully`); + return true; + } catch (error: any) { + this.logger.error(`[BOT] Error processing order ${order.orderNo}: ${error.message}`); + return false; + } finally { + await this.redis.releaseLock(lockKey, lockValue); + } + } + + /** + * 扣减卖家的积分值余额 + */ + private async deductSellerBalance(accountSequence: string, amount: string): Promise { + const account = await this.tradingAccountRepository.findByAccountSequence(accountSequence); + if (!account) { + throw new Error(`Trading account ${accountSequence} not found`); + } + + const amountDecimal = new Decimal(amount); + const currentBalance = new Decimal(account.cashBalance.value); + + if (currentBalance.lessThan(amountDecimal)) { + throw new Error(`Insufficient cash balance for ${accountSequence}`); + } + + // 扣减余额 + await this.tradingAccountRepository.updateCashBalance( + accountSequence, + amountDecimal.negated().toString(), + ); + + this.logger.log(`[BOT] Deducted ${amount} from ${accountSequence}'s cash balance`); + } + + /** + * 检查 Bot 服务是否可用 + */ + async isAvailable(): Promise { + return await this.blockchainClient.isAvailable(); + } + + /** + * 获取热钱包余额 + */ + async getHotWalletBalance(): Promise { + const balance = await this.blockchainClient.getHotWalletBalance(); + return balance?.balance || null; + } +} diff --git a/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts b/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts new file mode 100644 index 00000000..3e84886b --- /dev/null +++ b/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts @@ -0,0 +1,111 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse } from 'axios'; + +export interface TransferResult { + success: boolean; + txHash?: string; + error?: string; + gasUsed?: string; + blockNumber?: number; +} + +export interface BalanceResult { + address: string; + balance: string; + chain: string; +} + +export interface StatusResult { + configured: boolean; + hotWalletAddress: string | null; +} + +/** + * 区块链客户端 + * 用于调用 mining-blockchain-service 进行 dUSDT 转账 + */ +@Injectable() +export class BlockchainClient { + private readonly logger = new Logger(BlockchainClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'MINING_BLOCKCHAIN_SERVICE_URL', + 'http://localhost:3020', + ); + this.logger.log(`[INIT] BlockchainClient initialized with URL: ${this.baseUrl}`); + } + + /** + * 转账 dUSDT 到指定地址 + * @param toAddress 接收地址 + * @param amount 金额(人类可读格式) + */ + async transferDusdt(toAddress: string, amount: string): Promise { + this.logger.log(`[TRANSFER] Calling mining-blockchain-service: to=${toAddress}, amount=${amount}`); + + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.post(`${this.baseUrl}/api/v1/transfer/dusdt`, { + toAddress, + amount, + }), + ); + + this.logger.log(`[TRANSFER] Response: ${JSON.stringify(response.data)}`); + return response.data; + } catch (error: any) { + const errorMessage = error.response?.data?.message || error.message || 'Unknown error'; + this.logger.error(`[TRANSFER] Failed: ${errorMessage}`); + return { + success: false, + error: errorMessage, + }; + } + } + + /** + * 查询热钱包 dUSDT 余额 + */ + async getHotWalletBalance(): Promise { + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/api/v1/transfer/dusdt/balance`), + ); + return response.data; + } catch (error: any) { + this.logger.error(`[BALANCE] Failed to get balance: ${error.message}`); + return null; + } + } + + /** + * 检查转账服务状态 + */ + async getStatus(): Promise { + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/api/v1/transfer/status`), + ); + return response.data; + } catch (error: any) { + this.logger.error(`[STATUS] Failed to get status: ${error.message}`); + return null; + } + } + + /** + * 检查服务是否可用 + */ + async isAvailable(): Promise { + const status = await this.getStatus(); + return status?.configured ?? false; + } +} diff --git a/backend/services/trading-service/src/infrastructure/identity/identity.client.ts b/backend/services/trading-service/src/infrastructure/identity/identity.client.ts new file mode 100644 index 00000000..59e99123 --- /dev/null +++ b/backend/services/trading-service/src/infrastructure/identity/identity.client.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse } from 'axios'; + +export interface UserInfo { + accountSequence: string; + kavaAddress?: string; + phone?: string; + nickname?: string; +} + +/** + * Identity 服务客户端 + * 用于获取用户信息,包括 Kava 地址 + */ +@Injectable() +export class IdentityClient { + private readonly logger = new Logger(IdentityClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'IDENTITY_SERVICE_URL', + 'http://localhost:3001', + ); + this.logger.log(`[INIT] IdentityClient initialized with URL: ${this.baseUrl}`); + } + + /** + * 获取用户的 Kava 地址 + * @param accountSequence 用户账户序列号 + */ + async getUserKavaAddress(accountSequence: string): Promise { + try { + const response: AxiosResponse<{ kavaAddress?: string }> = await firstValueFrom( + this.httpService.get<{ kavaAddress?: string }>( + `${this.baseUrl}/api/v1/internal/user/${accountSequence}/kava-address`, + ), + ); + + return response.data?.kavaAddress || null; + } catch (error: any) { + this.logger.error(`Failed to get Kava address for ${accountSequence}: ${error.message}`); + return null; + } + } + + /** + * 获取用户信息 + * @param accountSequence 用户账户序列号 + */ + async getUserInfo(accountSequence: string): Promise { + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.get( + `${this.baseUrl}/api/v1/internal/user/${accountSequence}`, + ), + ); + + return response.data; + } catch (error: any) { + this.logger.error(`Failed to get user info for ${accountSequence}: ${error.message}`); + return null; + } + } +} diff --git a/backend/services/trading-service/src/infrastructure/infrastructure.module.ts b/backend/services/trading-service/src/infrastructure/infrastructure.module.ts index fdda47d6..cfa86128 100644 --- a/backend/services/trading-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/trading-service/src/infrastructure/infrastructure.module.ts @@ -1,5 +1,6 @@ import { Module, Global } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { PrismaModule } from './persistence/prisma/prisma.module'; import { TradingAccountRepository } from './persistence/repositories/trading-account.repository'; @@ -16,11 +17,14 @@ import { RedisService } from './redis/redis.service'; import { KafkaProducerService } from './kafka/kafka-producer.service'; import { UserRegisteredConsumer } from './kafka/consumers/user-registered.consumer'; import { CdcConsumerService } from './kafka/cdc-consumer.service'; +import { BlockchainClient } from './blockchain/blockchain.client'; +import { IdentityClient } from './identity/identity.client'; @Global() @Module({ imports: [ PrismaModule, + HttpModule, ClientsModule.registerAsync([ { name: 'KAFKA_CLIENT', @@ -55,6 +59,8 @@ import { CdcConsumerService } from './kafka/cdc-consumer.service'; C2cOrderRepository, KafkaProducerService, CdcConsumerService, + BlockchainClient, + IdentityClient, { provide: 'REDIS_OPTIONS', useFactory: (configService: ConfigService) => ({ @@ -80,6 +86,8 @@ import { CdcConsumerService } from './kafka/cdc-consumer.service'; C2cOrderRepository, KafkaProducerService, RedisService, + BlockchainClient, + IdentityClient, ClientsModule, ], }) diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/c2c-order.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/c2c-order.repository.ts index d09224b0..9cd4dec3 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/c2c-order.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/c2c-order.repository.ts @@ -56,6 +56,10 @@ export interface C2cOrderEntity { confirmTimeoutMinutes: number; paymentDeadline?: Date | null; confirmDeadline?: Date | null; + // Bot 自动购买相关 + sellerKavaAddress?: string | null; + botPurchased: boolean; + paymentTxHash?: string | null; // 其他 remark?: string | null; createdAt: Date; @@ -280,6 +284,47 @@ export class C2cOrderRepository { return records.map((r: any) => this.toEntity(r)); } + /** + * 查询待处理的卖单(供 Bot 自动购买) + * 只返回未被 Bot 购买的 PENDING 状态卖单 + */ + async findPendingSellOrdersForBot(limit: number): Promise { + const records = await this.prisma.c2cOrder.findMany({ + where: { + type: C2C_ORDER_TYPE.SELL as any, + status: C2C_ORDER_STATUS.PENDING as any, + botPurchased: false, + }, + orderBy: { createdAt: 'asc' }, // 先进先出 + take: limit, + }); + return records.map((r: any) => this.toEntity(r)); + } + + /** + * 更新订单为 Bot 已购买状态 + */ + async updateBotPurchase( + orderNo: string, + data: { + sellerKavaAddress: string; + paymentTxHash: string; + }, + ): Promise { + const record = await this.prisma.c2cOrder.update({ + where: { orderNo }, + data: { + botPurchased: true, + sellerKavaAddress: data.sellerKavaAddress, + paymentTxHash: data.paymentTxHash, + status: C2C_ORDER_STATUS.COMPLETED as any, + completedAt: new Date(), + paidAt: new Date(), + }, + }); + return this.toEntity(record); + } + /** * 将Prisma记录转为实体 */ @@ -312,6 +357,10 @@ export class C2cOrderRepository { confirmTimeoutMinutes: record.confirmTimeoutMinutes, paymentDeadline: record.paymentDeadline, confirmDeadline: record.confirmDeadline, + // Bot 自动购买相关 + sellerKavaAddress: record.sellerKavaAddress, + botPurchased: record.botPurchased, + paymentTxHash: record.paymentTxHash, // 其他 remark: record.remark, createdAt: record.createdAt, diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-account.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-account.repository.ts index 7253c18b..7d8e44f1 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-account.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-account.repository.ts @@ -128,6 +128,30 @@ export class TradingAccountRepository { }); } + /** + * 更新积分值余额(用于 C2C Bot 扣减卖家余额) + * @param accountSequence 账户序列号 + * @param delta 变化量(正数增加,负数减少) + */ + async updateCashBalance(accountSequence: string, delta: string): Promise { + const deltaNum = parseFloat(delta); + if (deltaNum > 0) { + await this.prisma.tradingAccount.update({ + where: { accountSequence }, + data: { + cashBalance: { increment: deltaNum }, + }, + }); + } else if (deltaNum < 0) { + await this.prisma.tradingAccount.update({ + where: { accountSequence }, + data: { + cashBalance: { decrement: Math.abs(deltaNum) }, + }, + }); + } + } + private toDomain(record: any): TradingAccountAggregate { return TradingAccountAggregate.reconstitute({ id: record.id,