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/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/microservices": "^10.0.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.1.17",
|
||||
|
|
@ -27,6 +29,8 @@
|
|||
"ethers": "^6.9.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"uuid": "^9.0.0"
|
||||
|
|
@ -39,6 +43,7 @@
|
|||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
|
|
@ -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": {
|
||||
"version": "2.0.5",
|
||||
"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": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz",
|
||||
|
|
@ -2554,6 +2582,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
|
||||
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
|
|
@ -2578,13 +2615,44 @@
|
|||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "*",
|
||||
"@types/passport-strategy": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-strategy": {
|
||||
"version": "0.2.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
||||
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
|
|
@ -3711,6 +3779,12 @@
|
|||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -4537,6 +4611,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
|
@ -7161,6 +7244,49 @@
|
|||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kafkajs": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz",
|
||||
|
|
@ -7270,12 +7396,48 @@
|
|||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
|
@ -7290,6 +7452,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
|
|
@ -7913,6 +8081,43 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
|
@ -7990,6 +8195,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -8710,7 +8920,6 @@
|
|||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
|
@ -9886,7 +10095,6 @@
|
|||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,14 @@
|
|||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/microservices": "^10.0.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.1.17",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@scure/bip32": "^1.3.2",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
|
|
@ -56,6 +60,7 @@
|
|||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
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 { 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({
|
||||
imports: [ApplicationModule, DomainModule],
|
||||
controllers: [HealthController, BalanceController, InternalController],
|
||||
imports: [
|
||||
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 {}
|
||||
|
|
|
|||
|
|
@ -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 './balance.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 './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_CLIENT_ID=planting-service
|
||||
- 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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
@ -566,6 +569,7 @@ services:
|
|||
- APP_PORT=3012
|
||||
- API_PREFIX=api/v1
|
||||
- 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_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
|
|
@ -573,8 +577,21 @@ services:
|
|||
- KAFKA_BROKERS=kafka:29092
|
||||
- KAFKA_CLIENT_ID=blockchain-service
|
||||
- KAFKA_GROUP_ID=blockchain-service-group
|
||||
- KAVA_RPC_URL=https://evm.kava.io
|
||||
- BSC_RPC_URL=https://bsc-dataseed.binance.org
|
||||
# 网络模式: mainnet 或 testnet
|
||||
- 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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import {
|
|||
DepositController,
|
||||
HealthController,
|
||||
} from './controllers';
|
||||
import { InternalWalletController } from './controllers/internal-wallet.controller';
|
||||
import { WalletApplicationService } from '@/application/services';
|
||||
import { DepositConfirmedHandler } from '@/application/event-handlers';
|
||||
import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
||||
|
||||
@Module({
|
||||
|
|
@ -27,9 +29,11 @@ import { JwtStrategy } from '@/shared/strategies/jwt.strategy';
|
|||
LedgerController,
|
||||
DepositController,
|
||||
HealthController,
|
||||
InternalWalletController,
|
||||
],
|
||||
providers: [
|
||||
WalletApplicationService,
|
||||
DepositConfirmedHandler,
|
||||
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 {
|
||||
try {
|
||||
debugPrint('查询 USDT 余额...');
|
||||
final response = await _apiClient.get('/api/deposit/balances');
|
||||
final response = await _apiClient.get('/deposit/balances');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
|
|
|||
Loading…
Reference in New Issue