diff --git a/backend/services/blockchain-service/contracts/TestUSDT.sol b/backend/services/blockchain-service/contracts/TestUSDT.sol new file mode 100644 index 00000000..2f4136e3 --- /dev/null +++ b/backend/services/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/blockchain-service/contracts/TestUSDT_Flat.sol b/backend/services/blockchain-service/contracts/TestUSDT_Flat.sol new file mode 100644 index 00000000..5ea7ea20 --- /dev/null +++ b/backend/services/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/blockchain-service/package-lock.json b/backend/services/blockchain-service/package-lock.json index ec29cf38..525e9cc9 100644 --- a/backend/services/blockchain-service/package-lock.json +++ b/backend/services/blockchain-service/package-lock.json @@ -49,6 +49,7 @@ "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", @@ -4087,6 +4088,13 @@ "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", @@ -7053,6 +7061,13 @@ "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", @@ -7388,6 +7403,15 @@ "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", @@ -8917,6 +8941,48 @@ "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", diff --git a/backend/services/blockchain-service/package.json b/backend/services/blockchain-service/package.json index ce7c29f7..2bb3006f 100644 --- a/backend/services/blockchain-service/package.json +++ b/backend/services/blockchain-service/package.json @@ -68,6 +68,7 @@ "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", diff --git a/backend/services/blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql b/backend/services/blockchain-service/prisma/migrations/20241208000000_add_system_accounts_and_recovery/migration.sql new file mode 100644 index 00000000..14dd9afb --- /dev/null +++ b/backend/services/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" BIGINT; +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" BIGINT; +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" INTEGER 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/blockchain-service/prisma/schema.prisma b/backend/services/blockchain-service/prisma/schema.prisma index 4fa82cf1..ee4e2a16 100644 --- a/backend/services/blockchain-service/prisma/schema.prisma +++ b/backend/services/blockchain-service/prisma/schema.prisma @@ -12,7 +12,7 @@ datasource db { // ============================================ // 监控地址表 -// 存储需要监听充值的地址 +// 存储需要监听充值的地址(用户地址和系统账户地址) // ============================================ model MonitoredAddress { id BigInt @id @default(autoincrement()) @map("address_id") @@ -20,10 +20,17 @@ model MonitoredAddress { chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC address String @db.VarChar(42) // 0x地址 - // 使用 accountSequence 作为跨服务关联标识 (全局唯一业务ID) - accountSequence BigInt @map("account_sequence") - // 保留 userId 用于兼容,但主要使用 accountSequence - userId BigInt @map("user_id") + // 地址类型: USER (用户钱包) 或 SYSTEM (系统账户) + addressType String @default("USER") @map("address_type") @db.VarChar(20) + + // 用户地址关联 (addressType = USER 时使用) + accountSequence BigInt? @map("account_sequence") // 跨服务关联标识 + 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") // 是否激活监听 @@ -35,7 +42,9 @@ model MonitoredAddress { @@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") } @@ -66,8 +75,15 @@ model DepositTransaction { // 关联 - 使用 accountSequence 作为跨服务主键 addressId BigInt @map("address_id") - accountSequence BigInt @map("account_sequence") // 跨服务关联标识 - userId BigInt @map("user_id") // 保留兼容 + addressType String @default("USER") @map("address_type") @db.VarChar(20) // USER 或 SYSTEM + + // 用户地址关联 + accountSequence BigInt? @map("account_sequence") // 跨服务关联标识 + 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") diff --git a/backend/services/blockchain-service/scripts/deploy-kava-simple.ts b/backend/services/blockchain-service/scripts/deploy-kava-simple.ts new file mode 100644 index 00000000..4e3d4902 --- /dev/null +++ b/backend/services/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/blockchain-service/scripts/deploy-test-usdt-kava.ts b/backend/services/blockchain-service/scripts/deploy-test-usdt-kava.ts new file mode 100644 index 00000000..d1b591d3 --- /dev/null +++ b/backend/services/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/blockchain-service/scripts/deploy-test-usdt.ts b/backend/services/blockchain-service/scripts/deploy-test-usdt.ts new file mode 100644 index 00000000..2bdd1292 --- /dev/null +++ b/backend/services/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/blockchain-service/scripts/generate-wallet.ts b/backend/services/blockchain-service/scripts/generate-wallet.ts new file mode 100644 index 00000000..b0ac3025 --- /dev/null +++ b/backend/services/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/blockchain-service/src/application/application.module.ts b/backend/services/blockchain-service/src/application/application.module.ts index c980b96f..cfc09b94 100644 --- a/backend/services/blockchain-service/src/application/application.module.ts +++ b/backend/services/blockchain-service/src/application/application.module.ts @@ -7,7 +7,7 @@ import { BalanceQueryService, MnemonicVerificationService, } from './services'; -import { MpcKeygenCompletedHandler } from './event-handlers'; +import { MpcKeygenCompletedHandler, WithdrawalRequestedHandler } from './event-handlers'; @Module({ imports: [InfrastructureModule, DomainModule], @@ -20,6 +20,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers'; // 事件处理器 MpcKeygenCompletedHandler, + WithdrawalRequestedHandler, ], exports: [ AddressDerivationService, @@ -27,6 +28,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers'; BalanceQueryService, MnemonicVerificationService, MpcKeygenCompletedHandler, + WithdrawalRequestedHandler, ], }) export class ApplicationModule {} diff --git a/backend/services/blockchain-service/src/application/event-handlers/index.ts b/backend/services/blockchain-service/src/application/event-handlers/index.ts index 92a915c1..7231998d 100644 --- a/backend/services/blockchain-service/src/application/event-handlers/index.ts +++ b/backend/services/blockchain-service/src/application/event-handlers/index.ts @@ -1 +1,2 @@ export * from './mpc-keygen-completed.handler'; +export * from './withdrawal-requested.handler'; diff --git a/backend/services/blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts b/backend/services/blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts new file mode 100644 index 00000000..f03222f5 --- /dev/null +++ b/backend/services/blockchain-service/src/application/event-handlers/withdrawal-requested.handler.ts @@ -0,0 +1,120 @@ +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'; + +/** + * Withdrawal Requested Event Handler + * + * Handles withdrawal requests from wallet-service. + * For now, logs the event and publishes a status update. + * + * Future implementation will: + * 1. Create TransactionRequest record + * 2. Request MPC signing + * 3. Broadcast to blockchain + * 4. Monitor confirmation + */ +@Injectable() +export class WithdrawalRequestedHandler implements OnModuleInit { + private readonly logger = new Logger(WithdrawalRequestedHandler.name); + + constructor( + private readonly withdrawalEventConsumer: WithdrawalEventConsumerService, + private readonly eventPublisher: EventPublisherService, + ) {} + + onModuleInit() { + this.withdrawalEventConsumer.onWithdrawalRequested( + this.handleWithdrawalRequested.bind(this), + ); + this.logger.log(`[INIT] WithdrawalRequestedHandler registered`); + } + + /** + * Handle withdrawal requested event from wallet-service + * + * Current implementation: Log and acknowledge + * TODO: Implement full blockchain transaction flow + */ + private async handleWithdrawalRequested( + payload: WithdrawalRequestedPayload, + ): Promise { + this.logger.log(`[HANDLE] Received WithdrawalRequested event`); + 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 { + // TODO: Full implementation steps: + // 1. Validate the withdrawal request + // 2. Get system hot wallet address for the chain + // 3. Create TransactionRequest record + // 4. Request MPC signing + // 5. After signed, broadcast to blockchain + // 6. Monitor for confirmation + // 7. Publish status updates back to wallet-service + + // For now, just log that we received it + this.logger.log( + `[PROCESS] Withdrawal ${payload.orderNo} received for processing`, + ); + this.logger.log( + `[PROCESS] Chain: ${payload.chainType}, To: ${payload.toAddress}, Amount: ${payload.netAmount} USDT`, + ); + + // Publish acknowledgment event (wallet-service can listen for status updates) + await this.eventPublisher.publish({ + eventType: 'blockchain.withdrawal.received', + toPayload: () => ({ + orderNo: payload.orderNo, + accountSequence: payload.accountSequence, + status: 'RECEIVED', + message: 'Withdrawal request received by blockchain-service', + }), + eventId: `wd-received-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + this.logger.log( + `[COMPLETE] Withdrawal ${payload.orderNo} acknowledged`, + ); + + // NOTE: Actual blockchain transaction implementation would go here + // This would involve: + // - Creating a TransactionRequest aggregate + // - Calling MPC service for signing + // - Broadcasting the signed transaction + // - Monitoring for confirmations + // - Publishing final status (CONFIRMED or FAILED) + + } catch (error) { + this.logger.error( + `[ERROR] Failed to process withdrawal ${payload.orderNo}`, + error, + ); + + // Publish failure event + await this.eventPublisher.publish({ + eventType: 'blockchain.withdrawal.failed', + toPayload: () => ({ + orderNo: payload.orderNo, + accountSequence: payload.accountSequence, + status: 'FAILED', + error: error instanceof Error ? error.message : 'Unknown error', + }), + eventId: `wd-failed-${payload.orderNo}-${Date.now()}`, + occurredAt: new Date(), + }); + + throw error; + } + } +} diff --git a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts index 9b9893bb..6e07cd20 100644 --- a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts +++ b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts @@ -34,20 +34,20 @@ export interface DeriveAddressResult { * 处理从 MPC 公钥派生钱包地址的业务逻辑 * * 派生策略: - * - KAVA: Cosmos bech32 格式 (kava1...) + * - KAVA: EVM 格式 (0x...) - Kava EVM 兼容链 * - DST: Cosmos bech32 格式 (dst1...) * - BSC: EVM 格式 (0x...) * * 监控策略: - * - 只有 EVM 链 (BSC) 的地址会被注册到监控列表用于充值检测 - * - Cosmos 链 (KAVA, DST) 需要不同的监控机制 + * - EVM 链 (BSC, KAVA) 的地址会被注册到监控列表用于充值检测 + * - Cosmos 链 (DST) 需要不同的监控机制 */ @Injectable() export class AddressDerivationService { private readonly logger = new Logger(AddressDerivationService.name); // EVM 链类型列表,用于判断是否需要注册监控 - private readonly evmChains = new Set([ChainTypeEnum.BSC]); + private readonly evmChains = new Set([ChainTypeEnum.BSC, ChainTypeEnum.KAVA]); constructor( private readonly addressDerivation: AddressDerivationAdapter, diff --git a/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts b/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts index 11ecb6c2..17ef02a1 100644 --- a/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts +++ b/backend/services/blockchain-service/src/application/services/deposit-detection.service.ts @@ -125,7 +125,7 @@ export class DepositDetectionService implements OnModuleInit { const chainType = ChainType.fromEnum(event.chainType); - // 查找监控地址以获取用户ID + // 查找监控地址以获取用户ID或系统账户信息 const monitoredAddress = await this.monitoredAddressRepo.findByChainAndAddress( chainType, EvmAddress.fromUnchecked(event.to), @@ -136,7 +136,7 @@ export class DepositDetectionService implements OnModuleInit { return; } - // 创建充值记录 - 使用 accountSequence 作为跨服务关联键 + // 创建充值记录 - 用户地址 const deposit = DepositTransaction.create({ chainType, txHash, @@ -162,7 +162,7 @@ export class DepositDetectionService implements OnModuleInit { deposit.clearDomainEvents(); this.logger.log( - `New deposit saved: ${txHash.toShort()} -> ${event.to} (${deposit.amount.formatted} USDT)`, + `User deposit saved: ${txHash.toShort()} -> ${event.to} (${deposit.amount.formatted} USDT)`, ); } diff --git a/backend/services/blockchain-service/src/config/blockchain.config.ts b/backend/services/blockchain-service/src/config/blockchain.config.ts index aadc027f..c0178a53 100644 --- a/backend/services/blockchain-service/src/config/blockchain.config.ts +++ b/backend/services/blockchain-service/src/config/blockchain.config.ts @@ -31,8 +31,8 @@ export default registerAs('blockchain', () => { // KAVA Testnet rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.testnet.kava.io', chainId: parseInt(process.env.KAVA_CHAIN_ID || '2221', 10), - // 测试网 USDT 合约 (需要部署或使用已有的) - usdtContract: process.env.KAVA_USDT_CONTRACT || '0x0000000000000000000000000000000000000000', + // 测试网 USDT 合约 (自定义部署的 TestUSDT) + usdtContract: process.env.KAVA_USDT_CONTRACT || '0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF', confirmations: parseInt(process.env.KAVA_CONFIRMATIONS || '3', 10), } : { diff --git a/backend/services/blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts b/backend/services/blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts index 8eb01c2a..e673d0c6 100644 --- a/backend/services/blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts +++ b/backend/services/blockchain-service/src/infrastructure/blockchain/address-derivation.adapter.ts @@ -147,13 +147,12 @@ export class AddressDerivationAdapter { const evmAddress = this.deriveEvmAddress(compressedPublicKey); this.logger.log(`[DERIVE] EVM address derived: ${evmAddress}`); - // KAVA (Cosmos bech32 格式 - kava1...) - const kavaAddress = this.deriveCosmosAddress(compressedPublicKey, 'kava'); + // KAVA (EVM 格式 - 0x...) - Kava EVM 兼容链 addresses.push({ chainType: ChainTypeEnum.KAVA, - address: kavaAddress, + address: evmAddress, }); - this.logger.log(`[DERIVE] KAVA address (Cosmos): ${kavaAddress}`); + this.logger.log(`[DERIVE] KAVA address (EVM): ${evmAddress}`); // DST (Cosmos bech32 格式 - dst1...) const dstAddress = this.deriveCosmosAddress(compressedPublicKey, 'dst'); diff --git a/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts b/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts index ce4a17ee..e8b60d53 100644 --- a/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts @@ -1,7 +1,7 @@ import { Global, Module } from '@nestjs/common'; import { PrismaService } from './persistence/prisma/prisma.service'; import { RedisService, AddressCacheService } from './redis'; -import { EventPublisherService, MpcEventConsumerService } from './kafka'; +import { EventPublisherService, MpcEventConsumerService, WithdrawalEventConsumerService } from './kafka'; import { EvmProviderAdapter, AddressDerivationAdapter, MnemonicDerivationAdapter, RecoveryMnemonicAdapter, BlockScannerService } from './blockchain'; import { DomainModule } from '@/domain/domain.module'; import { @@ -26,6 +26,7 @@ import { RedisService, EventPublisherService, MpcEventConsumerService, + WithdrawalEventConsumerService, // 区块链适配器 EvmProviderAdapter, @@ -60,6 +61,7 @@ import { RedisService, EventPublisherService, MpcEventConsumerService, + WithdrawalEventConsumerService, EvmProviderAdapter, AddressDerivationAdapter, MnemonicDerivationAdapter, diff --git a/backend/services/blockchain-service/src/infrastructure/kafka/index.ts b/backend/services/blockchain-service/src/infrastructure/kafka/index.ts index 07da6b8d..460bc092 100644 --- a/backend/services/blockchain-service/src/infrastructure/kafka/index.ts +++ b/backend/services/blockchain-service/src/infrastructure/kafka/index.ts @@ -1,3 +1,4 @@ export * from './event-publisher.service'; export * from './event-consumer.controller'; export * from './mpc-event-consumer.service'; +export * from './withdrawal-event-consumer.service'; diff --git a/backend/services/blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts b/backend/services/blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts new file mode 100644 index 00000000..3ad6b11d --- /dev/null +++ b/backend/services/blockchain-service/src/infrastructure/kafka/withdrawal-event-consumer.service.ts @@ -0,0 +1,147 @@ +/** + * 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', +} 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 type WithdrawalEventHandler = (payload: WithdrawalRequestedPayload) => 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; + + 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(', ')}`); + + this.kafka = new Kafka({ + clientId, + brokers, + logLevel: logLevel.WARN, + retry: { + initialRetryTime: 100, + retries: 8, + }, + }); + + 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`); + } + + 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 { + 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/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts index d8a70ec5..dab37f55 100644 --- a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts +++ b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/deposit-transaction.mapper.ts @@ -7,7 +7,16 @@ import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domai 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`); + } + const props: DepositTransactionProps = { id: prisma.id, chainType: ChainType.create(prisma.chainType), @@ -52,8 +61,11 @@ export class DepositTransactionMapper { 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/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts index 1d47b76d..4c765087 100644 --- a/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts +++ b/backend/services/blockchain-service/src/infrastructure/persistence/mappers/monitored-address.mapper.ts @@ -3,7 +3,16 @@ import { MonitoredAddress, MonitoredAddressProps } from '@/domain/aggregates/mon 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), @@ -25,8 +34,12 @@ export class MonitoredAddressMapper { 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, }; }