feat(deposit): add deposit balance API and Kafka consumer for deposit events
Blockchain Service: - Add /api/v1/deposit/balances endpoint to query on-chain USDT balances - Add JWT authentication (passport, passport-jwt) - Add JwtStrategy, JwtAuthGuard, Public decorator Wallet Service: - Add Kafka consumer for blockchain.deposits topic - Add DepositConfirmedHandler to process deposit events and update wallet balance Infrastructure: - Add JWT_SECRET env var to blockchain-service in docker-compose.yml - Add blockchain-service routes to Kong API Gateway Frontend: - Fix deposit_service.dart API path (remove duplicate /api prefix) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
26ecb39476
commit
001b6501a0
|
|
@ -13,7 +13,9 @@
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/microservices": "^10.0.0",
|
"@nestjs/microservices": "^10.0.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/schedule": "^4.0.0",
|
"@nestjs/schedule": "^4.0.0",
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
|
|
@ -27,6 +29,8 @@
|
||||||
"ethers": "^6.9.0",
|
"ethers": "^6.9.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
|
|
@ -39,6 +43,7 @@
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
|
@ -1840,6 +1845,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/@nestjs/mapped-types": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
||||||
|
|
@ -1919,6 +1937,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/@nestjs/platform-express": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz",
|
||||||
|
|
@ -2554,6 +2582,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||||
|
|
@ -2578,13 +2615,44 @@
|
||||||
"version": "20.19.25",
|
"version": "20.19.25",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"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": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
|
|
@ -3711,6 +3779,12 @@
|
||||||
"ieee754": "^1.1.13"
|
"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": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
|
@ -4537,6 +4611,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
|
@ -7161,6 +7244,49 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"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": {
|
"node_modules/kafkajs": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz",
|
||||||
|
|
@ -7270,12 +7396,48 @@
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.isarguments": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
|
@ -7290,6 +7452,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/log-symbols": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||||
|
|
@ -7913,6 +8081,43 @@
|
||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
|
@ -7990,6 +8195,11 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -8710,7 +8920,6 @@
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
|
@ -9886,7 +10095,6 @@
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,14 @@
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/microservices": "^10.0.0",
|
"@nestjs/microservices": "^10.0.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@nestjs/schedule": "^4.0.0",
|
"@nestjs/schedule": "^4.0.0",
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@scure/bip32": "^1.3.2",
|
"@scure/bip32": "^1.3.2",
|
||||||
"@scure/bip39": "^1.6.0",
|
"@scure/bip39": "^1.6.0",
|
||||||
|
|
@ -56,6 +60,7 @@
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ApplicationModule } from '@/application/application.module';
|
import { ApplicationModule } from '@/application/application.module';
|
||||||
import { DomainModule } from '@/domain/domain.module';
|
import { DomainModule } from '@/domain/domain.module';
|
||||||
import { HealthController, BalanceController, InternalController } from './controllers';
|
import { HealthController, BalanceController, InternalController, DepositController } from './controllers';
|
||||||
|
import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ApplicationModule, DomainModule],
|
imports: [
|
||||||
controllers: [HealthController, BalanceController, InternalController],
|
ApplicationModule,
|
||||||
|
DomainModule,
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.get<string>('JWT_SECRET') || 'default-secret',
|
||||||
|
signOptions: { expiresIn: '7d' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [HealthController, BalanceController, InternalController, DepositController],
|
||||||
|
providers: [JwtStrategy],
|
||||||
})
|
})
|
||||||
export class ApiModule {}
|
export class ApiModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* 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<DepositBalancesResponseDto> {
|
||||||
|
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: this.toRawBalance(balance.usdtBalance, this.getDecimals(chainTypeStr)),
|
||||||
|
decimals: this.getDecimals(chainTypeStr),
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDecimals(chainType: string): number {
|
||||||
|
// USDT decimals by chain
|
||||||
|
switch (chainType) {
|
||||||
|
case ChainTypeEnum.KAVA:
|
||||||
|
return 18; // Our test USDT on KAVA uses 18 decimals
|
||||||
|
case ChainTypeEnum.BSC:
|
||||||
|
return 18; // Standard USDT on BSC
|
||||||
|
default:
|
||||||
|
return 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toRawBalance(formattedBalance: string, decimals: number): string {
|
||||||
|
try {
|
||||||
|
const value = parseFloat(formattedBalance);
|
||||||
|
return BigInt(Math.floor(value * Math.pow(10, decimals))).toString();
|
||||||
|
} catch {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './health.controller';
|
export * from './health.controller';
|
||||||
export * from './balance.controller';
|
export * from './balance.controller';
|
||||||
export * from './internal.controller';
|
export * from './internal.controller';
|
||||||
|
export * from './deposit.controller';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './public.decorator';
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
|
|
@ -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<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './exceptions';
|
export * from './exceptions';
|
||||||
export * from './filters';
|
export * from './filters';
|
||||||
|
export * from './decorators';
|
||||||
|
|
|
||||||
|
|
@ -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: number;
|
||||||
|
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<string>('JWT_SECRET') || 'default-secret',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
return {
|
||||||
|
userId: payload.userId,
|
||||||
|
accountSequence: payload.accountSequence,
|
||||||
|
deviceId: payload.deviceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -236,6 +236,9 @@ services:
|
||||||
- KAFKA_BROKERS=kafka:29092
|
- KAFKA_BROKERS=kafka:29092
|
||||||
- KAFKA_CLIENT_ID=planting-service
|
- KAFKA_CLIENT_ID=planting-service
|
||||||
- KAFKA_GROUP_ID=planting-service-group
|
- KAFKA_GROUP_ID=planting-service-group
|
||||||
|
- WALLET_SERVICE_URL=http://rwa-wallet-service:3001
|
||||||
|
- IDENTITY_SERVICE_URL=http://rwa-identity-service:3000
|
||||||
|
- REFERRAL_SERVICE_URL=http://rwa-referral-service:3004
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -566,6 +569,7 @@ services:
|
||||||
- APP_PORT=3012
|
- APP_PORT=3012
|
||||||
- API_PREFIX=api/v1
|
- API_PREFIX=api/v1
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_blockchain?schema=public
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_blockchain?schema=public
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
- REDIS_PORT=6379
|
- REDIS_PORT=6379
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||||
|
|
@ -573,8 +577,21 @@ services:
|
||||||
- KAFKA_BROKERS=kafka:29092
|
- KAFKA_BROKERS=kafka:29092
|
||||||
- KAFKA_CLIENT_ID=blockchain-service
|
- KAFKA_CLIENT_ID=blockchain-service
|
||||||
- KAFKA_GROUP_ID=blockchain-service-group
|
- KAFKA_GROUP_ID=blockchain-service-group
|
||||||
- KAVA_RPC_URL=https://evm.kava.io
|
# 网络模式: mainnet 或 testnet
|
||||||
- BSC_RPC_URL=https://bsc-dataseed.binance.org
|
- NETWORK_MODE=${NETWORK_MODE:-mainnet}
|
||||||
|
# 主网配置 (NETWORK_MODE=mainnet 时使用)
|
||||||
|
# - KAVA_RPC_URL=https://evm.kava.io
|
||||||
|
# - KAVA_CHAIN_ID=2222
|
||||||
|
# - KAVA_USDT_CONTRACT=0x919C1c267BC06a7039e03fcc2eF738525769109c
|
||||||
|
# - BSC_RPC_URL=https://bsc-dataseed.binance.org
|
||||||
|
# - BSC_CHAIN_ID=56
|
||||||
|
# - BSC_USDT_CONTRACT=0x55d398326f99059fF775485246999027B3197955
|
||||||
|
# 测试网配置 (NETWORK_MODE=testnet 时使用)
|
||||||
|
# - KAVA_RPC_URL=https://evm.testnet.kava.io
|
||||||
|
# - KAVA_CHAIN_ID=2221
|
||||||
|
# - BSC_RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545
|
||||||
|
# - BSC_CHAIN_ID=97
|
||||||
|
# - BSC_USDT_CONTRACT=0x337610d27c682E347C9cD60BD4b3b107C9d34dDd
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import {
|
||||||
DepositController,
|
DepositController,
|
||||||
HealthController,
|
HealthController,
|
||||||
} from './controllers';
|
} from './controllers';
|
||||||
|
import { InternalWalletController } from './controllers/internal-wallet.controller';
|
||||||
import { WalletApplicationService } from '@/application/services';
|
import { WalletApplicationService } from '@/application/services';
|
||||||
|
import { DepositConfirmedHandler } from '@/application/event-handlers';
|
||||||
import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -27,9 +29,11 @@ import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
||||||
LedgerController,
|
LedgerController,
|
||||||
DepositController,
|
DepositController,
|
||||||
HealthController,
|
HealthController,
|
||||||
|
InternalWalletController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
WalletApplicationService,
|
WalletApplicationService,
|
||||||
|
DepositConfirmedHandler,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Deposit Confirmed Event Handler
|
||||||
|
*
|
||||||
|
* Handles deposit confirmation events from blockchain-service.
|
||||||
|
* Credits user wallets when deposits are confirmed on-chain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { WalletApplicationService } from '@/application/services/wallet-application.service';
|
||||||
|
import { HandleDepositCommand } from '@/application/commands';
|
||||||
|
import {
|
||||||
|
DepositEventConsumerService,
|
||||||
|
DepositConfirmedPayload,
|
||||||
|
} from '@/infrastructure/kafka/deposit-event-consumer.service';
|
||||||
|
import { ChainType } from '@/domain/value-objects';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DepositConfirmedHandler implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(DepositConfirmedHandler.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly depositEventConsumer: DepositEventConsumerService,
|
||||||
|
private readonly walletApplicationService: WalletApplicationService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.depositEventConsumer.onDepositConfirmed(this.handleDepositConfirmed.bind(this));
|
||||||
|
this.logger.log('DepositConfirmedHandler registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDepositConfirmed(payload: DepositConfirmedPayload): Promise<void> {
|
||||||
|
this.logger.log(`Processing deposit confirmation: txHash=${payload.txHash}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Map chainType string to ChainType enum
|
||||||
|
const chainType = this.mapChainType(payload.chainType);
|
||||||
|
|
||||||
|
const command: HandleDepositCommand = {
|
||||||
|
accountSequence: payload.accountSequence,
|
||||||
|
userId: payload.userId,
|
||||||
|
amount: parseFloat(payload.amountFormatted),
|
||||||
|
chainType,
|
||||||
|
txHash: payload.txHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.walletApplicationService.handleDeposit(command);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Deposit credited successfully: ` +
|
||||||
|
`txHash=${payload.txHash}, ` +
|
||||||
|
`amount=${payload.amountFormatted} USDT, ` +
|
||||||
|
`userId=${payload.userId}, ` +
|
||||||
|
`accountSequence=${payload.accountSequence}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Check if it's a duplicate transaction error (already processed)
|
||||||
|
if (error.message?.includes('Duplicate transaction')) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Deposit already processed (duplicate): txHash=${payload.txHash}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to process deposit: txHash=${payload.txHash}`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapChainType(chainType: string): ChainType {
|
||||||
|
const normalized = chainType.toUpperCase();
|
||||||
|
switch (normalized) {
|
||||||
|
case 'KAVA':
|
||||||
|
return ChainType.KAVA;
|
||||||
|
case 'BSC':
|
||||||
|
return ChainType.BSC;
|
||||||
|
default:
|
||||||
|
this.logger.warn(`Unknown chain type: ${chainType}, defaulting to KAVA`);
|
||||||
|
return ChainType.KAVA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './deposit-confirmed.handler';
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
/**
|
||||||
|
* Deposit Event Consumer Service for Wallet Service
|
||||||
|
*
|
||||||
|
* Consumes deposit confirmed events from blockchain-service via Kafka.
|
||||||
|
* Credits user wallets when deposits are confirmed on-chain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
|
||||||
|
|
||||||
|
export const DEPOSIT_TOPICS = {
|
||||||
|
BLOCKCHAIN_DEPOSITS: 'blockchain.deposits',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface DepositConfirmedPayload {
|
||||||
|
depositId: string;
|
||||||
|
chainType: string;
|
||||||
|
txHash: string;
|
||||||
|
toAddress: string;
|
||||||
|
amount: string;
|
||||||
|
amountFormatted: string;
|
||||||
|
confirmations: number;
|
||||||
|
accountSequence: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DepositEventHandler = (payload: DepositConfirmedPayload) => Promise<void>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DepositEventConsumerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(DepositEventConsumerService.name);
|
||||||
|
private kafka: Kafka;
|
||||||
|
private consumer: Consumer;
|
||||||
|
private isConnected = false;
|
||||||
|
|
||||||
|
private depositConfirmedHandler?: DepositEventHandler;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const brokers = this.configService.get<string>('KAFKA_BROKERS')?.split(',') || ['localhost:9092'];
|
||||||
|
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID') || 'wallet-service';
|
||||||
|
const groupId = 'wallet-service-deposit-events';
|
||||||
|
|
||||||
|
this.logger.log(`[INIT] Deposit 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(DEPOSIT_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 Deposit Event consumer...`);
|
||||||
|
await this.consumer.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log(`[CONNECT] Deposit Event consumer connected successfully`);
|
||||||
|
|
||||||
|
await this.consumer.subscribe({
|
||||||
|
topics: Object.values(DEPOSIT_TOPICS),
|
||||||
|
fromBeginning: false,
|
||||||
|
});
|
||||||
|
this.logger.log(`[SUBSCRIBE] Subscribed to deposit topics`);
|
||||||
|
|
||||||
|
await this.startConsuming();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[ERROR] Failed to connect Deposit Event consumer`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
await this.consumer.disconnect();
|
||||||
|
this.logger.log('Deposit Event consumer disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handler for deposit confirmed events
|
||||||
|
*/
|
||||||
|
onDepositConfirmed(handler: DepositEventHandler): void {
|
||||||
|
this.depositConfirmedHandler = handler;
|
||||||
|
this.logger.log(`[REGISTER] DepositConfirmed handler registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startConsuming(): Promise<void> {
|
||||||
|
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.debug(`[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 === 'blockchain.deposit.confirmed') {
|
||||||
|
this.logger.log(`[HANDLE] Processing DepositConfirmed event`);
|
||||||
|
this.logger.log(`[HANDLE] depositId: ${payload.depositId}`);
|
||||||
|
this.logger.log(`[HANDLE] chainType: ${payload.chainType}`);
|
||||||
|
this.logger.log(`[HANDLE] txHash: ${payload.txHash}`);
|
||||||
|
this.logger.log(`[HANDLE] amount: ${payload.amountFormatted}`);
|
||||||
|
this.logger.log(`[HANDLE] accountSequence: ${payload.accountSequence}`);
|
||||||
|
this.logger.log(`[HANDLE] userId: ${payload.userId}`);
|
||||||
|
|
||||||
|
if (this.depositConfirmedHandler) {
|
||||||
|
await this.depositConfirmedHandler(payload as DepositConfirmedPayload);
|
||||||
|
this.logger.log(`[HANDLE] DepositConfirmed handler completed`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`[HANDLE] No handler registered for DepositConfirmed`);
|
||||||
|
}
|
||||||
|
} else if (eventType === 'blockchain.deposit.detected') {
|
||||||
|
// Log detected deposits but don't process them yet (wait for confirmation)
|
||||||
|
this.logger.log(`[SKIP] DepositDetected event received, waiting for confirmation`);
|
||||||
|
this.logger.log(`[SKIP] txHash: ${payload.txHash}, amount: ${payload.amountFormatted}`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`[RECEIVE] Unknown event type: ${eventType}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[ERROR] Error processing deposit event from ${topic}`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[START] Started consuming deposit events`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* Kafka Event Publisher Service for Wallet Service
|
||||||
|
*
|
||||||
|
* Publishes domain events to Kafka for cross-service communication.
|
||||||
|
* Used to notify blockchain-service about withdrawal requests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Kafka, Producer, logLevel } from 'kafkajs';
|
||||||
|
|
||||||
|
export interface EventPayload {
|
||||||
|
eventId?: string;
|
||||||
|
eventType: string;
|
||||||
|
occurredAt?: Date;
|
||||||
|
payload: { [key: string]: unknown };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventPublisherService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(EventPublisherService.name);
|
||||||
|
private kafka: Kafka;
|
||||||
|
private producer: Producer;
|
||||||
|
private isConnected = false;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const brokers = this.configService.get<string>('KAFKA_BROKERS')?.split(',') || ['localhost:9092'];
|
||||||
|
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID') || 'wallet-service';
|
||||||
|
|
||||||
|
this.logger.log(`[INIT] Event Publisher initializing...`);
|
||||||
|
this.logger.log(`[INIT] ClientId: ${clientId}`);
|
||||||
|
this.logger.log(`[INIT] Brokers: ${brokers.join(', ')}`);
|
||||||
|
|
||||||
|
this.kafka = new Kafka({
|
||||||
|
clientId,
|
||||||
|
brokers,
|
||||||
|
logLevel: logLevel.WARN,
|
||||||
|
retry: {
|
||||||
|
initialRetryTime: 100,
|
||||||
|
retries: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.producer = this.kafka.producer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.producer.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
this.logger.log(`[CONNECT] Kafka producer connected successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[ERROR] Failed to connect Kafka producer`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
await this.producer.disconnect();
|
||||||
|
this.logger.log('Kafka producer disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an event to Kafka
|
||||||
|
*/
|
||||||
|
async publish(event: EventPayload): Promise<void> {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.logger.warn(`[PUBLISH] Kafka not connected, skipping event: ${event.eventType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = this.getTopicForEvent(event.eventType);
|
||||||
|
const eventId = event.eventId || this.generateEventId();
|
||||||
|
const occurredAt = event.occurredAt || new Date();
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
key: eventId,
|
||||||
|
value: JSON.stringify({
|
||||||
|
eventId,
|
||||||
|
eventType: event.eventType,
|
||||||
|
occurredAt: occurredAt.toISOString(),
|
||||||
|
payload: event.payload,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
eventType: event.eventType,
|
||||||
|
source: 'wallet-service',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.producer.send({
|
||||||
|
topic,
|
||||||
|
messages: [message],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PUBLISH] Published event: ${event.eventType} to topic: ${topic}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[ERROR] Failed to publish event: ${event.eventType}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTopicForEvent(eventType: string): string {
|
||||||
|
const topicMap: Record<string, string> = {
|
||||||
|
'wallet.withdrawal.requested': 'wallet.withdrawals',
|
||||||
|
'wallet.withdrawal.completed': 'wallet.withdrawals',
|
||||||
|
'wallet.withdrawal.failed': 'wallet.withdrawals',
|
||||||
|
};
|
||||||
|
return topicMap[eventType] || 'wallet.events';
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateEventId(): string {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './kafka.module';
|
||||||
|
export * from './event-publisher.service';
|
||||||
|
export * from './deposit-event-consumer.service';
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
import { EventPublisherService } from './event-publisher.service';
|
||||||
|
import { DepositEventConsumerService } from './deposit-event-consumer.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [EventPublisherService, DepositEventConsumerService],
|
||||||
|
exports: [EventPublisherService, DepositEventConsumerService],
|
||||||
|
})
|
||||||
|
export class KafkaModule {}
|
||||||
|
|
@ -176,7 +176,7 @@ class DepositService {
|
||||||
Future<BalanceResponse> getUsdtBalances() async {
|
Future<BalanceResponse> getUsdtBalances() async {
|
||||||
try {
|
try {
|
||||||
debugPrint('查询 USDT 余额...');
|
debugPrint('查询 USDT 余额...');
|
||||||
final response = await _apiClient.get('/api/deposit/balances');
|
final response = await _apiClient.get('/deposit/balances');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue