diff --git a/package-lock.json b/package-lock.json index d3dceeb1..3f7905ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "@bull-board/api": "^5.0.0", "@bull-board/express": "^5.0.0", "@cosmjs/amino": "^0.29.4", - "@cosmjs/cosmwasm-stargate": "^0.29.4", + "@cosmjs/cosmwasm-stargate": "^0.29.5", "@cosmjs/crypto": "^0.29.4", "@cosmjs/encoding": "^0.29.4", - "@influxdata/influxdb-client": "^1.31.0", + "@cosmjs/stargate": "^0.29.5", "@json2csv/plainjs": "^6.1.3", "@nestjs-modules/mailer": "^1.8.1", "@nestjs/axios": "0.0.3", @@ -58,7 +58,7 @@ "rxjs": "^7.2.0", "swagger-ui-express": "^4.1.6", "tendermint": "^5.0.2", - "typeorm": "^0.2.38", + "typeorm": "^0.2.40", "uuid": "^8.3.2", "winston": "^3.3.3" }, @@ -2769,11 +2769,6 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, - "node_modules/@influxdata/influxdb-client": { - "version": "1.33.2", - "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.33.2.tgz", - "integrity": "sha512-RT5SxH+grHAazo/YK3UTuWK/frPWRM0N7vkrCUyqVprDgQzlLP+bSK4ak2Jv3QVF/pazTnsxWjvtKZdwskV5Xw==" - }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -7960,6 +7955,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -8634,14 +8630,6 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" }, - "node_modules/figlet": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", - "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9763,25 +9751,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -13662,14 +13631,6 @@ "node": ">=6" } }, - "node_modules/parent-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", - "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -16382,9 +16343,9 @@ } }, "node_modules/typeorm": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.38.tgz", - "integrity": "sha512-M6Y3KQcAREQcphOVJciywf4mv6+A0I/SeR+lWNjKsjnQ+a3XcMwGYMGL0Jonsx3H0Cqlf/3yYqVki1jIXSK/xg==", + "version": "0.2.40", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.40.tgz", + "integrity": "sha512-F1WQKFl3opokSBNxUdX68uhD1Hpk8mgigsLtJKluI7YRs52+tsHNuaO5gLSzvjYZoLqnYc6pomsCYZMRUT2zxA==", "dependencies": { "@sqltools/formatter": "^1.2.2", "app-root-path": "^3.0.0", @@ -16400,7 +16361,6 @@ "sha.js": "^2.4.11", "tslib": "^2.1.0", "xml2js": "^0.4.23", - "yargonaut": "^1.1.4", "yargs": "^17.0.1", "zen-observable-ts": "^1.0.0" }, @@ -17439,66 +17399,6 @@ "node": ">= 6" } }, - "node_modules/yargonaut": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", - "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", - "dependencies": { - "chalk": "^1.1.1", - "figlet": "^1.1.1", - "parent-require": "^1.0.0" - } - }, - "node_modules/yargonaut/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargonaut/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargonaut/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargonaut/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargonaut/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -19857,11 +19757,6 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, - "@influxdata/influxdb-client": { - "version": "1.33.2", - "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.33.2.tgz", - "integrity": "sha512-RT5SxH+grHAazo/YK3UTuWK/frPWRM0N7vkrCUyqVprDgQzlLP+bSK4ak2Jv3QVF/pazTnsxWjvtKZdwskV5Xw==" - }, "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -24065,7 +23960,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "2.0.0", @@ -24570,11 +24466,6 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" }, - "figlet": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", - "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==" - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -25407,21 +25298,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -28539,11 +28415,6 @@ "callsites": "^3.0.0" } }, - "parent-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", - "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=" - }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -30638,9 +30509,9 @@ } }, "typeorm": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.38.tgz", - "integrity": "sha512-M6Y3KQcAREQcphOVJciywf4mv6+A0I/SeR+lWNjKsjnQ+a3XcMwGYMGL0Jonsx3H0Cqlf/3yYqVki1jIXSK/xg==", + "version": "0.2.40", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.40.tgz", + "integrity": "sha512-F1WQKFl3opokSBNxUdX68uhD1Hpk8mgigsLtJKluI7YRs52+tsHNuaO5gLSzvjYZoLqnYc6pomsCYZMRUT2zxA==", "requires": { "@sqltools/formatter": "^1.2.2", "app-root-path": "^3.0.0", @@ -30656,7 +30527,6 @@ "sha.js": "^2.4.11", "tslib": "^2.1.0", "xml2js": "^0.4.23", - "yargonaut": "^1.1.4", "yargs": "^17.0.1", "zen-observable-ts": "^1.0.0" }, @@ -31389,53 +31259,6 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true }, - "yargonaut": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", - "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", - "requires": { - "chalk": "^1.1.1", - "figlet": "^1.1.1", - "parent-require": "^1.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index a7ba9d80..a3c83c8d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "rxjs": "^7.2.0", "swagger-ui-express": "^4.1.6", "tendermint": "^5.0.2", - "typeorm": "^0.2.38", + "typeorm": "^0.2.40", "uuid": "^8.3.2", "winston": "^3.3.3" }, diff --git a/src/app.module.ts b/src/app.module.ts index 32cce526..3a758988 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,6 +19,7 @@ import { ExportCsvModule } from './components/export-csv/export-csv.module'; import { GoogleRecaptchaModule } from '@nestlab/google-recaptcha/google-recaptcha.module'; import { NotificationModule } from './components/notification/notification.module'; import { WatchListModule } from './components/watch-list/watch-list.module'; +import { AssetModule } from './components/asset/asset.module'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { WatchListModule } from './components/watch-list/watch-list.module'; ExportCsvModule, MailModule, WatchListModule, + AssetModule, ConfigModule.forRoot({ isGlobal: true, }), diff --git a/src/components/account/account.module.ts b/src/components/account/account.module.ts index 8a32bf53..14f445d3 100644 --- a/src/components/account/account.module.ts +++ b/src/components/account/account.module.ts @@ -7,10 +7,18 @@ import { SharedModule } from '../../shared/shared.module'; import { AccountController } from './controllers/account.controller'; import { AccountService } from './services/account.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { RpcUtil } from 'src/shared/utils/rpc.util'; @Module({ - imports: [SharedModule, HttpModule, ConfigModule], - providers: [AccountService, ServiceUtil], + imports: [ + SharedModule, + HttpModule, + ConfigModule, + TypeOrmModule.forFeature([Explorer]), + ], + providers: [AccountService, ServiceUtil, RpcUtil], controllers: [AccountController], exports: [AccountService], }) diff --git a/src/components/account/controllers/account.controller.ts b/src/components/account/controllers/account.controller.ts index 4f5fdbc2..3d48dbad 100644 --- a/src/components/account/controllers/account.controller.ts +++ b/src/components/account/controllers/account.controller.ts @@ -1,5 +1,4 @@ import { - Body, ClassSerializerInterceptor, Controller, Get, diff --git a/src/components/account/services/account.service.ts b/src/components/account/services/account.service.ts index 8acbdaea..040e0510 100644 --- a/src/components/account/services/account.service.ts +++ b/src/components/account/services/account.service.ts @@ -24,32 +24,31 @@ import { QueryValidatorCommissionResponse, } from 'cosmjs-types/cosmos/distribution/v1beta1/query'; import { TransactionHelper } from '../../../shared/helpers/transaction.helper'; +import { Repository } from 'typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { RpcUtil } from 'src/shared/utils/rpc.util'; @Injectable() export class AccountService { private api; private minimalDenom; - private precisionDiv; - private decimals; private chainDB; constructor( private readonly logger: AkcLogger, private serviceUtil: ServiceUtil, + private rpcUtil: RpcUtil, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) { this.logger.setContext(AccountService.name); const appParams = appConfig.default(); this.api = appParams.node.api; this.minimalDenom = appParams.chainInfo.coinMinimalDenom; - this.precisionDiv = appParams.chainInfo.precisionDiv; - this.decimals = appParams.chainInfo.coinDecimals; this.chainDB = appParams.indexerV2.chainDB; } - changeUauraToAura(amount) { - return (amount / this.precisionDiv).toFixed(this.decimals); - } - async getTotalBalanceByAddress( ctx: RequestContext, address: string, @@ -75,7 +74,10 @@ export class AccountService { }; const graphqlQueryVal = { - query: INDEXER_API_V2.GRAPH_QL.LIST_VALIDATOR, + query: util.format( + INDEXER_API_V2.GRAPH_QL.LIST_VALIDATOR, + this.chainDB, + ), variables: { address: address, }, @@ -241,9 +243,16 @@ export class AccountService { ); } + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + // get list account detail const graphqlQueryAcc = { - query: util.format(INDEXER_API_V2.GRAPH_QL.LIST_ACCOUNT), + query: util.format( + INDEXER_API_V2.GRAPH_QL.LIST_ACCOUNT, + explorer.chainDb, + ), variables: { address: address, }, @@ -251,7 +260,10 @@ export class AccountService { }; const graphqlQueryVal = { - query: INDEXER_API_V2.GRAPH_QL.LIST_VALIDATOR, + query: util.format( + INDEXER_API_V2.GRAPH_QL.LIST_VALIDATOR, + explorer.chainDb, + ), variables: { address: address, }, @@ -262,14 +274,14 @@ export class AccountService { await Promise.all([ this.serviceUtil.fetchDataFromGraphQL(graphqlQueryAcc), this.serviceUtil.fetchDataFromGraphQL(graphqlQueryVal), - this.getDelegatorDelegations(address), - this.getDelegatorUnbondingDelegations(address), + this.getDelegatorDelegations(address, ctx.chainId), + this.getDelegatorUnbondingDelegations(address, ctx.chainId), ]); - const accountData = account?.data[this.chainDB]['account'] || []; + const accountData = account?.data[explorer.chainDb]['account'] || []; const result = []; for (const data of accountData) { - const validatorData = validators?.data[this.chainDB]['validator']; + const validatorData = validators?.data[explorer.chainDb]['validator']; if (!data) { return { address, amount: 0 }; } @@ -288,7 +300,7 @@ export class AccountService { let balancesAmount = 0; const balances = data.balances?.length ? data.balances : []; balances.forEach((item) => { - if (item.denom === this.minimalDenom) { + if (item.denom === explorer.minimalDenom) { balancesAmount = parseFloat(item.amount); } }); @@ -300,7 +312,7 @@ export class AccountService { ? data.spendable_balances : []; const uaura = spendable_balances?.find( - (f) => f.denom === this.minimalDenom, + (f) => f.denom === explorer.minimalDenom, ); if (uaura) { const amount = uaura.amount; @@ -313,13 +325,16 @@ export class AccountService { let stakeReward = 0; if (accountDelegations?.length > 0) { const delegationsRewardRespone = - await this.queryDelegationTotalRewardsRequests(data.address); + await this.queryDelegationTotalRewardsRequests( + data.address, + ctx.chainId, + ); accountDelegations.forEach((item) => { delegatedAmount += parseInt(item.balance.amount); if ( delegationsRewardRespone && delegationsRewardRespone.length > 0 && - delegationsRewardRespone[0].denom === this.minimalDenom + delegationsRewardRespone[0].denom === explorer.minimalDenom ) { stakeReward = parseInt(delegationsRewardRespone[0].amount); } @@ -345,10 +360,11 @@ export class AccountService { if (validator?.length > 0) { const commissionData = await this.queryValidatorCommissionRequests( validator[0].operator_address, + ctx.chainId, ); if ( commissionData?.length > 0 && - commissionData[0].denom === this.minimalDenom + commissionData[0].denom === explorer.minimalDenom ) { commission = commissionData[0].amount; } @@ -380,9 +396,9 @@ export class AccountService { } } - async getDelegatorDelegations(address = []) { + async getDelegatorDelegations(address = [], explorer: string) { const res = address.map((delegatorAddr) => - this.queryDelegatorDelegationsRequests(delegatorAddr), + this.queryDelegatorDelegationsRequests(delegatorAddr, explorer), ); const results = await Promise.allSettled(res); return results @@ -390,13 +406,17 @@ export class AccountService { .map((x: any) => x.value); } - async queryDelegatorDelegationsRequests(delegatorAddr: string) { + async queryDelegatorDelegationsRequests( + delegatorAddr: string, + explorer: string, + ) { try { - const response = await this.serviceUtil.queryComosRPC( + const response = await this.rpcUtil.queryComosRPC( RPC_QUERY_URL.DELEGATOR_DELEGATIONS, QueryDelegatorDelegationsRequest.encode({ delegatorAddr, }).finish(), + explorer, ); const value = response.result.response.value; return QueryDelegatorDelegationsResponse.decode( @@ -411,9 +431,9 @@ export class AccountService { } } - async getDelegatorUnbondingDelegations(address = []) { + async getDelegatorUnbondingDelegations(address = [], explorer: string) { const res = address.map((delegatorAddr) => - this.queryDelegatorUnbondingDelegationsRequests(delegatorAddr), + this.queryDelegatorUnbondingDelegationsRequests(delegatorAddr, explorer), ); const results = await Promise.allSettled(res); return results @@ -421,13 +441,17 @@ export class AccountService { .map((x: any) => x.value); } - async queryDelegatorUnbondingDelegationsRequests(delegatorAddr: string) { + async queryDelegatorUnbondingDelegationsRequests( + delegatorAddr: string, + explorer: string, + ) { try { - const response = await this.serviceUtil.queryComosRPC( + const response = await this.rpcUtil.queryComosRPC( RPC_QUERY_URL.DELEGATOR_UNBONDING_DELEGATIONS, QueryDelegatorUnbondingDelegationsRequest.encode({ delegatorAddr, }).finish(), + explorer, ); const value = response.result.response.value; return QueryDelegatorUnbondingDelegationsResponse.decode( @@ -442,13 +466,17 @@ export class AccountService { } } - async queryDelegationTotalRewardsRequests(delegatorAddress: string) { + async queryDelegationTotalRewardsRequests( + delegatorAddress: string, + explorer: string, + ) { try { - const response = await this.serviceUtil.queryComosRPC( + const response = await this.rpcUtil.queryComosRPC( RPC_QUERY_URL.DELEGATION_TOTAL_REWARDS, QueryDelegationTotalRewardsRequest.encode({ delegatorAddress, }).finish(), + explorer, ); const value = response.result.response.value; return QueryDelegationTotalRewardsResponse.decode( @@ -463,13 +491,17 @@ export class AccountService { } } - async queryValidatorCommissionRequests(validatorAddress: string) { + async queryValidatorCommissionRequests( + validatorAddress: string, + explorer: string, + ) { try { - const response = await this.serviceUtil.queryComosRPC( + const response = await this.rpcUtil.queryComosRPC( RPC_QUERY_URL.VALIDATOR_COMMISSION, QueryValidatorCommissionRequest.encode({ validatorAddress, }).finish(), + explorer, ); const value = response.result.response.value; return QueryValidatorCommissionResponse.decode( diff --git a/src/components/asset/asset.module.ts b/src/components/asset/asset.module.ts new file mode 100644 index 00000000..25fe7c9a --- /dev/null +++ b/src/components/asset/asset.module.ts @@ -0,0 +1,23 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SharedModule } from '../../shared/shared.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AssetService } from './services/asset.service'; +import { AssetController } from './controllers/asset.controller'; +import { Asset } from 'src/shared'; +import { AssetsRepository } from './repositories/assets.repository'; +import { TokenHolderStatistic } from 'src/shared/entities/token-holder-statistic.entity'; + +@Module({ + imports: [ + SharedModule, + HttpModule, + ConfigModule, + TypeOrmModule.forFeature([Asset, AssetsRepository, TokenHolderStatistic]), + ], + providers: [AssetService], + controllers: [AssetController], + exports: [AssetService], +}) +export class AssetModule {} diff --git a/src/components/asset/controllers/asset.controller.ts b/src/components/asset/controllers/asset.controller.ts new file mode 100644 index 00000000..0a7017a8 --- /dev/null +++ b/src/components/asset/controllers/asset.controller.ts @@ -0,0 +1,88 @@ +import { + CacheInterceptor, + ClassSerializerInterceptor, + Controller, + Get, + HttpCode, + HttpStatus, + Param, + Query, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiOkResponse, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { + AkcLogger, + Asset, + BaseApiResponse, + MESSAGES, + ReqContext, + RequestContext, +} from 'src/shared'; +import { AssetService } from '../services/asset.service'; +import { AssetParamsDto } from '../dtos/asset-params.dto'; +import { AssetAttributes, GetAssetResult } from '../dtos/get-asset.dto'; +import { AssetsTokenMarketParamsDto } from '../dtos/cw20-token-market-params.dto'; + +@Controller() +@ApiTags('asset') +@ApiBadRequestResponse({ + description: MESSAGES.ERROR.BAD_REQUEST, +}) +export class AssetController { + constructor( + private readonly assetService: AssetService, + private readonly logger: AkcLogger, + ) { + this.logger.setContext(AssetController.name); + } + + @Get('assets') + @ApiOperation({ summary: 'Get list Assets' }) + @HttpCode(HttpStatus.OK) + @UseInterceptors(ClassSerializerInterceptor) + @UseInterceptors(CacheInterceptor) + @ApiOkResponse({ type: GetAssetResult }) + async getAssets( + @ReqContext() ctx: RequestContext, + @Query() param: AssetParamsDto, + ): Promise> { + this.logger.log(ctx, `${this.getAssets.name} was called!`); + const { result, count } = await this.assetService.getAssets(ctx, param); + return { data: result, meta: { count } }; + } + + @Get('assets/token-market') + @ApiOperation({ + summary: 'Get token market of cw20 token by contract address', + }) + @ApiResponse({ status: HttpStatus.OK }) + @UseInterceptors(ClassSerializerInterceptor) + @UseInterceptors(CacheInterceptor) + async getAssetsTokenMarket( + @ReqContext() ctx: RequestContext, + @Query() query: AssetsTokenMarketParamsDto, + ): Promise { + this.logger.log(ctx, `${this.getAssetsTokenMarket.name} was called!`); + return await this.assetService.getAssetsTokenMarket(ctx, query); + } + + @Get('assets/:denom') + @ApiOperation({ summary: 'Get Assets detail' }) + @HttpCode(HttpStatus.OK) + @UseInterceptors(ClassSerializerInterceptor) + @UseInterceptors(CacheInterceptor) + @ApiOkResponse({ type: AssetAttributes }) + async getAssetsDetail( + @ReqContext() ctx: RequestContext, + @Param('denom') denom: string, + ): Promise { + this.logger.log(ctx, `${this.getAssetsDetail.name} was called!`); + return await this.assetService.getAssetsDetail(ctx, denom); + } +} diff --git a/src/components/asset/dtos/asset-params.dto.ts b/src/components/asset/dtos/asset-params.dto.ts new file mode 100644 index 00000000..c812ed91 --- /dev/null +++ b/src/components/asset/dtos/asset-params.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { Max, Min } from 'class-validator'; +import { ASSETS_TYPE, PAGE_REQUEST } from '../../../shared/constants/common'; + +export class AssetParamsDto { + @ApiPropertyOptional({ default: '' }) + keyword: string; + + @ApiPropertyOptional({ + description: `Optional, defaults to 100`, + default: 100, + type: Number, + }) + @Transform(({ value }) => parseInt(value, 0), { toClassOnly: true }) + @Min(PAGE_REQUEST.MIN) + @Max(PAGE_REQUEST.MAX) + limit: number; + + @ApiProperty({ default: 0 }) + offset: number; + + @ApiPropertyOptional({ + default: '', + example: `${ASSETS_TYPE.IBC},${ASSETS_TYPE.NATIVE}`, + }) + type: string; +} diff --git a/src/components/asset/dtos/cw20-token-market-params.dto.ts b/src/components/asset/dtos/cw20-token-market-params.dto.ts new file mode 100644 index 00000000..d6c0eb3e --- /dev/null +++ b/src/components/asset/dtos/cw20-token-market-params.dto.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class AssetsTokenMarketParamsDto { + @ApiPropertyOptional({ + description: `Optional denom to search token market`, + type: String, + default: '', + }) + denom: string; +} diff --git a/src/components/asset/dtos/get-asset.dto.ts b/src/components/asset/dtos/get-asset.dto.ts new file mode 100644 index 00000000..f8490544 --- /dev/null +++ b/src/components/asset/dtos/get-asset.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ASSETS_TYPE } from 'src/shared'; +import { TokenHolderStatistic } from '../../../shared/entities/token-holder-statistic.entity'; + +export class AssetAttributes { + @ApiProperty({ example: 1 }) + id: number; + + @ApiProperty({ example: 'Aura serenity testnet (AURA)' }) + name: string; + + @ApiProperty({ example: 'NATIVE' }) + type: ASSETS_TYPE; + + @ApiProperty({ + example: 0.04092, + }) + currentPrice: number; + + @ApiProperty({ + example: 10000000, + }) + totalSupply: number; + + @ApiProperty({ + example: [ + { + created_at: '2024-02-20T23:20:44.112Z', + updated_at: '2024-02-20T23:22:56.220Z', + id: 122, + totalHolder: 28687, + }, + { + created_at: '2024-02-20T23:20:44.112Z', + updated_at: '2024-02-20T23:22:56.321Z', + id: 127, + totalHolder: 28687, + }, + ], + }) + holders: TokenHolderStatistic[]; +} + +export class GetAssetResult { + @ApiProperty({ type: AssetAttributes, isArray: true }) + data: AssetAttributes[]; +} diff --git a/src/components/asset/repositories/assets.repository.ts b/src/components/asset/repositories/assets.repository.ts new file mode 100644 index 00000000..67eb9404 --- /dev/null +++ b/src/components/asset/repositories/assets.repository.ts @@ -0,0 +1,122 @@ +import { Logger } from '@nestjs/common'; +import { Brackets, EntityRepository, Repository } from 'typeorm'; +import { Asset } from '../../../shared'; + +@EntityRepository(Asset) +export class AssetsRepository extends Repository { + private readonly _logger = new Logger(AssetsRepository.name); + + async countAssetsHavingCoinId() { + this._logger.log( + `============== ${this.countAssetsHavingCoinId.name} was called! ==============`, + ); + const sqlSelect = `tm.denom, tm.coin_id`; + + const queryBuilder = this.createQueryBuilder('tm') + .select(sqlSelect) + .where("tm.coin_id <> '' "); + + return await queryBuilder.getCount(); + } + + async getAssetsHavingCoinId(limit: number, pageIndex: number) { + this._logger.log( + `============== ${this.getAssetsHavingCoinId.name} was called! ==============`, + ); + const sqlSelect = ` tm.coin_id`; + + const queryBuilder = this.createQueryBuilder('tm') + .select(sqlSelect) + .where("tm.coin_id <> '' ") + .limit(limit) + .offset(pageIndex * limit); + + return await queryBuilder.getRawMany(); + } + + async getAssets(keyword, limit = 1, offset = 0, type = '') { + this._logger.log( + `============== ${this.getAssets.name} was called! ==============`, + ); + + const builder = this.createQueryBuilder('asset').where( + 'asset.name IS NOT NULL', + ); + + const _finalizeResult = async () => { + const result: Asset[] = await builder + .limit(limit) + .offset(offset) + .orderBy(`CASE WHEN asset.\`type\`='NATIVE' THEN 0 ELSE 1 END`) + .addOrderBy('asset.verify_status', 'DESC') + .addOrderBy('asset.total_supply', 'DESC') + .getMany(); + + const count = await builder.getCount(); + return { result, count }; + }; + + if (keyword) { + builder.andWhere( + new Brackets((qb) => { + qb.where('asset.denom =:address', { + address: keyword, + }) + .orWhere('asset.denom =:ibc', { + ibc: `ibc/${keyword}`, + }) + .orWhere('LOWER(asset.name) LIKE LOWER(:keyword)', { + keyword: `%${keyword}%`, + }) + .orWhere('LOWER(asset.symbol) LIKE LOWER(:keyword)', { + keyword: `%${keyword}%`, + }); + }), + ); + } + + const assetType = !!type ? type.split(',') : []; + if (assetType?.length > 0) { + builder.andWhere('asset.type IN(:...type)', { + type: assetType, + }); + } + + return await _finalizeResult(); + } + + async getAssetsDetail(denom, days = 2) { + this._logger.log( + `============== ${this.getAssets.name} was called! ==============`, + ); + return await this.createQueryBuilder('asset') + .leftJoinAndSelect( + 'asset.tokenHolderStatistics', + 'tokenHolderStatistics', + 'DATE(tokenHolderStatistics.date) > DATE(NOW() - INTERVAL :days DAY)', + { days }, + ) + .where('asset.denom =:denom', { + denom: `${denom}`, + }) + .orWhere('asset.denom =:ibcDenom', { + ibcDenom: `ibc/${denom}`, + }) + .getMany(); + } + + /** + * Insert Or Update Asset. + * + * @param {listAsset} List - list data Asset + */ + async storeAsset(listAsset) { + return this.createQueryBuilder() + .insert() + .into(Asset) + .values(listAsset) + .orUpdate(['total_supply', 'type'], ['denom']) + .orIgnore() + .execute(); + } +} diff --git a/src/components/asset/services/asset.service.ts b/src/components/asset/services/asset.service.ts new file mode 100644 index 00000000..0e082d1c --- /dev/null +++ b/src/components/asset/services/asset.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { AkcLogger, Asset, RequestContext } from '../../../shared'; +import { AssetsRepository } from '../../asset/repositories/assets.repository'; +import { AssetParamsDto } from '../dtos/asset-params.dto'; +import { AssetsTokenMarketParamsDto } from '../dtos/cw20-token-market-params.dto'; +import { In, MoreThan, Not, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TokenHolderStatistic } from 'src/shared/entities/token-holder-statistic.entity'; +import * as moment from 'moment'; + +@Injectable() +export class AssetService { + constructor( + private readonly logger: AkcLogger, + private assetsRepository: AssetsRepository, + @InjectRepository(TokenHolderStatistic) + private readonly tokenHolderStatisticRepo: Repository, + ) {} + + async getAssets(ctx: RequestContext, param: AssetParamsDto) { + this.logger.log(ctx, `${this.getAssets.name} was called!`); + const { result, count } = await this.assetsRepository.getAssets( + param.keyword, + param.limit, + param.offset, + param.type, + ); + + const assetIds = result.map((item) => item.id); + const tokenHolders = await this.tokenHolderStatisticRepo.find({ + where: { + asset: { id: In(assetIds) }, + date: MoreThan(moment().subtract(2, 'days').toDate()), + }, + loadRelationIds: true, + }); + + result.forEach((element) => { + element.tokenHolderStatistics = tokenHolders.filter( + (item) => Number(item.asset) === element.id, + ); + }); + return { result, count }; + } + + async getAssetsDetail(ctx: RequestContext, denom: string) { + this.logger.log(ctx, `${this.getAssetsDetail.name} was called!`); + const result = await this.assetsRepository.getAssetsDetail(denom); + return result[0]; + } + + async getAssetsTokenMarket( + ctx: RequestContext, + param: AssetsTokenMarketParamsDto, + ): Promise { + this.logger.log(ctx, `${this.getAssetsTokenMarket.name} was called!`); + + if (param.denom) { + return await this.assetsRepository.find({ + where: { denom: param.denom }, + }); + } else { + return await this.assetsRepository.find({ + where: { coinId: Not('') }, + }); + } + } +} diff --git a/src/components/contract/contract.module.ts b/src/components/contract/contract.module.ts index 0588fbad..6e52eab7 100644 --- a/src/components/contract/contract.module.ts +++ b/src/components/contract/contract.module.ts @@ -9,6 +9,7 @@ import { HttpModule } from '@nestjs/axios'; import { TokenMarketsRepository } from '../cw20-token/repositories/token-markets.repository'; import { SoulboundTokenRepository } from '../soulbound-token/repositories/soulbound-token.repository'; import { ContractUtil } from '../../shared/utils/contract.util'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { ContractUtil } from '../../shared/utils/contract.util'; TypeOrmModule.forFeature([ TokenMarketsRepository, SoulboundTokenRepository, + Explorer, ]), ConfigModule, HttpModule, diff --git a/src/components/contract/services/contract.service.ts b/src/components/contract/services/contract.service.ts index 38ff7db0..7045763b 100644 --- a/src/components/contract/services/contract.service.ts +++ b/src/components/contract/services/contract.service.ts @@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config'; import { plainToClass } from 'class-transformer'; import { lastValueFrom, retry, timeout } from 'rxjs'; import { + AURA_INFO, AkcLogger, CONTRACT_STATUS, ERROR_MAP, @@ -21,9 +22,11 @@ import { VerifyCodeStepOutputDto } from '../dtos/verify-code-step-output.dto'; import { VerifyCodeIdParamsDto } from '../dtos/verify-code-id-params.dto'; import { SoulboundTokenRepository } from '../../soulbound-token/repositories/soulbound-token.repository'; import { ContractUtil } from '../../../shared/utils/contract.util'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; @Injectable() export class ContractService { - private verifyContractUrl; private chainDB: string; constructor( @@ -33,14 +36,15 @@ export class ContractService { private httpService: HttpService, private soulboundTokenRepository: SoulboundTokenRepository, private contractUtil: ContractUtil, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) { this.logger.setContext(ContractService.name); - this.verifyContractUrl = this.configService.get('VERIFY_CONTRACT_URL'); const appParams = appConfig.default(); this.chainDB = appParams.indexerV2.chainDB; } - private async getCodeDetail(codeId: number) { + private async getCodeDetail(explorer: Explorer, codeId: number) { // Attributes for contract code detail const codeAttributes = `code_id creator @@ -65,8 +69,8 @@ export class ContractService { const graphqlQuery = { query: util.format( INDEXER_API_V2.GRAPH_QL.CONTRACT_CODE_DETAIL, - this.chainDB, - this.chainDB, + explorer?.chainDb, + explorer?.chainDb, codeAttributes, ), variables: { @@ -77,7 +81,7 @@ export class ContractService { const contracts = ( await this.serviceUtil.fetchDataFromGraphQL(graphqlQuery) - ).data[this.chainDB]['code']; + ).data[explorer?.chainDb]['code']; return contracts; } @@ -87,7 +91,10 @@ export class ContractService { request: VerifyCodeIdParamsDto, ): Promise { this.logger.log(ctx, `${this.verifyCodeId.name} was called!`); - const contract = await this.getCodeDetail(request.code_id); + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + const contract = await this.getCodeDetail(explorer, request.code_id); if (contract?.length === 0) { const error = { Code: ERROR_MAP.CONTRACT_NOT_EXIST.Code, @@ -115,7 +122,14 @@ export class ContractService { wasmFile: request.wasm_file, }; const result = await lastValueFrom( - this.httpService.post(this.verifyContractUrl, properties), + this.httpService.post( + this.configService.get( + explorer?.addressPrefix === AURA_INFO.ADDRESS_PREFIX + ? 'VERIFY_CONTRACT_URL' + : `${explorer?.addressPrefix.toUpperCase()}_VERIFY_CONTRACT_URL`, + ), + properties, + ), ).then((rs) => rs.data); return result; @@ -124,13 +138,17 @@ export class ContractService { async getVerifyCodeStep(ctx: RequestContext, codeId: number) { this.logger.log(ctx, `${this.getVerifyCodeStep.name} was called!`); + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + const codeVerificationAttributes = `verify_step verification_status`; const graphqlQuery = { query: util.format( INDEXER_API_V2.GRAPH_QL.VERIFY_STEP, - this.chainDB, + explorer?.chainDb, codeVerificationAttributes, ), variables: { @@ -140,7 +158,7 @@ export class ContractService { }; const response = (await this.serviceUtil.fetchDataFromGraphQL(graphqlQuery)) - .data[this.chainDB]['code_id_verification']; + .data[explorer?.chainDb]['code_id_verification']; const verifySteps = []; @@ -195,7 +213,10 @@ export class ContractService { codeId: number, ): Promise { this.logger.log(ctx, `${this.verifyContractStatus.name} was called!`); - const contract = await this.getCodeDetail(codeId); + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + const contract = await this.getCodeDetail(explorer, codeId); if (contract?.length === 0) { const error = { Code: ERROR_MAP.CONTRACT_NOT_EXIST.Code, diff --git a/src/components/cw20-token/cw20-token.module.ts b/src/components/cw20-token/cw20-token.module.ts index 3320aa69..a1d68ce5 100644 --- a/src/components/cw20-token/cw20-token.module.ts +++ b/src/components/cw20-token/cw20-token.module.ts @@ -6,17 +6,16 @@ import { ServiceUtil } from '../../shared/utils/service.util'; import { SharedModule, TokenMarkets } from '../../shared'; import { Cw20TokenController } from './controllers/cw20-token.controller'; import { Cw20TokenService } from './services/cw20-token.service'; -import { RedisUtil } from '../../shared/utils/redis.util'; -import { AccountService } from '../account/services/account.service'; import { TokenMarketsRepository } from './repositories/token-markets.repository'; import { UserModule } from '../user/user.module'; import { IsUniqueConstraint } from './validators/is-unique.validator'; import { IsUniqueManyColumnConstraint } from './validators/is-unique-many-column.validator'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Module({ imports: [ SharedModule, - TypeOrmModule.forFeature([TokenMarkets, TokenMarketsRepository]), + TypeOrmModule.forFeature([TokenMarkets, TokenMarketsRepository, Explorer]), ConfigModule, HttpModule, UserModule, @@ -24,8 +23,6 @@ import { IsUniqueManyColumnConstraint } from './validators/is-unique-many-column providers: [ Cw20TokenService, ServiceUtil, - RedisUtil, - AccountService, IsUniqueConstraint, IsUniqueManyColumnConstraint, ], diff --git a/src/components/cw20-token/repositories/token-markets.repository.ts b/src/components/cw20-token/repositories/token-markets.repository.ts index 8852caad..d858261f 100644 --- a/src/components/cw20-token/repositories/token-markets.repository.ts +++ b/src/components/cw20-token/repositories/token-markets.repository.ts @@ -41,18 +41,23 @@ export class TokenMarketsRepository extends Repository { /** * Retrieves IBC token with statistics for a specified number of days. * + * @param {exploreId} int - chain id * @param {number} days - The number of days to retrieve token statistics for. Defaults to 2. * @return {Promise} - A Promise that resolves to an array of token market data. */ - async getIbcTokenWithStatistics(days = 2): Promise { + async getIbcTokenWithStatistics( + exploreId, + days = 2, + ): Promise { return this.createQueryBuilder('tokenMarket') .leftJoinAndSelect( 'tokenMarket.tokenHolderStatistics', 'tokenHolderStatistics', - 'DATE(tokenHolderStatistics.created_at) > DATE(NOW() - INTERVAL :days DAY)', + 'DATE(tokenHolderStatistics.date) > DATE(NOW() - INTERVAL :days DAY)', { days }, ) .where('denom is not null') + .andWhere('explorer_id = :exploreId', { exploreId }) .getMany(); } } diff --git a/src/components/cw20-token/services/cw20-token.service.ts b/src/components/cw20-token/services/cw20-token.service.ts index 29adf705..b0ded0f9 100644 --- a/src/components/cw20-token/services/cw20-token.service.ts +++ b/src/components/cw20-token/services/cw20-token.service.ts @@ -1,6 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { AkcLogger, RequestContext, TokenMarkets } from '../../../shared'; -import * as appConfig from '../../../shared/configs/configuration'; import { TokenMarketsRepository } from '../repositories/token-markets.repository'; import { Cw20TokenMarketParamsDto } from '../dtos/cw20-token-market-params.dto'; import { CreateCw20TokenDto } from '../dtos/create-cw20-token.dto'; @@ -10,27 +9,19 @@ import { plainToClass } from 'class-transformer'; import { Cw20TokenResponseDto } from '../dtos/cw20-token-response.dto'; import { IbcResponseDto } from '../dtos/ibc-response.dto'; import { UpdateIbcDto } from '../dtos/update-ibc.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { Repository } from 'typeorm'; @Injectable() export class Cw20TokenService { - private appParams; - private denom; - private minimalDenom; - private decimals; - private precisionDiv; - private chainDB; - constructor( private readonly logger: AkcLogger, private tokenMarketsRepository: TokenMarketsRepository, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) { this.logger.setContext(Cw20TokenService.name); - this.appParams = appConfig.default(); - this.denom = this.appParams.chainInfo.coinDenom; - this.minimalDenom = this.appParams.chainInfo.coinMinimalDenom; - this.decimals = this.appParams.chainInfo.coinDecimals; - this.precisionDiv = this.appParams.chainInfo.precisionDiv; - this.chainDB = this.appParams.indexerV2.chainDB; } async getTokenMarket( @@ -38,17 +29,28 @@ export class Cw20TokenService { query: Cw20TokenMarketParamsDto, ): Promise { this.logger.log(ctx, `${this.getTokenMarket.name} was called!`); + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + if (query.contractAddress) { return await this.tokenMarketsRepository.find({ where: [ - { contract_address: query.contractAddress }, - { denom: query.contractAddress }, + { + contract_address: query.contractAddress, + explorer: { id: explorer.id }, + }, + { denom: query.contractAddress, explorer: { id: explorer.id } }, ], }); } else if (query.onlyIbc === 'true') { - return await this.tokenMarketsRepository.getIbcTokenWithStatistics(); + return await this.tokenMarketsRepository.getIbcTokenWithStatistics( + explorer.id, + ); } else { - return await this.tokenMarketsRepository.find(); + return await this.tokenMarketsRepository.find({ + where: { explorer: { id: explorer.id } }, + }); } } diff --git a/src/components/export-csv/controllers/export-csv.controller.ts b/src/components/export-csv/controllers/export-csv.controller.ts index df7fb779..6a3fe1ff 100644 --- a/src/components/export-csv/controllers/export-csv.controller.ts +++ b/src/components/export-csv/controllers/export-csv.controller.ts @@ -67,22 +67,28 @@ export class ExportCsvController { } private async proccessCSV(ctx, query, res, userId = null) { - const { data, fileName, fields } = - await this.exportCsvService.exportTransactionDataToCSV( - ctx, - query, - userId, - ); + try { + const { data, fileName, fields } = + await this.exportCsvService.exportTransactionDataToCSV( + ctx, + query, + userId, + ); - const csvParser = new Parser({ - fields, - }); - const csv = csvParser.parse(data?.length > 0 ? data : {}); + const csvParser = new Parser({ + fields, + }); + const csv = csvParser.parse(data?.length > 0 ? data : {}); - res.set({ - 'Content-Type': 'application/json', - 'Content-Disposition': `attachment; filename="${fileName}"`, - }); - res.send(csv); + res.set({ + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="${fileName}"`, + }); + res.send(csv); + } catch (error) { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: { message: 'Something went wrong!' } }); + } } } diff --git a/src/components/export-csv/export-csv.module.ts b/src/components/export-csv/export-csv.module.ts index 818cbe2f..261bfb9e 100644 --- a/src/components/export-csv/export-csv.module.ts +++ b/src/components/export-csv/export-csv.module.ts @@ -10,7 +10,8 @@ import { PrivateNameTagRepository } from '../private-name-tag/repositories/priva import { EncryptionService } from '../encryption/encryption.service'; import { CipherKey } from '../../shared/entities/cipher-key.entity'; import { UserModule } from '../user/user.module'; -import { TokenMarketsRepository } from '../cw20-token/repositories/token-markets.repository'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { AssetsRepository } from '../asset/repositories/assets.repository'; @Module({ imports: [ @@ -21,7 +22,8 @@ import { TokenMarketsRepository } from '../cw20-token/repositories/token-markets TypeOrmModule.forFeature([ PrivateNameTagRepository, CipherKey, - TokenMarketsRepository, + AssetsRepository, + Explorer, ]), ], providers: [ExportCsvService, EncryptionService, ServiceUtil], diff --git a/src/components/export-csv/services/export-csv.service.ts b/src/components/export-csv/services/export-csv.service.ts index 76f9e4bf..73f19d83 100644 --- a/src/components/export-csv/services/export-csv.service.ts +++ b/src/components/export-csv/services/export-csv.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import * as appConfig from '../../../shared/configs/configuration'; import { ServiceUtil } from '../../../shared/utils/service.util'; import { + ASSETS_TYPE, AkcLogger, EXPORT_LIMIT_RECORD, INDEXER_API_V2, @@ -12,7 +13,6 @@ import { } from '../../../shared'; import { TransactionHelper } from '../../../shared/helpers/transaction.helper'; import { HttpService } from '@nestjs/axios'; -import { lastValueFrom } from 'rxjs'; import { RANGE_EXPORT, TYPE_EXPORT, @@ -20,25 +20,29 @@ import { import { ExportCsvParamDto } from '../dtos/export-csv-param.dto'; import { PrivateNameTagRepository } from '../../private-name-tag/repositories/private-name-tag.repository'; import { EncryptionService } from '../../encryption/encryption.service'; -import { IsNull, Not } from 'typeorm'; -import { TokenMarketsRepository } from '../../cw20-token/repositories/token-markets.repository'; +import { IsNull, Not, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import * as util from 'util'; +import { AssetsRepository } from '../../asset/repositories/assets.repository'; @Injectable() export class ExportCsvService { private config; - private chainDB: string; + private defaultChainDB: string; constructor( private serviceUtil: ServiceUtil, private readonly logger: AkcLogger, - private httpService: HttpService, private privateNameTagRepository: PrivateNameTagRepository, private encryptionService: EncryptionService, - private tokenMarketsRepository: TokenMarketsRepository, + private assetRepository: AssetsRepository, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) { this.logger.setContext(ExportCsvService.name); this.config = appConfig.default(); - this.chainDB = this.config.indexerV2.chainDB; + this.defaultChainDB = this.config.indexerV2.chainDB; } async exportTransactionDataToCSV( @@ -48,15 +52,18 @@ export class ExportCsvService { ) { this.logger.log(ctx, `${this.exportTransactionDataToCSV.name} was called!`); try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); switch (payload.dataType) { case TYPE_EXPORT.ExecutedTxs: - return this.executed(payload); + return this.executed(payload, explorer); case TYPE_EXPORT.AuraTxs: - return this.coinTransfer(payload, userId); + return this.coinTransfer(ctx, payload, userId, explorer); case TYPE_EXPORT.FtsTxs: - return this.tokenTransfer(payload, userId); + return this.tokenTransfer(ctx, payload, userId, explorer); case TYPE_EXPORT.NftTxs: - return this.nftTransfer(payload, userId); + return this.nftTransfer(ctx, payload, userId, explorer); default: break; } @@ -68,10 +75,10 @@ export class ExportCsvService { } } - private async executed(payload: ExportCsvParamDto) { + private async executed(payload: ExportCsvParamDto, explorer: Explorer) { const fileName = `export-account-executed-${payload.address}.csv`; const graphqlQuery = { - query: INDEXER_API_V2.GRAPH_QL.TX_EXECUTED, + query: util.format(INDEXER_API_V2.GRAPH_QL.TX_EXECUTED, explorer.chainDb), variables: { limit: QUERY_LIMIT_RECORD, address: payload.address, @@ -93,19 +100,18 @@ export class ExportCsvService { operationName: INDEXER_API_V2.OPERATION_NAME.TX_EXECUTED, }; - const response = await this.queryData(graphqlQuery); - - const envConfig = await lastValueFrom( - this.httpService.get(this.config.configUrl), - ).then((rs) => rs.data); + const response = await this.queryData(graphqlQuery, explorer.chainDb); - const coinConfig = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + const coinConfig = await this.assetRepository.find({ + where: { + type: ASSETS_TYPE.IBC, + name: Not(IsNull()), + }, }); const txs = TransactionHelper.convertDataAccountTransaction( response, - envConfig?.chainConfig?.chain_info?.currencies[0], + explorer, payload.dataType, payload.address, coinConfig, @@ -128,10 +134,18 @@ export class ExportCsvService { return { data, fileName, fields }; } - private async coinTransfer(payload: ExportCsvParamDto, userId) { + private async coinTransfer( + ctx: RequestContext, + payload: ExportCsvParamDto, + userId, + explorer: Explorer, + ) { const fileName = `export-account-native-transfer-${payload.address}.csv`; const graphqlQuery = { - query: INDEXER_API_V2.GRAPH_QL.TX_COIN_TRANSFER, + query: util.format( + INDEXER_API_V2.GRAPH_QL.TX_COIN_TRANSFER, + explorer.chainDb, + ), variables: { limit: QUERY_LIMIT_RECORD, from: payload.address, @@ -154,19 +168,18 @@ export class ExportCsvService { operationName: INDEXER_API_V2.OPERATION_NAME.TX_COIN_TRANSFER, }; - const response = await this.queryData(graphqlQuery); - - const envConfig = await lastValueFrom( - this.httpService.get(this.config.configUrl), - ).then((rs) => rs.data); + const response = await this.queryData(graphqlQuery, explorer.chainDb); - const coinConfig = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + const coinConfig = await this.assetRepository.find({ + where: { + type: ASSETS_TYPE.IBC, + name: Not(IsNull()), + }, }); const txs = TransactionHelper.convertDataAccountTransaction( response, - envConfig?.chainConfig?.chain_info?.currencies[0], + explorer, payload.dataType, payload.address, coinConfig, @@ -182,6 +195,7 @@ export class ExportCsvService { null, LIMIT_PRIVATE_NAME_TAG, 0, + ctx.chainId, ); lstPrivateName = await Promise.all( result.map(async (item) => { @@ -218,10 +232,18 @@ export class ExportCsvService { return { data, fileName, fields }; } - private async tokenTransfer(payload: ExportCsvParamDto, userId) { + private async tokenTransfer( + ctx: RequestContext, + payload: ExportCsvParamDto, + userId, + explorer: Explorer, + ) { const fileName = `export-account-cw20-transfer-${payload.address}.csv`; const graphqlQuery = { - query: INDEXER_API_V2.GRAPH_QL.TX_TOKEN_TRANSFER, + query: util.format( + INDEXER_API_V2.GRAPH_QL.TX_TOKEN_TRANSFER, + explorer.chainDb, + ), variables: { limit: QUERY_LIMIT_RECORD, receiver: payload.address, @@ -253,19 +275,18 @@ export class ExportCsvService { operationName: INDEXER_API_V2.OPERATION_NAME.TX_TOKEN_TRANSFER, }; - const response = await this.queryData(graphqlQuery); - - const envConfig = await lastValueFrom( - this.httpService.get(this.config.configUrl), - ).then((rs) => rs.data); + const response = await this.queryData(graphqlQuery, explorer.chainDb); - const coinConfig = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + const coinConfig = await this.assetRepository.find({ + where: { + type: ASSETS_TYPE.IBC, + name: Not(IsNull()), + }, }); const txs = TransactionHelper.convertDataAccountTransaction( response, - envConfig?.chainConfig?.chain_info?.currencies[0], + explorer, payload.dataType, payload.address, coinConfig, @@ -281,6 +302,7 @@ export class ExportCsvService { null, LIMIT_PRIVATE_NAME_TAG, 0, + ctx.chainId, ); lstPrivateName = await Promise.all( result.map(async (item) => { @@ -317,10 +339,18 @@ export class ExportCsvService { return { data, fileName, fields }; } - private async nftTransfer(payload: ExportCsvParamDto, userId) { + private async nftTransfer( + ctx: RequestContext, + payload: ExportCsvParamDto, + userId, + explorer: Explorer, + ) { const fileName = `export-account-nft-transfer-${payload.address}.csv`; const graphqlQuery = { - query: INDEXER_API_V2.GRAPH_QL.TX_NFT_TRANSFER, + query: util.format( + INDEXER_API_V2.GRAPH_QL.TX_NFT_TRANSFER, + explorer.chainDb, + ), variables: { limit: QUERY_LIMIT_RECORD, receiver: payload.address, @@ -344,19 +374,18 @@ export class ExportCsvService { operationName: INDEXER_API_V2.OPERATION_NAME.TX_NFT_TRANSFER, }; - const response = await this.queryData(graphqlQuery); + const response = await this.queryData(graphqlQuery, explorer.chainDb); - const envConfig = await lastValueFrom( - this.httpService.get(this.config.configUrl), - ).then((rs) => rs.data); - - const coinConfig = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + const coinConfig = await this.assetRepository.find({ + where: { + type: ASSETS_TYPE.IBC, + name: Not(IsNull()), + }, }); const txs = TransactionHelper.convertDataAccountTransaction( response, - envConfig?.chainConfig?.chain_info?.currencies[0], + explorer, payload.dataType, payload.address, coinConfig, @@ -372,6 +401,7 @@ export class ExportCsvService { null, LIMIT_PRIVATE_NAME_TAG, 0, + ctx.chainId, ); lstPrivateName = await Promise.all( result.map(async (item) => { @@ -407,7 +437,7 @@ export class ExportCsvService { return { data, fileName, fields }; } - private async queryData(graphqlQuery) { + private async queryData(graphqlQuery, chainDB = this.defaultChainDB) { const result = { transaction: [] }; let next = true; let timesLoop = 0; @@ -419,7 +449,7 @@ export class ExportCsvService { ) { const response = ( await this.serviceUtil.fetchDataFromGraphQL(graphqlQuery) - )?.data[this.chainDB]; + )?.data[chainDB]; // break loop when horoscope return no data if (!response) { diff --git a/src/components/notification/notification.module.ts b/src/components/notification/notification.module.ts index 773d198c..8721ae09 100644 --- a/src/components/notification/notification.module.ts +++ b/src/components/notification/notification.module.ts @@ -8,11 +8,12 @@ import { NotificationService } from './services/notification.service'; import { NotificationController } from './controllers/notification.controller'; import { UserModule } from '../user/user.module'; import { UserActivity } from '../../shared/entities/user-activity.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Module({ imports: [ SharedModule, - TypeOrmModule.forFeature([NotificationRepository, UserActivity]), + TypeOrmModule.forFeature([NotificationRepository, UserActivity, Explorer]), HttpModule, ConfigModule, UserModule, diff --git a/src/components/notification/services/notification.service.ts b/src/components/notification/services/notification.service.ts index caf23afd..d6e24202 100644 --- a/src/components/notification/services/notification.service.ts +++ b/src/components/notification/services/notification.service.ts @@ -5,6 +5,7 @@ import { NotificationParamsDto } from '../dtos/get-notification-param.dto'; import { Repository, UpdateResult } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { UserActivity } from '../../../shared/entities/user-activity.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Injectable() export class NotificationService { @@ -13,13 +14,20 @@ export class NotificationService { private notificationRepository: NotificationRepository, @InjectRepository(UserActivity) private userActivityRepository: Repository, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) {} async getNotifications(ctx: RequestContext, param: NotificationParamsDto) { this.logger.log(ctx, `${this.getNotifications.name} was called!`); + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + return await this.notificationRepository.getNotifications( ctx.user.id, + explorer?.id, param, ); } @@ -37,18 +45,24 @@ export class NotificationService { async readAllNotification(ctx: RequestContext): Promise { this.logger.log(ctx, `${this.readAllNotification.name} was called!`); + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); return await this.notificationRepository.update( - { user_id: ctx.user.id, is_read: false }, + { user_id: ctx.user.id, explorer: { id: explorer?.id }, is_read: false }, { is_read: true }, ); } async getDailyQuotaNotification(ctx: RequestContext): Promise { this.logger.log(ctx, `${this.getDailyQuotaNotification.name} was called!`); - + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); const userActivities = await this.userActivityRepository.findOne({ where: { user: { id: ctx.user.id }, + explorer: { id: explorer?.id }, type: USER_ACTIVITIES.DAILY_NOTIFICATIONS, }, }); diff --git a/src/components/private-name-tag/private-name-tag.module.ts b/src/components/private-name-tag/private-name-tag.module.ts index e4c09ff8..435bdd60 100644 --- a/src/components/private-name-tag/private-name-tag.module.ts +++ b/src/components/private-name-tag/private-name-tag.module.ts @@ -10,11 +10,12 @@ import { UserModule } from '../user/user.module'; import { EncryptionService } from '../encryption/encryption.service'; import { CipherKey } from '../../shared/entities/cipher-key.entity'; import { ServiceUtil } from '../../shared/utils/service.util'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Module({ imports: [ SharedModule, - TypeOrmModule.forFeature([PrivateNameTagRepository, CipherKey]), + TypeOrmModule.forFeature([PrivateNameTagRepository, CipherKey, Explorer]), HttpModule, ConfigModule, UserModule, diff --git a/src/components/private-name-tag/repositories/private-name-tag.repository.ts b/src/components/private-name-tag/repositories/private-name-tag.repository.ts index 0f7a341b..15702f17 100644 --- a/src/components/private-name-tag/repositories/private-name-tag.repository.ts +++ b/src/components/private-name-tag/repositories/private-name-tag.repository.ts @@ -21,6 +21,7 @@ export class PrivateNameTagRepository extends Repository { keywordEncrypt: string, limit: number, offset: number, + chainId: string, ) { this._logger.log( `============== ${this.getNameTags.name} was called! ==============`, @@ -37,7 +38,9 @@ export class PrivateNameTagRepository extends Repository { tag.updated_at as updatedAt`, ) .leftJoin(User, 'user', 'user.id = tag.created_by') - .where('tag.created_by = :user_id', { user_id }); + .leftJoin('tag.explorer', 'explorer') + .where('tag.created_by = :user_id', { user_id }) + .andWhere('chain_id = :chainId', { chainId }); const _finalizeResult = async () => { const result = await builder diff --git a/src/components/private-name-tag/services/private-name-tag.service.ts b/src/components/private-name-tag/services/private-name-tag.service.ts index fb4161ed..a78fe32f 100644 --- a/src/components/private-name-tag/services/private-name-tag.service.ts +++ b/src/components/private-name-tag/services/private-name-tag.service.ts @@ -17,12 +17,12 @@ import { CreatePrivateNameTagParamsDto } from '../dtos/create-private-name-tag-p import { PrivateNameTag } from '../../../shared/entities/private-name-tag.entity'; import { UpdatePrivateNameTagParamsDto } from '../dtos/update-private-name-tag-params.dto'; import { EncryptionService } from '../../encryption/encryption.service'; -import { - ServiceUtil, - isValidBench32Address, -} from '../../../shared/utils/service.util'; -import { Not } from 'typeorm'; +import { isValidBench32Address } from '../../../shared/utils/service.util'; +import { Not, Repository } from 'typeorm'; import * as appConfig from '../../../shared/configs/configuration'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import * as util from 'util'; @Injectable() export class PrivateNameTagService { @@ -32,29 +32,37 @@ export class PrivateNameTagService { private readonly logger: AkcLogger, private encryptionService: EncryptionService, private privateNameTagRepository: PrivateNameTagRepository, - private serviceUtil: ServiceUtil, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) { this.config = appConfig.default(); } async getNameTags(ctx: RequestContext, req: PrivateNameTagParamsDto) { - this.logger.log(ctx, `${this.getNameTags.name} was called!`); - const { result, count, countFavorite } = - await this.privateNameTagRepository.getNameTags( - ctx.user.id, - req.keyword, - await this.encryptionService.encrypt(req.keyword ?? ''), - req.limit, - req.offset, + try { + this.logger.log(ctx, `${this.getNameTags.name} was called!`); + + const { result, count, countFavorite } = + await this.privateNameTagRepository.getNameTags( + ctx.user.id, + req.keyword, + await this.encryptionService.encrypt(req.keyword ?? ''), + req.limit, + req.offset, + ctx.chainId, + ); + const data = await Promise.all( + result.map(async (item) => { + item.nameTag = await this.encryptionService.decrypt(item.nameTag); + return item; + }), ); - const data = await Promise.all( - result.map(async (item) => { - item.nameTag = await this.encryptionService.decrypt(item.nameTag); - return item; - }), - ); - - return { data, count, countFavorite }; + + return { data, count, countFavorite }; + } catch (error) { + this.logger.error(ctx, error); + throw new BadRequestException(error.message); + } } async getNameTagsDetail(ctx: RequestContext, id: number) { @@ -71,20 +79,24 @@ export class PrivateNameTagService { } async createNameTag(ctx: RequestContext, req: CreatePrivateNameTagParamsDto) { - this.logger.log(ctx, `${this.createNameTag.name} was called!`); - const errorMsg = await this.validate(0, ctx.user.id, req); - if (errorMsg) { - return errorMsg; - } - const entity = new PrivateNameTag(); - entity.address = req.address; - entity.isFavorite = req.isFavorite; - entity.type = req.type; - entity.note = req.note; - entity.nameTag = await this.encryptionService.encrypt(req.nameTag); - entity.createdBy = ctx.user.id; - try { + this.logger.log(ctx, `${this.createNameTag.name} was called!`); + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + const errorMsg = await this.validate(0, ctx, req); + if (errorMsg) { + return errorMsg; + } + const entity = new PrivateNameTag(); + entity.address = req.address; + entity.isFavorite = req.isFavorite; + entity.type = req.type; + entity.note = req.note; + entity.nameTag = await this.encryptionService.encrypt(req.nameTag); + entity.createdBy = ctx.user.id; + entity.explorer = explorer; + const result = await this.privateNameTagRepository.save(entity); return { data: result, meta: {} }; } catch (err) { @@ -103,7 +115,7 @@ export class PrivateNameTagService { ) { this.logger.log(ctx, `${this.updateNameTag.name} was called!`); const request: CreatePrivateNameTagParamsDto = { ...req, address: '' }; - const errorMsg = await this.validate(id, ctx.user.id, request, false); + const errorMsg = await this.validate(id, ctx, request, false); if (errorMsg) { return errorMsg; } @@ -156,10 +168,15 @@ export class PrivateNameTagService { private async validate( id: number, - user_id: number, + ctx: RequestContext, req: CreatePrivateNameTagParamsDto, isCreate = true, ) { + const user_id = ctx.user.id; + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + if (req.nameTag && !req.nameTag?.match(REGEX_PARTERN.NAME_TAG)) { return { code: ADMIN_ERROR_MAP.INVALID_NAME_TAG.Code, @@ -168,7 +185,10 @@ export class PrivateNameTagService { } if (isCreate) { - const validFormat = await isValidBench32Address(req.address); + const validFormat = isValidBench32Address( + req.address, + explorer.addressPrefix, + ); if ( !validFormat || @@ -179,7 +199,10 @@ export class PrivateNameTagService { ) { return { code: ADMIN_ERROR_MAP.INVALID_FORMAT.Code, - message: ADMIN_ERROR_MAP.INVALID_FORMAT.Message, + message: util.format( + ADMIN_ERROR_MAP.INVALID_FORMAT.Message, + explorer.addressPrefix, + ), }; } diff --git a/src/components/public-name-tag/controllers/public-name-tag.controller.ts b/src/components/public-name-tag/controllers/public-name-tag.controller.ts index 1fc0319b..e583b456 100644 --- a/src/components/public-name-tag/controllers/public-name-tag.controller.ts +++ b/src/components/public-name-tag/controllers/public-name-tag.controller.ts @@ -171,9 +171,14 @@ export class PublicNameTagController { description: 'Key for next page.', }) async getNameTag( + @ReqContext() ctx: RequestContext, @Query('limit') limit?: number, @Query('nextKey') nextKey?: number, ): Promise { - return await this.nameTagService.getNameTagMainSite({ limit, nextKey }); + return await this.nameTagService.getNameTagMainSite({ + limit, + nextKey, + chainId: ctx.chainId, + }); } } diff --git a/src/components/public-name-tag/dtos/update-public-name-tag-params.dto.ts b/src/components/public-name-tag/dtos/update-public-name-tag-params.dto.ts index ceefb6a0..d5646e5c 100644 --- a/src/components/public-name-tag/dtos/update-public-name-tag-params.dto.ts +++ b/src/components/public-name-tag/dtos/update-public-name-tag-params.dto.ts @@ -1,5 +1,4 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { NAME_TAG_TYPE } from '../../../shared'; import { IsOptional, MaxLength } from 'class-validator'; export class UpdatePublicNameTagParamsDto { @@ -15,4 +14,7 @@ export class UpdatePublicNameTagParamsDto { default: null, }) enterpriseUrl: string; + + @ApiProperty({ default: '' }) + address: string; } diff --git a/src/components/public-name-tag/public-name-tag.module.ts b/src/components/public-name-tag/public-name-tag.module.ts index b2ab8a26..bbccc466 100644 --- a/src/components/public-name-tag/public-name-tag.module.ts +++ b/src/components/public-name-tag/public-name-tag.module.ts @@ -7,12 +7,13 @@ import { PublicNameTagController } from './controllers/public-name-tag.controlle import { PublicNameTagRepository } from './repositories/public-name-tag.repository'; import { PublicNameTagService } from './services/public-name-tag.service'; import { UserModule } from '../user/user.module'; -import { ServiceUtil } from 'src/shared/utils/service.util'; +import { ServiceUtil } from '../../shared/utils/service.util'; +import { Explorer } from '../../shared/entities/explorer.entity'; @Module({ imports: [ SharedModule, - TypeOrmModule.forFeature([PublicNameTagRepository]), + TypeOrmModule.forFeature([PublicNameTagRepository, Explorer]), HttpModule, ConfigModule, UserModule, diff --git a/src/components/public-name-tag/repositories/public-name-tag.repository.ts b/src/components/public-name-tag/repositories/public-name-tag.repository.ts index 12200163..8fd033b1 100644 --- a/src/components/public-name-tag/repositories/public-name-tag.repository.ts +++ b/src/components/public-name-tag/repositories/public-name-tag.repository.ts @@ -15,7 +15,12 @@ export class PublicNameTagRepository extends Repository { * @param offset * @returns */ - async getPublicNameTags(keyword: string, limit: number, offset: number) { + async getPublicNameTags( + keyword: string, + limit: number, + offset: number, + explorerID: number, + ) { this._logger.log( `============== ${this.getPublicNameTags.name} was called! ==============`, ); @@ -29,7 +34,8 @@ export class PublicNameTagRepository extends Repository { user.email, enterprise_url as enterpriseUrl`, ) - .leftJoin(User, 'user', 'user.id = public_name_tag.updated_by'); + .leftJoin(User, 'user', 'user.id = public_name_tag.updated_by') + .where('explorer_id = :explorerID', { explorerID }); const _finalizeResult = async () => { const result = await builder @@ -60,6 +66,7 @@ export class PublicNameTagRepository extends Repository { async getNameTagMainSite( limit: number, nextKey: number, + explorerId: number, ): Promise { limit = Number(limit) || PAGE_REQUEST.MAX_500; @@ -67,8 +74,15 @@ export class PublicNameTagRepository extends Repository { limit = PAGE_REQUEST.MAX_500; } - let qb = this.createQueryBuilder() - .select(['id', 'address', 'name_tag', 'enterprise_url as enterpriseUrl']) + let qb = this.createQueryBuilder('publicNameTag') + .select([ + 'publicNameTag.id as id', + 'address', + 'name_tag', + 'enterprise_url as enterpriseUrl', + ]) + .leftJoin('publicNameTag.explorer', 'explorer') + .where('explorer_id = :explorerId', { explorerId }) .limit(Number(limit) || PAGE_REQUEST.MAX_500); if (nextKey) { diff --git a/src/components/public-name-tag/services/public-name-tag.service.ts b/src/components/public-name-tag/services/public-name-tag.service.ts index eb285d1c..e3047bcd 100644 --- a/src/components/public-name-tag/services/public-name-tag.service.ts +++ b/src/components/public-name-tag/services/public-name-tag.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { ADMIN_ERROR_MAP, AkcLogger, @@ -12,30 +12,39 @@ import { PublicNameTagRepository } from '../repositories/public-name-tag.reposit import { StorePublicNameTagParamsDto } from '../dtos/store-public-name-tag-params.dto'; import { PublicNameTag } from '../../../shared/entities/public-name-tag.entity'; import { GetPublicNameTagResult } from '../dtos/get-public-name-tag-result.dto'; -import { Not } from 'typeorm'; -import { - ServiceUtil, - isValidBench32Address, -} from '../../../shared/utils/service.util'; +import { Not, Repository } from 'typeorm'; +import { isValidBench32Address } from '../../../shared/utils/service.util'; import { UpdatePublicNameTagParamsDto } from '../dtos/update-public-name-tag-params.dto'; +import { Explorer } from '../../../shared/entities/explorer.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as util from 'util'; @Injectable() export class PublicNameTagService { constructor( private readonly logger: AkcLogger, private nameTagRepository: PublicNameTagRepository, - private serviceUtil: ServiceUtil, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) {} async getPublicNameTags(ctx: RequestContext, req: PublicNameTagParamsDto) { this.logger.log(ctx, `${this.getPublicNameTags.name} was called!`); - const { result, count } = await this.nameTagRepository.getPublicNameTags( - req.keyword, - req.limit, - req.offset, - ); + try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + const { result, count } = await this.nameTagRepository.getPublicNameTags( + req.keyword, + req.limit, + req.offset, + explorer.id, + ); - return { result, count }; + return { result, count }; + } catch (error) { + throw new BadRequestException(error.message); + } } async getPublicNameTagsDetail(ctx: RequestContext, id: number) { @@ -49,17 +58,24 @@ export class PublicNameTagService { req: StorePublicNameTagParamsDto, ) { this.logger.log(ctx, `${this.createPublicNameTag.name} was called!`); - const errorMsg = await this.validate(req); + const errorMsg = await this.validate(ctx, req); if (errorMsg) { return errorMsg; } - const entity = new PublicNameTag(); - entity.address = req.address; - entity.type = req.type; - entity.name_tag = req.nameTag; - entity.updated_by = userId; - entity.enterpriseUrl = req.enterpriseUrl; + try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + + const entity = new PublicNameTag(); + entity.address = req.address; + entity.type = req.type; + entity.name_tag = req.nameTag; + entity.updated_by = userId; + entity.enterpriseUrl = req.enterpriseUrl; + entity.explorer = explorer; + const result = await this.nameTagRepository.save(entity); return { data: result, meta: {} }; } catch (err) { @@ -67,6 +83,7 @@ export class PublicNameTagService { ctx, `Class ${PublicNameTagService.name} call ${this.createPublicNameTag.name} error ${err?.code} method error: ${err?.stack}`, ); + throw new BadRequestException(err.message); } } @@ -76,7 +93,7 @@ export class PublicNameTagService { req: UpdatePublicNameTagParamsDto, ) { this.logger.log(ctx, `${this.updatePublicNameTag.name} was called!`); - const errorMsg = await this.validate(req, false); + const errorMsg = await this.validate(ctx, req, false); if (errorMsg) { return errorMsg; } @@ -92,6 +109,7 @@ export class PublicNameTagService { ctx, `Class ${PublicNameTagService.name} call ${this.updatePublicNameTag.name} error ${err?.code} method error: ${err?.stack}`, ); + throw new BadRequestException(err.message); } } @@ -107,7 +125,11 @@ export class PublicNameTagService { } } - private async validate(req: any, isCreate = true) { + private async validate(ctx: any, req: any, isCreate = true) { + const explorer = await this.explorerRepository.findOne({ + chainId: ctx.chainId, + }); + if (!req.nameTag.match(REGEX_PARTERN.NAME_TAG)) { return { code: ADMIN_ERROR_MAP.INVALID_NAME_TAG.Code, @@ -123,7 +145,11 @@ export class PublicNameTagService { } if (isCreate) { - const validFormat = await isValidBench32Address(req.address); + const validFormat = isValidBench32Address( + req.address, + explorer?.addressPrefix, + req.type, + ); if ( !validFormat || @@ -134,7 +160,10 @@ export class PublicNameTagService { ) { return { code: ADMIN_ERROR_MAP.INVALID_FORMAT.Code, - message: ADMIN_ERROR_MAP.INVALID_FORMAT.Message, + message: util.format( + ADMIN_ERROR_MAP.INVALID_FORMAT.Message, + explorer.addressPrefix, + ), }; } // check duplicate address @@ -153,6 +182,7 @@ export class PublicNameTagService { where: { name_tag: req.nameTag, address: Not(req.address), + explorer: { id: explorer.id }, }, }); @@ -169,22 +199,32 @@ export class PublicNameTagService { async getNameTagMainSite(req: { limit: number; nextKey: number; + chainId: string; }): Promise { - const nameTags = await this.nameTagRepository.getNameTagMainSite( - Number(req.limit), - Number(req.nextKey), - ); - - const nextKey = nameTags.slice(-1)[0]?.id; - - const data = { - data: { - nameTags: nameTags, - count: Number(nameTags.length), - nextKey: nextKey || null, - }, - }; + try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: req.chainId, + }); - return data; + const nameTags = await this.nameTagRepository.getNameTagMainSite( + Number(req.limit), + Number(req.nextKey), + explorer.id, + ); + + const nextKey = nameTags.slice(-1)[0]?.id; + + const data = { + data: { + nameTags: nameTags, + count: Number(nameTags.length), + nextKey: nextKey || null, + }, + }; + + return data; + } catch (error) { + throw new BadRequestException(error.message); + } } } diff --git a/src/components/queues/notification/dtos/notification.dtos.ts b/src/components/queues/notification/dtos/notification.dtos.ts index 86e60335..8ef8de1b 100644 --- a/src/components/queues/notification/dtos/notification.dtos.ts +++ b/src/components/queues/notification/dtos/notification.dtos.ts @@ -1,3 +1,5 @@ +import { Explorer } from 'src/shared/entities/explorer.entity'; + export class NotificationDto { title: string; body: any; @@ -7,4 +9,5 @@ export class NotificationDto { type: string; user_id: number; height: number; + explorer: Explorer; } diff --git a/src/components/queues/notification/notification.module.ts b/src/components/queues/notification/notification.module.ts index 436fafc8..4a5ae1b5 100644 --- a/src/components/queues/notification/notification.module.ts +++ b/src/components/queues/notification/notification.module.ts @@ -17,7 +17,8 @@ import { UserActivity } from '../../../shared/entities/user-activity.entity'; import { NotificationRepository } from './repositories/notification.repository'; import { WatchList } from '../../../shared/entities/watch-list.entity'; import { User } from '../../../shared/entities/user.entity'; -import { TokenMarketsRepository } from '../../cw20-token/repositories/token-markets.repository'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { AssetsRepository } from '../../asset/repositories/assets.repository'; @Module({ imports: [ @@ -29,12 +30,13 @@ import { TokenMarketsRepository } from '../../cw20-token/repositories/token-mark PublicNameTagRepository, NotificationTokenRepository, NotificationRepository, - TokenMarketsRepository, + AssetsRepository, SyncPointRepository, CipherKey, UserActivity, User, WatchList, + Explorer, ]), BullModule.registerQueueAsync({ name: QUEUES.NOTIFICATION.QUEUE_NAME, diff --git a/src/components/queues/notification/notification.processor.ts b/src/components/queues/notification/notification.processor.ts index b8bef826..18a04285 100644 --- a/src/components/queues/notification/notification.processor.ts +++ b/src/components/queues/notification/notification.processor.ts @@ -23,6 +23,7 @@ import { SyncPointRepository } from '../../sync-point/repositories/sync-point.re import { lastValueFrom } from 'rxjs'; import { HttpService } from '@nestjs/axios'; import * as firebaseAdmin from 'firebase-admin'; +import * as util from 'util'; import { PrivateNameTagRepository } from '../../private-name-tag/repositories/private-name-tag.repository'; import { PublicNameTagRepository } from '../../public-name-tag/repositories/public-name-tag.repository'; import { NotificationTokenRepository } from './repositories/notification-token.repository'; @@ -37,12 +38,11 @@ import { SyncPoint } from '../../../shared/entities/sync-point.entity'; import { EncryptionService } from '../../../components/encryption/encryption.service'; import { NotificationToken } from '../../../shared/entities/notification-token.entity'; import { User } from '../../../shared/entities/user.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Processor(QUEUES.NOTIFICATION.QUEUE_NAME) export class NotificationProcessor { private readonly logger = new Logger(NotificationProcessor.name); - private indexerChainId; - private chainDB; private notificationConfig; constructor( @@ -63,6 +63,8 @@ export class NotificationProcessor { @InjectRepository(WatchList) private watchListRepository: Repository, @InjectQueue(QUEUES.NOTIFICATION.QUEUE_NAME) private readonly queue: Queue, + @InjectRepository(Explorer) + private readonly explorerRepository: Repository, ) { this.logger.log( '============== Constructor NotificationProcessor Service ==============', @@ -78,41 +80,9 @@ export class NotificationProcessor { clientEmail: this.notificationConfig.fcmClientEmail, }), }); + } - this.indexerChainId = this.configService.get('indexer.chainId'); - - this.queue.add( - QUEUES.NOTIFICATION.JOBS.NOTIFICATION_EXECUTED, - {}, - { - repeat: { cron: CronExpression.EVERY_30_SECONDS }, - }, - ); - - this.queue.add( - QUEUES.NOTIFICATION.JOBS.NOTIFICATION_COIN_TRANSFER, - {}, - { - repeat: { cron: CronExpression.EVERY_30_SECONDS }, - }, - ); - - this.queue.add( - QUEUES.NOTIFICATION.JOBS.NOTIFICATION_NFT_TRANSFER, - {}, - { - repeat: { cron: CronExpression.EVERY_30_SECONDS }, - }, - ); - - this.queue.add( - QUEUES.NOTIFICATION.JOBS.NOTIFICATION_TOKEN_TRANSFER, - {}, - { - repeat: { cron: CronExpression.EVERY_30_SECONDS }, - }, - ); - + async onModuleInit() { this.queue.add( QUEUES.NOTIFICATION.JOBS.RESET_NOTIFICATION, {}, @@ -121,28 +91,69 @@ export class NotificationProcessor { }, ); - this.chainDB = configService.get('indexerV2.chainDB'); + const explorer = await this.explorerRepository.find(); + explorer?.forEach((item, index) => { + // Every 30s + index + const cronTime = 30 + index; + this.queue.add( + QUEUES.NOTIFICATION.JOBS.NOTIFICATION_EXECUTED, + { explorer: item }, + { + repeat: { cron: `*/${cronTime.toString()} * * * * *` }, + }, + ); + this.queue.add( + QUEUES.NOTIFICATION.JOBS.NOTIFICATION_COIN_TRANSFER, + { explorer: item }, + { + repeat: { cron: `*/${cronTime.toString()} * * * * *` }, + }, + ); + this.queue.add( + QUEUES.NOTIFICATION.JOBS.NOTIFICATION_NFT_TRANSFER, + { explorer: item }, + { + repeat: { cron: `*/${cronTime.toString()} * * * * *` }, + }, + ); + this.queue.add( + QUEUES.NOTIFICATION.JOBS.NOTIFICATION_TOKEN_TRANSFER, + { explorer: item }, + { + repeat: { cron: `*/${cronTime.toString()} * * * * *` }, + }, + ); + }); } @Process(QUEUES.NOTIFICATION.JOBS.NOTIFICATION_EXECUTED) - async notificationExecuted() { + async notificationExecuted(job: Job) { try { + const explorer: Explorer = job.data.explorer; + const currentTxHeight = await this.syncPointRepos.findOne({ where: { type: SYNC_POINT_TYPE.EXECUTED_HEIGHT, + explorer: { id: explorer.id }, }, }); if (!currentTxHeight) { - await this.updateBlockNotification(SYNC_POINT_TYPE.EXECUTED_HEIGHT); + await this.updateBlockNotification( + SYNC_POINT_TYPE.EXECUTED_HEIGHT, + explorer, + ); return; } // Get watch list - const watchList = await this.queryWatchList(); + const watchList = await this.queryWatchList(explorer); if (watchList.length > 0) { const graphQlQuery = { - query: INDEXER_API_V2.GRAPH_QL.EXECUTED_NOTIFICATION, + query: util.format( + INDEXER_API_V2.GRAPH_QL.EXECUTED_NOTIFICATION, + explorer.chainDb, + ), variables: { heightGT: currentTxHeight.point, }, @@ -151,7 +162,7 @@ export class NotificationProcessor { const response = ( await this.serviceUtil.fetchDataFromGraphQL(graphQlQuery) - )?.data[this.chainDB]; + )?.data[explorer.chainDb]; if (response?.executed?.length > 0) { // get list address let listAddress = []; @@ -164,7 +175,7 @@ export class NotificationProcessor { // Pre-Process const { notificationTokens, privateNameTags, publicNameTags } = - await this.preProcessNotification(null, listAddress); + await this.preProcessNotification(null, listAddress, explorer); // Get executed notification const notifications = @@ -176,6 +187,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Process notification and push to firebase @@ -184,6 +196,7 @@ export class NotificationProcessor { notificationTokens, currentTxHeight, response?.executed[0], + explorer, ); } } @@ -193,26 +206,32 @@ export class NotificationProcessor { } @Process(QUEUES.NOTIFICATION.JOBS.NOTIFICATION_COIN_TRANSFER) - async notificationCoinTransfer() { + async notificationCoinTransfer(job: Job) { try { + const explorer: Explorer = job.data.explorer; const currentTxHeight = await this.syncPointRepos.findOne({ where: { type: SYNC_POINT_TYPE.COIN_TRANSFER_HEIGHT, + explorer: { id: explorer.id }, }, }); if (!currentTxHeight) { await this.updateBlockNotification( SYNC_POINT_TYPE.COIN_TRANSFER_HEIGHT, + explorer, ); return; } // Get watch list - const watchList = await this.queryWatchList(); + const watchList = await this.queryWatchList(explorer); if (watchList.length > 0) { const graphQlQuery = { - query: INDEXER_API_V2.GRAPH_QL.COIN_TRANSFER_NOTIFICATION, + query: util.format( + INDEXER_API_V2.GRAPH_QL.COIN_TRANSFER_NOTIFICATION, + explorer.chainDb, + ), variables: { heightGT: currentTxHeight.point, }, @@ -222,12 +241,13 @@ export class NotificationProcessor { const response = ( await this.serviceUtil.fetchDataFromGraphQL(graphQlQuery) - )?.data[this.chainDB]; + )?.data[explorer.chainDb]; if (response?.coin_transfer?.length > 0) { // Convert data coin transfer const listTx = await this.notificationUtil.convertDataCoinTransfer( response.coin_transfer, + explorer, ); // Get list address @@ -243,7 +263,7 @@ export class NotificationProcessor { publicNameTags, notifyReceived, notifySent, - } = await this.preProcessNotification(listTx, listAddress); + } = await this.preProcessNotification(listTx, listAddress, explorer); // Get received native coin notification const coinTransferReceived = @@ -255,6 +275,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Get sent native coin notification @@ -267,6 +288,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Process notification and push to firebase @@ -275,6 +297,7 @@ export class NotificationProcessor { notificationTokens, currentTxHeight, response?.coin_transfer[0], + explorer, ); } } @@ -284,26 +307,32 @@ export class NotificationProcessor { } @Process(QUEUES.NOTIFICATION.JOBS.NOTIFICATION_TOKEN_TRANSFER) - async notificationTokenTransfer() { + async notificationTokenTransfer(job: Job) { try { + const explorer: Explorer = job.data.explorer; const currentTxHeight = await this.syncPointRepos.findOne({ where: { type: SYNC_POINT_TYPE.TOKEN_TRANSFER_HEIGHT, + explorer: { id: explorer.id }, }, }); if (!currentTxHeight) { await this.updateBlockNotification( SYNC_POINT_TYPE.TOKEN_TRANSFER_HEIGHT, + explorer, ); return; } // Get watch list - const watchList = await this.queryWatchList(); + const watchList = await this.queryWatchList(explorer); if (watchList.length > 0) { const graphQlQuery = { - query: INDEXER_API_V2.GRAPH_QL.TOKEN_TRANSFER_NOTIFICATION, + query: util.format( + INDEXER_API_V2.GRAPH_QL.TOKEN_TRANSFER_NOTIFICATION, + explorer.chainDb, + ), variables: { heightGT: currentTxHeight.point, listFilterCW20: [ @@ -322,7 +351,7 @@ export class NotificationProcessor { const response = ( await this.serviceUtil.fetchDataFromGraphQL(graphQlQuery) - )?.data[this.chainDB]; + )?.data[explorer.chainDb]; if (response?.token_transfer?.length > 0) { // Get list address @@ -341,6 +370,7 @@ export class NotificationProcessor { } = await this.preProcessNotification( response.token_transfer, listAddress, + explorer, ); // Get received token notification @@ -352,6 +382,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Get sent token notification @@ -363,6 +394,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Process notification and push to firebase @@ -371,6 +403,7 @@ export class NotificationProcessor { notificationTokens, currentTxHeight, response?.token_transfer[0], + explorer, ); } } @@ -380,24 +413,32 @@ export class NotificationProcessor { } @Process(QUEUES.NOTIFICATION.JOBS.NOTIFICATION_NFT_TRANSFER) - async notificationNftTransfer() { + async notificationNftTransfer(job: Job) { try { + const explorer: Explorer = job.data.explorer; const currentTxHeight = await this.syncPointRepos.findOne({ where: { type: SYNC_POINT_TYPE.NFT_TRANSFER_HEIGHT, + explorer: { id: explorer.id }, }, }); if (!currentTxHeight) { - await this.updateBlockNotification(SYNC_POINT_TYPE.NFT_TRANSFER_HEIGHT); + await this.updateBlockNotification( + SYNC_POINT_TYPE.NFT_TRANSFER_HEIGHT, + explorer, + ); return; } // Get watch list - const watchList = await this.queryWatchList(); + const watchList = await this.queryWatchList(explorer); if (watchList.length > 0) { const graphQlQuery = { - query: INDEXER_API_V2.GRAPH_QL.NFT_TRANSFER_NOTIFICATION, + query: util.format( + INDEXER_API_V2.GRAPH_QL.NFT_TRANSFER_NOTIFICATION, + explorer.chainDb, + ), variables: { heightGT: currentTxHeight.point, listFilterCW721: ['mint', 'burn', 'transfer_nft', 'send_nft'], @@ -408,7 +449,7 @@ export class NotificationProcessor { const response = ( await this.serviceUtil.fetchDataFromGraphQL(graphQlQuery) - )?.data[this.chainDB]; + )?.data[explorer.chainDb]; if (response?.nft_transfer?.length > 0) { // Get list address @@ -427,6 +468,7 @@ export class NotificationProcessor { } = await this.preProcessNotification( response.nft_transfer, listAddress, + explorer, ); // Get sent nft notification @@ -438,6 +480,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Get received nft notification @@ -449,6 +492,7 @@ export class NotificationProcessor { ), privateNameTags, publicNameTags, + explorer, ); // Process notification and push to firebase @@ -457,6 +501,7 @@ export class NotificationProcessor { notificationTokens, currentTxHeight, response?.nft_transfer[0], + explorer, ); } } @@ -530,7 +575,10 @@ export class NotificationProcessor { .catch((error) => this.logger.error('cannot-send-notification', error)); } - private async blockLimitNotification(notifications: NotificationDto[]) { + private async blockLimitNotification( + notifications: NotificationDto[], + explorer: Explorer, + ) { const counts = {}; for (const element of notifications) { if (counts.hasOwnProperty(element.user_id)) { @@ -545,7 +593,9 @@ export class NotificationProcessor { where: { user: { id: Number(userId) }, type: USER_ACTIVITIES.DAILY_NOTIFICATIONS, + explorer: { id: explorer.id }, }, + relations: ['explorer'], }); if (userActivities) { const currentTotal = userActivities.total || 0; @@ -556,7 +606,7 @@ export class NotificationProcessor { if (total >= this.notificationConfig.limitNotifications) { this.watchListRepository.update( - { user: { id: Number(userId) } }, + { user: { id: Number(userId) }, explorer: { id: explorer.id } }, { tracking: false, }, @@ -575,10 +625,14 @@ export class NotificationProcessor { } } - private async updateBlockNotification(type, syncPoint = null) { + private async updateBlockNotification( + type, + explorer: Explorer, + syncPoint = null, + ) { const data = await lastValueFrom( this.httpService.get( - `${process.env.INDEXER_V2_URL}api/v2/statistics/dashboard?chainid=${this.indexerChainId}`, + `${process.env.INDEXER_V2_URL}api/v2/statistics/dashboard?chainid=${explorer.chainId}`, ), ).then((rs) => rs.data); if (syncPoint) { @@ -589,13 +643,19 @@ export class NotificationProcessor { await this.syncPointRepos.save({ type: type, point: data?.total_blocks, + explorer: explorer, }); } } - private async preProcessNotification(response: any, listAddress: string[]) { + private async preProcessNotification( + response: any, + listAddress: string[], + explorer: Explorer, + ) { const result = await this.privateNameTagRepository.find({ - where: { address: In(listAddress) }, + where: { address: In(listAddress), explorer: { id: explorer.id } }, + relations: ['explorer'], }); const privateNameTags = await Promise.all( result.map(async (item) => { @@ -604,17 +664,24 @@ export class NotificationProcessor { }), ); const publicNameTags = await this.publicNameTagRepository.find({ - where: { address: In(listAddress) }, + where: { address: In(listAddress), explorer: { id: explorer.id } }, + relations: ['explorer'], }); const fcmToken = await this.notificationTokenRepository.find({ where: { status: NOTIFICATION.STATUS.ACTIVE }, - relations: ['user', 'user.userActivities'], + relations: [ + 'user', + 'user.userActivities', + 'user.userActivities.explorer', + ], }); // Filter fcm token less than 100 notification per days. const notificationTokens = fcmToken.filter((item) => { const dailyNotification = item.user?.userActivities?.find( - (activity) => activity.type === USER_ACTIVITIES.DAILY_NOTIFICATIONS, + (activity) => + activity.type === USER_ACTIVITIES.DAILY_NOTIFICATIONS && + activity.explorer.id === explorer.id, ); return dailyNotification?.total >= this.notificationConfig.limitNotifications @@ -640,6 +707,7 @@ export class NotificationProcessor { notificationTokens: NotificationToken[], currentTxHeight: SyncPoint, response: any, + explorer: Explorer, ) { // Push notification to firebase if (notifications?.length > 0) { @@ -660,22 +728,29 @@ export class NotificationProcessor { point: response?.height, }); // Process limit when 100 notification per day - await this.blockLimitNotification(notifications); + await this.blockLimitNotification(notifications, explorer); } - private async queryWatchList() { + private async queryWatchList(explorer: Explorer) { // Get all watch list with tracking true const watchList = await this.watchListRepository.find({ where: { tracking: true, + explorer: { id: explorer.id }, }, - relations: ['user', 'user.userActivities'], + relations: [ + 'user', + 'user.userActivities', + 'user.userActivities.explorer', + ], }); // Filter watch list less than 100 notification per days. const watchListFilter = watchList.filter((item) => { const dailyNotification = item.user?.userActivities?.find( - (activity) => activity.type === USER_ACTIVITIES.DAILY_NOTIFICATIONS, + (activity) => + activity.type === USER_ACTIVITIES.DAILY_NOTIFICATIONS && + activity.explorer.id === explorer.id, ); return dailyNotification?.total >= this.notificationConfig.limitNotifications diff --git a/src/components/queues/notification/repositories/notification.repository.ts b/src/components/queues/notification/repositories/notification.repository.ts index f0a41a9a..61b0e2f3 100644 --- a/src/components/queues/notification/repositories/notification.repository.ts +++ b/src/components/queues/notification/repositories/notification.repository.ts @@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm'; import { Notification } from '../../../../shared/entities/notification.entity'; import { Logger } from '@nestjs/common'; import { NotificationParamsDto } from '../../../notification/dtos/get-notification-param.dto'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @EntityRepository(Notification) export class NotificationRepository extends Repository { @@ -9,17 +10,23 @@ export class NotificationRepository extends Repository { /** * Get list notifications - * @param user_id + * @param userId + * @param explorerId * @param is_read * @returns */ - async getNotifications(user_id: number, param: NotificationParamsDto) { + async getNotifications( + userId: number, + explorerId: number, + param: NotificationParamsDto, + ) { this._logger.log( `============== ${this.getNotifications.name} was called! ==============`, ); const builder = this.createQueryBuilder('noti') .select('noti.*') - .where('noti.user_id = :user_id', { user_id }); + .where('noti.user_id = :userId', { userId }) + .andWhere('noti.explorer_id =:explorerId', { explorerId }); const _finalizeResult = async () => { const result = await builder @@ -33,7 +40,8 @@ export class NotificationRepository extends Repository { const countUnread = await this.count({ where: { is_read: false, - user_id: user_id, + user_id: userId, + explorer: { id: explorerId }, }, }); diff --git a/src/components/queues/notification/utils/notification.util.ts b/src/components/queues/notification/utils/notification.util.ts index e0b4683a..b2df8c76 100644 --- a/src/components/queues/notification/utils/notification.util.ts +++ b/src/components/queues/notification/utils/notification.util.ts @@ -8,18 +8,19 @@ import { PrivateNameTag } from '../../../../shared/entities/private-name-tag.ent import { PublicNameTag } from '../../../../shared/entities/public-name-tag.entity'; import { NotificationDto } from '../dtos/notification.dtos'; import { TransactionHelper } from '../../../../shared/helpers/transaction.helper'; -import { NOTIFICATION } from '../../../../shared'; +import { ASSETS_TYPE, NOTIFICATION } from '../../../../shared'; import { WatchList } from '../../../../shared/entities/watch-list.entity'; import { TRANSACTION_TYPE_ENUM } from '../../../../shared/constants/transaction'; -import { TokenMarketsRepository } from '../../../cw20-token/repositories/token-markets.repository'; import { IsNull, Not } from 'typeorm'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { AssetsRepository } from '../../../asset/repositories/assets.repository'; @Injectable() export class NotificationUtil { private config; constructor( private httpService: HttpService, - private tokenMarketsRepository: TokenMarketsRepository, + private assetsRepository: AssetsRepository, ) { this.config = appConfig.default(); } @@ -29,6 +30,7 @@ export class NotificationUtil { watchList: WatchList[], listPrivateNameTag: PrivateNameTag[], listPublicNameTag: PublicNameTag[], + explorer: Explorer, ) { const lstNotification: NotificationDto[] = []; for (const tx of response) { @@ -43,6 +45,7 @@ export class NotificationUtil { element.user.id, listPrivateNameTag, listPublicNameTag, + explorer, ); const notification = new NotificationDto(); notification.title = NOTIFICATION.TITLE.EXECUTED; @@ -58,6 +61,7 @@ export class NotificationUtil { nameTag: nameTagPhase, }, }; + notification.explorer = explorer; lstNotification.push(notification); } } @@ -70,6 +74,7 @@ export class NotificationUtil { watchList: WatchList[], listPrivateNameTag: PrivateNameTag[], listPublicNameTag: PublicNameTag[], + explorer: Explorer, ) { const lstNotification: NotificationDto[] = []; for (const tx of data) { @@ -105,6 +110,7 @@ export class NotificationUtil { element.user.id, listPrivateNameTag, listPublicNameTag, + explorer, ); const notification = new NotificationDto(); @@ -132,6 +138,7 @@ export class NotificationUtil { nameTag: nameTagPhase, }, }; + notification.explorer = explorer; lstNotification.push(notification); } } @@ -144,6 +151,7 @@ export class NotificationUtil { watchList: WatchList[], listPrivateNameTag: PrivateNameTag[], listPublicNameTag: PublicNameTag[], + explorer: Explorer, ) { const lstNotification: NotificationDto[] = []; for (const tx of data) { @@ -168,6 +176,7 @@ export class NotificationUtil { element.user.id, listPrivateNameTag, listPublicNameTag, + explorer, ); const notification = new NotificationDto(); @@ -196,6 +205,7 @@ export class NotificationUtil { nameTag: nameTagPhase, }, }; + notification.explorer = explorer; lstNotification.push(notification); } } @@ -208,6 +218,7 @@ export class NotificationUtil { watchList: WatchList[], listPrivateNameTag: PrivateNameTag[], listPublicNameTag: PublicNameTag[], + explorer: Explorer, ) { const lstNotification: NotificationDto[] = []; for (const tx of data) { @@ -226,6 +237,7 @@ export class NotificationUtil { element.user.id, listPrivateNameTag, listPublicNameTag, + explorer, ); const notification = new NotificationDto(); @@ -257,6 +269,7 @@ export class NotificationUtil { nameTag: nameTagPhase, }, }; + notification.explorer = explorer; lstNotification.push(notification); } } @@ -307,14 +320,13 @@ export class NotificationUtil { }); } - async convertDataCoinTransfer(data) { - const envConfig = await lastValueFrom( - this.httpService.get(this.config.configUrl), - ).then((rs) => rs.data); - const coinConfig = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + async convertDataCoinTransfer(data, explorer: Explorer) { + const coinConfig = await this.assetsRepository.find({ + where: { + type: ASSETS_TYPE.IBC, + name: Not(IsNull()), + }, }); - const coinInfo = envConfig?.chainConfig?.chain_info?.currencies[0]; const listTx = []; data?.forEach((tx) => { tx.coin_transfers?.forEach((coin) => { @@ -326,7 +338,9 @@ export class NotificationUtil { : dataIBC['symbol']; // Get denom ibc not find in config or denom is native const denom = - coin.denom?.indexOf('ibc') === -1 ? coinInfo.coinDenom : coin.denom; + coin.denom?.indexOf('ibc') === -1 + ? explorer.minimalDenom + : coin.denom; listTx.push({ tx_hash: tx.hash, @@ -339,7 +353,7 @@ export class NotificationUtil { to: coin.to, amount: TransactionHelper.balanceOf( Number(coin.amount) || 0, - dataIBC['decimal'] || coinInfo.coinDecimals, + dataIBC['decimal'] || explorer.decimal, ), image: dataIBC['logo'] || '', denom: denomIBC || denom, @@ -354,12 +368,16 @@ export class NotificationUtil { userId: number, listPrivateNameTag: PrivateNameTag[], listPublicNameTag: PublicNameTag[], + explorer: Explorer, ) { const privateNameTag = listPrivateNameTag.find( - (item) => item.createdBy === userId && item.address === address, + (item) => + item.createdBy === userId && + item.address === address && + item.explorer.id === explorer.id, )?.nameTag; const publicNameTag = listPublicNameTag.find( - (item) => item.address === address, + (item) => item.address === address && item.explorer.id === explorer.id, )?.name_tag; const nameTagPhase = []; diff --git a/src/components/queues/token/token.module.ts b/src/components/queues/token/token.module.ts index ac265182..bb41bb6b 100644 --- a/src/components/queues/token/token.module.ts +++ b/src/components/queues/token/token.module.ts @@ -4,17 +4,26 @@ import { QUEUES, SharedModule } from '../../../shared'; import { TokenProcessor } from './token.processor'; import { ServiceUtil } from '../../../shared/utils/service.util'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TokenMarketsRepository } from '../../cw20-token/repositories/token-markets.repository'; import { ConfigModule } from '@nestjs/config'; import { HttpModule } from '@nestjs/axios'; import { TokenHolderStatistic } from '../../../shared/entities/token-holder-statistic.entity'; +import { AssetsRepository } from '../../asset/repositories/assets.repository'; +import { Asset } from 'src/shared/entities/asset.entity'; +import { SyncPoint } from 'src/shared/entities/sync-point.entity'; +import { TokenMarketsRepository } from 'src/components/cw20-token/repositories/token-markets.repository'; @Module({ imports: [ SharedModule, ConfigModule, HttpModule, - TypeOrmModule.forFeature([TokenMarketsRepository, TokenHolderStatistic]), + TypeOrmModule.forFeature([ + TokenMarketsRepository, + TokenHolderStatistic, + AssetsRepository, + Asset, + SyncPoint, + ]), BullModule.registerQueueAsync({ name: QUEUES.TOKEN.QUEUE_NAME, }), diff --git a/src/components/queues/token/token.processor.ts b/src/components/queues/token/token.processor.ts index 1528d708..92e45410 100644 --- a/src/components/queues/token/token.processor.ts +++ b/src/components/queues/token/token.processor.ts @@ -5,53 +5,70 @@ import { Process, Processor, } from '@nestjs/bull'; -import { TokenMarketsRepository } from '../../cw20-token/repositories/token-markets.repository'; -import { Logger } from '@nestjs/common'; +import { Logger, OnModuleInit } from '@nestjs/common'; import { Job, Queue } from 'bull'; import { + Asset, COINGECKO_API, - COIN_MARKET_CAP, - COIN_MARKET_CAP_API, INDEXER_API_V2, QUEUES, - TokenMarkets, + INDEXER_V2_DB, + SYNC_POINT_TYPE, + ASSETS_TYPE, } from '../../../shared'; import * as appConfig from '../../../shared/configs/configuration'; import * as util from 'util'; import { ServiceUtil } from '../../../shared/utils/service.util'; -import { In, IsNull, Not, Repository } from 'typeorm'; -import { CronExpression } from '@nestjs/schedule'; +import { In, LessThan, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { TokenHolderStatistic } from '../../../shared/entities/token-holder-statistic.entity'; +import { AssetsRepository } from '../../asset/repositories/assets.repository'; +import { CronExpression } from '@nestjs/schedule'; +import { SyncPoint } from 'src/shared/entities/sync-point.entity'; +import { TransactionHelper } from '../../../shared/helpers/transaction.helper'; +import * as moment from 'moment'; @Processor(QUEUES.TOKEN.QUEUE_NAME) -export class TokenProcessor { +export class TokenProcessor implements OnModuleInit { private readonly logger = new Logger(TokenProcessor.name); private appParams: any; - private chainDB; constructor( private serviceUtil: ServiceUtil, - private tokenMarketsRepository: TokenMarketsRepository, + private assetsRepository: AssetsRepository, @InjectRepository(TokenHolderStatistic) private readonly tokenHolderStatisticRepo: Repository, + @InjectRepository(SyncPoint) + private readonly syncPointRepository: Repository, @InjectQueue(QUEUES.TOKEN.QUEUE_NAME) private readonly tokenQueue: Queue, ) { this.logger.log( '============== Constructor Token Price Processor Service ==============', ); this.appParams = appConfig.default(); - this.chainDB = this.appParams.indexerV2.chainDB; - this.tokenQueue.add( - QUEUES.TOKEN.JOB_SYNC_CW20_PRICE, + QUEUES.TOKEN.JOB_SYNC_ASSET, {}, { - repeat: { cron: this.appParams.priceTimeSync }, + repeat: { cron: `30 * * * * *` }, + }, + ); + this.tokenQueue.add( + QUEUES.TOKEN.JOB_SYNC_NATIVE_ASSET_HOLDER, + {}, + { + repeat: { cron: `2 * * * *` }, + }, + ); + this.tokenQueue.add( + QUEUES.TOKEN.JOB_SYNC_CW20_ASSET_HOLDER, + {}, + { + repeat: { cron: '1 * * * *' }, }, ); this.tokenQueue.add( - QUEUES.TOKEN.JOB_SYNC_TOKEN_HOLDER, + QUEUES.TOKEN.JOB_CLEAN_ASSET_HOLDER, {}, { repeat: { cron: CronExpression.EVERY_DAY_AT_MIDNIGHT }, @@ -59,90 +76,43 @@ export class TokenProcessor { ); } + async onModuleInit() { + this.logger.log( + '============== On Module Init Token Price Processor Service ==============', + ); + this.tokenQueue.add( + QUEUES.TOKEN.JOB_SYNC_CW20_PRICE, + {}, + { + repeat: { cron: this.appParams.priceTimeSync }, + }, + ); + } + @Process(QUEUES.TOKEN.JOB_SYNC_CW20_PRICE) async syncCW20TokenPrice(): Promise { const numberCW20Tokens = - await this.tokenMarketsRepository.countCw20TokensHavingCoinId(); + await this.assetsRepository.countAssetsHavingCoinId(); const limit = this.appParams.coingecko.maxRequest; const pages = Math.ceil(numberCW20Tokens / limit); for (let i = 0; i < pages; i++) { // Get data CW20 by paging const dataHavingCoinId = - await this.tokenMarketsRepository.getCw20TokenMarketsHavingCoinId( - limit, - i, - ); + await this.assetsRepository.getAssetsHavingCoinId(limit, i); const tokensHavingCoinId = dataHavingCoinId?.map((i) => i.coin_id); if (tokensHavingCoinId.length > 0) { - this.handleSyncPriceVolume(tokensHavingCoinId); + this.syncCoingeckoPrice(tokensHavingCoinId); } } } - async handleSyncPriceVolume(listTokens: string[]): Promise { - try { - if (this.appParams.priceHostSync === COIN_MARKET_CAP) { - await this.syncCoinMarketCapPrice(listTokens); - } else { - await this.syncCoingeckoPrice(listTokens); - } - } catch (err) { - this.logger.log(`sync-price-volume has error: ${err.message}`, err.stack); - } - } - - async syncCoinMarketCapPrice(listTokens) { - const coinMarketCap = this.appParams.coinMarketCap; - this.logger.log(`============== Call CoinMarketCap Api ==============`); - const coinIds = listTokens.join(','); - const coinMarkets: TokenMarkets[] = []; - - const para = `${util.format( - COIN_MARKET_CAP_API.GET_COINS_MARKET, - coinIds, - )}`; - - const headersRequest = { - 'Content-Type': 'application/json', - 'X-CMC_PRO_API_KEY': coinMarketCap.apiKey, - }; - - const [response, tokenInfos] = await Promise.all([ - this.serviceUtil.getDataAPIWithHeader( - coinMarketCap.api, - para, - headersRequest, - ), - this.tokenMarketsRepository.find({ - where: { - coin_id: In(listTokens), - }, - }), - ]); - - if (response?.status?.error_code == 0 && response?.data) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [key, value] of Object.entries(response?.data)) { - const data = response?.data[key]; - let tokenInfo = tokenInfos?.find((f) => f.coin_id === data.slug); - if (tokenInfo) { - tokenInfo = this.updateCoinMarketsData(tokenInfo, data); - coinMarkets.push(tokenInfo); - } - } - } - if (coinMarkets.length > 0) { - await this.tokenMarketsRepository.save(coinMarkets); - } - } - async syncCoingeckoPrice(listTokens) { const coingecko = this.appParams.coingecko; this.logger.log(`============== Call Coingecko Api ==============`); const coinIds = listTokens.join(','); - const coinMarkets: TokenMarkets[] = []; + const coinMarkets: Asset[] = []; const para = `${util.format( COINGECKO_API.GET_COINS_MARKET, @@ -152,9 +122,9 @@ export class TokenProcessor { const [response, tokenInfos] = await Promise.all([ this.serviceUtil.getDataAPI(coingecko.api, para, ''), - this.tokenMarketsRepository.find({ + this.assetsRepository.find({ where: { - coin_id: In(listTokens), + coinId: In(listTokens), }, }), ]); @@ -162,7 +132,7 @@ export class TokenProcessor { if (response) { for (let index = 0; index < response.length; index++) { const data = response[index]; - const tokenInfo = tokenInfos?.filter((f) => f.coin_id === data.id); + const tokenInfo = tokenInfos?.filter((f) => f.coinId === data.id); tokenInfo?.forEach((item) => { const tokenInfoUpdated = this.updateTokenMarketsData(item, data); coinMarkets.push(tokenInfoUpdated); @@ -170,97 +140,207 @@ export class TokenProcessor { } } if (coinMarkets.length > 0) { - await this.tokenMarketsRepository.save(coinMarkets); + await this.assetsRepository.save(coinMarkets); } } - updateCoinMarketsData(currentData: TokenMarkets, data: any): TokenMarkets { - const quote = data.quote?.USD; + updateTokenMarketsData(currentData: Asset, data: any): Asset { const coinInfo = { ...currentData }; - coinInfo.current_price = Number(quote?.price?.toFixed(6)) || 0; - coinInfo.price_change_percentage_24h = - Number(quote?.percent_change_24h?.toFixed(6)) || 0; - coinInfo.total_volume = Number(quote?.volume_24h?.toFixed(6)) || 0; - coinInfo.circulating_supply = - Number(data.circulating_supply?.toFixed(6)) || 0; - const circulating_market_cap = - coinInfo.circulating_supply * coinInfo.current_price; - coinInfo.circulating_market_cap = - Number(circulating_market_cap?.toFixed(6)) || 0; - coinInfo.max_supply = Number(data.max_supply?.toFixed(6)) || 0; - coinInfo.market_cap = - Number(data.self_reported_market_cap?.toFixed(6)) || 0; - coinInfo.fully_diluted_valuation = - Number(quote?.fully_diluted_market_cap?.toFixed(6)) || 0; - + coinInfo.currentPrice = Number(data.current_price?.toFixed(6)); + coinInfo.priceChangePercentage24h = Number( + data.price_change_percentage_24h?.toFixed(6), + ); + coinInfo.marketCap = Number(data.market_cap?.toFixed(6)); + coinInfo.totalVolume = Number(data.total_volume?.toFixed(6)); return coinInfo; } - updateTokenMarketsData(currentData: TokenMarkets, data: any): TokenMarkets { - const coinInfo = { ...currentData }; - coinInfo.current_price = Number(data.current_price?.toFixed(6)) || 0; - coinInfo.price_change_percentage_24h = - Number(data.price_change_percentage_24h?.toFixed(6)) || 0; - coinInfo.total_volume = Number(data.total_volume?.toFixed(6)) || 0; - coinInfo.circulating_supply = - Number(data.circulating_supply?.toFixed(6)) || 0; - - const circulating_market_cap = - coinInfo.circulating_supply * coinInfo.current_price; - coinInfo.circulating_market_cap = - Number(circulating_market_cap?.toFixed(6)) || 0; - coinInfo.max_supply = Number(data.max_supply?.toFixed(6)) || 0; - coinInfo.market_cap = Number(data.market_cap?.toFixed(6)) || 0; - coinInfo.fully_diluted_valuation = - Number(data.fully_diluted_valuation?.toFixed(6)) || 0; + @Process(QUEUES.TOKEN.JOB_SYNC_ASSET) + async syncAsset(): Promise { + let from = new Date(new Date().getTime() - 60 * 1000).toJSON(); + const alreadySynced = await this.syncPointRepository.findOne({ + where: { type: SYNC_POINT_TYPE.FIRST_TIME_SYNC_ASSETS }, + }); - return coinInfo; + if (!alreadySynced) { + from = null; + + await this.syncPointRepository.save({ + type: SYNC_POINT_TYPE.FIRST_TIME_SYNC_ASSETS, + }); + } + + const queryAssets = { + query: INDEXER_API_V2.GRAPH_QL.ASSETS, + variables: { from: from }, + operationName: INDEXER_API_V2.OPERATION_NAME.ASSETS, + }; + + const listAsset = await this.getDataWithPagination(queryAssets, 'asset'); + await this.assetsRepository.storeAsset(listAsset); } - @Process(QUEUES.TOKEN.JOB_SYNC_TOKEN_HOLDER) - async syncTokenHolder(): Promise { - let subQuery = ''; + @Process(QUEUES.TOKEN.JOB_SYNC_NATIVE_ASSET_HOLDER) + async syncNativeAssetHolder(): Promise { + const listHolderStatistic = await this.getNewNativeHolders(); + + await this.upsertTokenHolderStatistic(listHolderStatistic); + } - const tokenMarkets = await this.tokenMarketsRepository.find({ - where: { denom: Not(IsNull()) }, + @Process(QUEUES.TOKEN.JOB_SYNC_CW20_ASSET_HOLDER) + async syncCw20AssetHolder(): Promise { + const { cw20WithNewImage, listHolderStatistic } = + await this.getNewCw20Info(); + + await this.assetsRepository.save(cw20WithNewImage); + + await this.upsertTokenHolderStatistic(listHolderStatistic); + } + + async getNewNativeHolders(): Promise { + const listHolder: TokenHolderStatistic[] = []; + const nativeAsset = await this.assetsRepository.find({ + where: { + type: In([ASSETS_TYPE.IBC, ASSETS_TYPE.NATIVE]), + }, }); + let subQuery = ''; - if (tokenMarkets.length > 0) { - for (const [index, denom] of tokenMarkets.entries()) { - subQuery = - subQuery.concat(`total_holder_${index}: account_balance_aggregate(where: {denom: {_eq: "${denom.denom}"}}) { + for (const [index, asset] of nativeAsset.entries()) { + subQuery = + subQuery.concat(`total_holder_${index}: account_balance_aggregate(where: {denom: {_eq: "${asset.denom}"}}) { aggregate { count } }`); - } + } + + const query = util.format( + INDEXER_API_V2.GRAPH_QL.BASE_QUERY, + INDEXER_V2_DB, + subQuery, + ); - const query = util.format(INDEXER_API_V2.GRAPH_QL.BASE_QUERY, subQuery); + const graphqlQueryTotalHolder = { + query, + operationName: INDEXER_API_V2.OPERATION_NAME.BASE_QUERY, + variables: {}, + }; - const graphqlQueryTotalHolder = { - query, - operationName: INDEXER_API_V2.OPERATION_NAME.BASE_QUERY, - variables: {}, - }; + const totalHolders = ( + await this.serviceUtil.fetchDataFromGraphQL(graphqlQueryTotalHolder) + )?.data[INDEXER_V2_DB]; - const totalHolders = ( - await this.serviceUtil.fetchDataFromGraphQL(graphqlQueryTotalHolder) - )?.data[this.chainDB]; + for (const [index, asset] of nativeAsset.entries()) { + const newTotalHolder = + totalHolders[`total_holder_${index}`].aggregate.count; - const totalHolderStatistics = []; - for (const [index, tokenMarket] of tokenMarkets.entries()) { - const totalHolder = - totalHolders[`total_holder_${index}`].aggregate.count; + const newHolderStatistic = new TokenHolderStatistic(); + newHolderStatistic.id = null; + newHolderStatistic.asset = asset; + newHolderStatistic.totalHolder = newTotalHolder; + newHolderStatistic.date = new Date(); - const newTokenHolderStatistic = new TokenHolderStatistic(); - newTokenHolderStatistic.totalHolder = totalHolder; - newTokenHolderStatistic.tokenMarket = tokenMarket; + listHolder.push(newHolderStatistic); + } - totalHolderStatistics.push(newTokenHolderStatistic); + return listHolder; + } + + @Process(QUEUES.TOKEN.JOB_CLEAN_ASSET_HOLDER) + async cleanAssetHolder(): Promise { + await this.tokenHolderStatisticRepo.delete({ + created_at: LessThan(moment().subtract(2, 'days').toDate()), + }); + } + async getNewCw20Info(): Promise<{ + cw20WithNewImage: Asset[]; + listHolderStatistic: TokenHolderStatistic[]; + }> { + const cw20WithNewImage: Asset[] = []; + const listHolderStatistic: TokenHolderStatistic[] = []; + const listCw20Asset = await this.assetsRepository.find({ + where: { type: ASSETS_TYPE.CW20 }, + }); + const cw20Query = { + variables: {}, + query: INDEXER_API_V2.GRAPH_QL.CW20_HOLDER_STAT, + operationName: INDEXER_API_V2.OPERATION_NAME.CW20_HOLDER_STAT, + }; + const newCw20Holders = await this.getDataWithPagination( + cw20Query, + 'cw20_contract', + ); + + for (const cw20Asset of listCw20Asset) { + if (!cw20Asset.image || !cw20Asset.symbol) { + const newCw20Image = newCw20Holders.find( + (cw20) => cw20.smart_contract.address === cw20Asset.denom, + ); + cw20Asset.image = newCw20Image?.marketing_info?.logo?.url || ''; + cw20Asset.symbol = newCw20Image?.symbol || ''; + + cw20WithNewImage.push(cw20Asset); } - await this.tokenHolderStatisticRepo.save(totalHolderStatistics); + const newCw20Holder = newCw20Holders.find( + (cw20) => cw20.smart_contract.address === cw20Asset.denom, + ); + + if (newCw20Holder?.cw20_total_holder_stats[0]) { + const newCw20HolderStatistic = new TokenHolderStatistic(); + newCw20HolderStatistic.id = null; + newCw20HolderStatistic.totalHolder = + newCw20Holder.cw20_total_holder_stats[0]?.total_holder; + newCw20HolderStatistic.date = + newCw20Holder.cw20_total_holder_stats[0]?.date; + newCw20HolderStatistic.asset = cw20Asset; + listHolderStatistic.push(newCw20HolderStatistic); + } } + + return { cw20WithNewImage, listHolderStatistic }; + } + + async getDataWithPagination(query, keyData) { + const result = []; + let pageLength; + + do { + const { data } = await this.serviceUtil.fetchDataFromGraphQL(query); + const newData = data[INDEXER_V2_DB][keyData]; + + if (keyData === 'asset') { + newData.map((asset) => { + asset.totalSupply = TransactionHelper.balanceOf( + asset.total_supply, + asset.decimal || 6, + ); + + return asset; + }); + } + + result.push(...newData); + + query.variables.id_gt = newData[newData.length - 1]?.id; + pageLength = newData.length; + } while (pageLength === INDEXER_API_V2.MAX_REQUEST); + + result.map((e) => (e.id = null)); + + return result; + } + + async upsertTokenHolderStatistic( + listHolderStatistic: TokenHolderStatistic[], + ) { + await this.tokenHolderStatisticRepo + .createQueryBuilder() + .insert() + .values(listHolderStatistic) + .orUpdate(['total_holder'], ['asset', 'date']) + .execute(); } @OnQueueError() diff --git a/src/components/user/controllers/users.controller.ts b/src/components/user/controllers/users.controller.ts index 1933a522..301540d4 100644 --- a/src/components/user/controllers/users.controller.ts +++ b/src/components/user/controllers/users.controller.ts @@ -31,7 +31,13 @@ import { UserService } from '../user.service'; import { CreateUserDto } from '../dtos/create-user.dto'; import { UpdateUserDto } from '../dtos/update-user.dto'; import { JwtAuthGuard } from '../../../auth/jwt/jwt-auth.guard'; -import { BaseApiResponse, MESSAGES, USER_ROLE } from '../../../shared'; +import { + BaseApiResponse, + MESSAGES, + ReqContext, + RequestContext, + USER_ROLE, +} from '../../../shared'; import { RoleGuard } from '../../../auth/role/roles.guard'; import { Roles } from '../../../auth/role/roles.decorator'; import { User } from '../../../../src/shared/entities/user.entity'; @@ -173,10 +179,15 @@ export class UsersController { @HttpCode(HttpStatus.OK) @Post('register-notification-token') async registerNotificationToken( + @ReqContext() ctx: RequestContext, @Req() req, @Body() body: NotificationTokenDto, ): Promise { - return await this.userService.registerNotificationToken(req.user.id, body); + return await this.userService.registerNotificationToken( + ctx, + req.user.id, + body, + ); } @ApiBearerAuth() diff --git a/src/components/user/user.module.ts b/src/components/user/user.module.ts index c693de70..fe95e698 100644 --- a/src/components/user/user.module.ts +++ b/src/components/user/user.module.ts @@ -9,10 +9,11 @@ import { MatchPasswordConstraint } from './validators/validate-match-password'; import { UserActivity } from '../../shared/entities/user-activity.entity'; import { SendMailModule } from '../queues/send-mail/send-mail.module'; import { NotificationTokenRepository } from '../queues/notification/repositories/notification-token.repository'; +import { Explorer } from 'src/shared/entities/explorer.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([User]), + TypeOrmModule.forFeature([User, Explorer]), TypeOrmModule.forFeature([UserActivity, NotificationTokenRepository]), SendMailModule, ], diff --git a/src/components/user/user.service.ts b/src/components/user/user.service.ts index 73ba2e1f..ab58cb7f 100644 --- a/src/components/user/user.service.ts +++ b/src/components/user/user.service.ts @@ -22,6 +22,7 @@ import { NOTIFICATION, PROVIDER, QUEUES, + RequestContext, SITE, SUPPORT_EMAIL, USER_ACTIVITIES, @@ -43,6 +44,7 @@ import { secondsToDate } from '../../shared/utils/service.util'; import { NotificationTokenDto } from './dtos/notification-token.dto'; import { NotificationTokenRepository } from '../queues/notification/repositories/notification-token.repository'; import { NotificationToken } from '../../shared/entities/notification-token.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; const VERIFICATION_TOKEN_LENGTH = 20; const RESET_PASSWORD_TOKEN_LENGTH = 21; const RANDOM_BYTES_LENGTH = 20; @@ -58,6 +60,8 @@ export class UserService { private userActivityRepository: Repository, private configService: ConfigService, private notificationTokenRepository: NotificationTokenRepository, + @InjectRepository(Explorer) + private explorerRepository: Repository, ) {} async findOne(params: FindOneOptions = {}): Promise { @@ -466,13 +470,19 @@ export class UserService { } async registerNotificationToken( + ctx: RequestContext, userId: number, token: NotificationTokenDto, ): Promise { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + const userActivities = await this.userActivityRepository.findOne({ where: { user: { id: userId }, type: USER_ACTIVITIES.DAILY_NOTIFICATIONS, + explorer: { id: explorer.id }, }, }); const user = await this.usersRepository.findOne({ @@ -482,6 +492,7 @@ export class UserService { const activity = new UserActivity(); activity.type = USER_ACTIVITIES.DAILY_NOTIFICATIONS; activity.user = user; + activity.explorer = explorer; activity.total = 0; await this.userActivityRepository.save(activity); } diff --git a/src/components/watch-list/controllers/watch-list.controller.ts b/src/components/watch-list/controllers/watch-list.controller.ts index 16901544..432db2b7 100644 --- a/src/components/watch-list/controllers/watch-list.controller.ts +++ b/src/components/watch-list/controllers/watch-list.controller.ts @@ -85,7 +85,7 @@ export class WatchListController { async create( @ReqContext() ctx: RequestContext, @Body() createWatchListDto: CreateWatchListDto, - ): Promise { + ): Promise { return await this.watchListService.create(ctx, createWatchListDto); } @@ -110,7 +110,7 @@ export class WatchListController { @ReqContext() ctx: RequestContext, @Param('id') id: string, @Body() updateWatchListDto: UpdateWatchListDto, - ): Promise { + ): Promise { return await this.watchListService.update(ctx, +id, updateWatchListDto); } diff --git a/src/components/watch-list/dto/create-watch-list.dto.ts b/src/components/watch-list/dto/create-watch-list.dto.ts index 02ee84e8..d54089d8 100644 --- a/src/components/watch-list/dto/create-watch-list.dto.ts +++ b/src/components/watch-list/dto/create-watch-list.dto.ts @@ -4,19 +4,16 @@ import { } from '@nestjs/swagger/dist/decorators'; import { IsBoolean, IsIn, IsNotEmpty, IsOptional } from 'class-validator'; import { NAME_TAG_TYPE, WATCH_LIST } from '../../../shared'; -import { IsValidBench32Address } from '../validators/validate-address'; import { User } from '../../../shared/entities/user.entity'; import { MatchKeys } from '../validators/match-keys'; -import { MatchType } from '../validators/match-type'; +import { Explorer } from 'src/shared/entities/explorer.entity'; export class CreateWatchListDto { @ApiProperty() @IsNotEmpty() - @IsValidBench32Address('address') address: string; @ApiProperty({ default: NAME_TAG_TYPE.ACCOUNT }) @IsIn([NAME_TAG_TYPE.ACCOUNT, NAME_TAG_TYPE.CONTRACT]) - @MatchType('address') type: NAME_TAG_TYPE; @ApiPropertyOptional() @@ -39,4 +36,6 @@ export class CreateWatchListDto { settings: JSON; user: User; + + explorer: Explorer; } diff --git a/src/components/watch-list/dto/update-watch-list.dto.ts b/src/components/watch-list/dto/update-watch-list.dto.ts index f04d2fb9..f73429fd 100644 --- a/src/components/watch-list/dto/update-watch-list.dto.ts +++ b/src/components/watch-list/dto/update-watch-list.dto.ts @@ -1,16 +1,12 @@ import { CreateWatchListDto } from './create-watch-list.dto'; -import { IsValidBench32Address } from '../validators/validate-address'; import { ApiPropertyOptional, PartialType } from '@nestjs/swagger'; import { IsOptional } from 'class-validator'; import { MatchKeys } from '../validators/match-keys'; import { WATCH_LIST } from '../../../shared/constants/common'; export class UpdateWatchListDto extends PartialType(CreateWatchListDto) { - id: number; - @ApiPropertyOptional() @IsOptional() - @IsValidBench32Address('address') address: string; @ApiPropertyOptional() diff --git a/src/components/watch-list/dto/watch-list-detail.response.ts b/src/components/watch-list/dto/watch-list-detail.response.ts index 7484bc71..e2146331 100644 --- a/src/components/watch-list/dto/watch-list-detail.response.ts +++ b/src/components/watch-list/dto/watch-list-detail.response.ts @@ -1,5 +1,8 @@ import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateWatchListDto } from './create-watch-list.dto'; +import { Exclude } from 'class-transformer'; +import { User } from 'src/shared/entities/user.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; export class WatchListDetailResponse extends PartialType(CreateWatchListDto) { @ApiProperty() @@ -13,4 +16,16 @@ export class WatchListDetailResponse extends PartialType(CreateWatchListDto) { @ApiProperty() publicNameTag: string; + + @ApiProperty() + created_at: Date; + + @ApiProperty() + updated_at: Date; + + @Exclude() + user: User; + + @Exclude() + explorer: Explorer; } diff --git a/src/components/watch-list/validators/validate-address.ts b/src/components/watch-list/validators/validate-address.ts index e9eb888f..4bbdd153 100644 --- a/src/components/watch-list/validators/validate-address.ts +++ b/src/components/watch-list/validators/validate-address.ts @@ -14,7 +14,7 @@ export class IsValidBech32AddressConstraint implements ValidatorConstraintInterface { async validate(value: string): Promise { - return await isValidBench32Address(value); + return isValidBench32Address(value); } defaultMessage() { diff --git a/src/components/watch-list/watch-list.module.ts b/src/components/watch-list/watch-list.module.ts index 276ac03d..ade08ce8 100644 --- a/src/components/watch-list/watch-list.module.ts +++ b/src/components/watch-list/watch-list.module.ts @@ -8,6 +8,8 @@ import { PublicNameTag } from '../../shared/entities/public-name-tag.entity'; import { PrivateNameTag } from '../../shared/entities/private-name-tag.entity'; import { EncryptionService } from '../encryption/encryption.service'; import { CipherKey } from '../../shared/entities/cipher-key.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { AkcLogger } from 'src/shared'; @Module({ imports: [ @@ -16,10 +18,11 @@ import { CipherKey } from '../../shared/entities/cipher-key.entity'; PublicNameTag, PrivateNameTag, CipherKey, + Explorer, ]), UserModule, ], controllers: [WatchListController], - providers: [WatchListService, EncryptionService], + providers: [WatchListService, EncryptionService, AkcLogger], }) export class WatchListModule {} diff --git a/src/components/watch-list/watch-list.service.ts b/src/components/watch-list/watch-list.service.ts index 1be3979d..f440d38f 100644 --- a/src/components/watch-list/watch-list.service.ts +++ b/src/components/watch-list/watch-list.service.ts @@ -1,8 +1,4 @@ -import { - BadRequestException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { CreateWatchListDto } from './dto/create-watch-list.dto'; import { UpdateWatchListDto } from './dto/update-watch-list.dto'; import { RequestContext } from '../../shared/request-context/request-context.dto'; @@ -11,14 +7,23 @@ import { Repository } from 'typeorm/repository/Repository'; import { WatchList } from '../../shared/entities/watch-list.entity'; import { UserService } from '../user/user.service'; import { DeleteResult } from 'typeorm/query-builder/result/DeleteResult'; -import { In, Not } from 'typeorm'; +import { EntityNotFoundError, In } from 'typeorm'; import { ConfigService } from '@nestjs/config'; -import { BaseApiResponse, WATCH_LIST } from '../../shared/'; +import { + AkcLogger, + BaseApiResponse, + TYPE_ORM_ERROR_CODE, + WATCH_LIST, +} from '../../shared/'; import { WatchListDetailResponse } from './dto/watch-list-detail.response'; import { isValidBench32Address } from 'src/shared/utils/service.util'; import { PublicNameTag } from '../../shared/entities/public-name-tag.entity'; import { EncryptionService } from '../encryption/encryption.service'; import { PrivateNameTag } from '../../shared/entities/private-name-tag.entity'; +import { Explorer } from 'src/shared/entities/explorer.entity'; +import { plainToClass } from 'class-transformer'; +import { User } from 'src/shared/entities/user.entity'; +import * as util from 'util'; @Injectable() export class WatchListService { @@ -29,38 +34,54 @@ export class WatchListService { private readonly publicNameTagRepository: Repository, @InjectRepository(PrivateNameTag) private readonly privateNameTagRepository: Repository, - private userService: UserService, + @InjectRepository(Explorer) + private readonly explorerRepository: Repository, private readonly encryptionService: EncryptionService, private readonly configService: ConfigService, ) {} async create( ctx: RequestContext, createWatchListDto: CreateWatchListDto, - ): Promise { - // Check limit number address - const totalWatchList = await this.watchListRepository.count({ - where: { user: { id: ctx.user.id } }, - }); + ): Promise { + try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); - if (totalWatchList >= this.configService.get('watchList.limitAddress')) { - throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_LIMIT_ADDRESS); - } + this.validateAddress( + createWatchListDto.address, + explorer.addressPrefix, + createWatchListDto.type, + ); - // Check unique - const duplicateRecord = await this.watchListRepository.findOne({ - where: { address: createWatchListDto.address, user: { id: ctx.user.id } }, - }); + // Check limit number address + const totalWatchList = await this.watchListRepository.count({ + where: { user: { id: ctx.user.id }, explorer: { id: explorer.id } }, + }); - if (duplicateRecord) { - throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_UNIQUE_ADDRESS); - } + if (totalWatchList >= this.configService.get('watchList.limitAddress')) { + throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_LIMIT_ADDRESS); + } - // Create address - createWatchListDto.user = await this.userService.findOne({ - where: { id: ctx.user.id }, - }); + createWatchListDto.explorer = explorer; + createWatchListDto.user = { id: ctx.user.id } as User; + + const newWatchList = await this.watchListRepository.save( + createWatchListDto, + ); - return this.watchListRepository.save(createWatchListDto); + return plainToClass(WatchListDetailResponse, newWatchList); + } catch (error) { + if (error instanceof EntityNotFoundError) { + throw new BadRequestException(error.message); + } else if ( + error?.driverError?.code === TYPE_ORM_ERROR_CODE.ER_DUP_ENTRY + ) { + throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_UNIQUE_ADDRESS); + } + + throw error; + } } async findAll( @@ -72,8 +93,12 @@ export class WatchListService { return await this.filterWatchList(ctx, keyword); } + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + const watchList = (await this.watchListRepository.find({ - where: { user: { id: ctx.user.id } }, + where: { user: { id: ctx.user.id }, explorer: { id: explorer.id } }, order: { favorite: 'DESC', updated_at: 'DESC' }, })) as any as WatchListDetailResponse[]; @@ -127,38 +152,42 @@ export class WatchListService { ctx: RequestContext, id: number, updateWatchListDto: UpdateWatchListDto, - ): Promise { - const foundedWatchList = await this.watchListRepository.findOne({ - where: { - id, - user: { id: ctx.user.id }, - }, - }); + ): Promise { + try { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); - // Check duplicate when update address - if (updateWatchListDto.address) { - const duplicateRecord = await this.watchListRepository.findOne({ + await this.watchListRepository.findOneOrFail({ where: { - id: Not(id), - address: updateWatchListDto.address, + id: id, user: { id: ctx.user.id }, + explorer: { id: explorer.id }, }, }); - if (duplicateRecord) - throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_UNIQUE_ADDRESS); - } + this.validateAddress( + updateWatchListDto.address, + explorer.addressPrefix, + updateWatchListDto.type, + ); - if (foundedWatchList) { - updateWatchListDto.id = id; - await this.watchListRepository.merge( - foundedWatchList, + const updatedWatchList = await this.watchListRepository.update( + id, updateWatchListDto, ); - return this.watchListRepository.save(foundedWatchList); - } else { - throw new NotFoundException(WATCH_LIST.ERROR_MSGS.ERR_ADDRESS_NOT_FOUND); + return plainToClass(WatchListDetailResponse, updatedWatchList); + } catch (error) { + if (error?.driverError?.code === TYPE_ORM_ERROR_CODE.ER_DUP_ENTRY) { + throw new BadRequestException(WATCH_LIST.ERROR_MSGS.ERR_UNIQUE_ADDRESS); + } else if (error instanceof EntityNotFoundError) { + throw new BadRequestException( + WATCH_LIST.ERROR_MSGS.ERR_ADDRESS_NOT_FOUND, + ); + } + + throw error; } } @@ -179,7 +208,13 @@ export class WatchListService { ctx: RequestContext, keyword: string, ): Promise> { - if (await isValidBench32Address(keyword)) { + const explorer = await this.explorerRepository.findOneOrFail({ + chainId: ctx.chainId, + }); + + const explorerId = explorer.id; + + if (isValidBench32Address(keyword, explorer.addressPrefix)) { // Find in watch list. let foundedPublicNameTag = null; let foundPrivateNameTag = null; @@ -191,12 +226,16 @@ export class WatchListService { if (foundedWatchList) { // Find in public tag. foundedPublicNameTag = await this.publicNameTagRepository.findOne({ - where: { address: keyword }, + where: { address: keyword, explorer: { id: explorerId } }, }); // Find in private tag. foundPrivateNameTag = await this.privateNameTagRepository.findOne({ - where: { address: keyword, createdBy: ctx.user.id }, + where: { + address: keyword, + createdBy: ctx.user.id, + explorer: { id: explorerId }, + }, }); // Mapping tags. @@ -219,12 +258,16 @@ export class WatchListService { const keywordEncrypted = await this.encryptionService.encrypt(keyword); const foundedPublicNameTag = await this.publicNameTagRepository.findOne({ - where: { name_tag: keyword }, + where: { name_tag: keyword, explorer: { id: explorerId } }, }); const foundedPrivateNameTag = await this.privateNameTagRepository.findOne( { - where: { createdBy: ctx.user.id, nameTag: keywordEncrypted }, + where: { + createdBy: ctx.user.id, + nameTag: keywordEncrypted, + explorer: { id: explorerId }, + }, }, ); @@ -236,6 +279,7 @@ export class WatchListService { foundedPrivateNameTag?.address, ]), user: { id: ctx.user.id }, + explorer: { id: explorerId }, }, order: { favorite: 'DESC', updated_at: 'DESC' }, })) as any as WatchListDetailResponse[]; @@ -293,4 +337,11 @@ export class WatchListService { return count; } + + validateAddress(address: string, prefix?: string, type?: string) { + if (!isValidBench32Address(address, prefix, type)) + throw new BadRequestException( + util.format(WATCH_LIST.ERROR_MSGS.ERR_INVALID_ADDRESS, prefix), + ); + } } diff --git a/src/migrations/1706070214520-create-explorer-table.ts b/src/migrations/1706070214520-create-explorer-table.ts new file mode 100644 index 00000000..c376d92c --- /dev/null +++ b/src/migrations/1706070214520-create-explorer-table.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createExplorerTable1706070214520 implements MigrationInterface { + name = 'createExplorerTable1706070214520'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`explorer\` + (\`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + \`id\` int NOT NULL AUTO_INCREMENT, + \`chain_id\` varchar(255) NOT NULL, + \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + + await queryRunner.query( + `INSERT INTO \`explorer\` (\`chain_id\`, \`name\`) VALUES + ('xstaxy-1', 'Aura Mainnet')`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`explorer\``); + } +} diff --git a/src/migrations/1706152426601-create-column-explorer-id-token-markets.ts b/src/migrations/1706152426601-create-column-explorer-id-token-markets.ts new file mode 100644 index 00000000..a7da2bea --- /dev/null +++ b/src/migrations/1706152426601-create-column-explorer-id-token-markets.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createColumnExplorerIdTokenMarkets1706152426601 + implements MigrationInterface +{ + name = 'createColumnExplorerIdTokenMarkets1706152426601'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`token_markets\` ADD \`explorer_id\` int NULL`, + ); + + await queryRunner.query( + `ALTER TABLE \`token_markets\` ADD CONSTRAINT \`FK_085454ab10428ee1a930aeba7b6\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + + await queryRunner.query(`UPDATE \`token_markets\` SET \`explorer_id\` = 1`); + + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`token_markets\` DROP FOREIGN KEY \`FK_085454ab10428ee1a930aeba7b6\``, + ); + + await queryRunner.query( + `ALTER TABLE \`token_markets\` DROP COLUMN \`explorer_id\``, + ); + } +} diff --git a/src/migrations/1706168813269-add-address-prefix-to-explorer.ts b/src/migrations/1706168813269-add-address-prefix-to-explorer.ts new file mode 100644 index 00000000..d1277c81 --- /dev/null +++ b/src/migrations/1706168813269-add-address-prefix-to-explorer.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addAddressPrefixToExplorer1706168813269 + implements MigrationInterface +{ + name = 'addAddressPrefixToExplorer1706168813269'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`explorer\` + ADD \`address_prefix\` varchar(255) NOT NULL, + ADD \`chain_db\` VARCHAR(255) NOT NULL;`, + ); + + await queryRunner.query( + `UPDATE \`explorer\` SET \`address_prefix\` = 'aura', \`chain_db\` = 'xstaxy' WHERE \`id\` = '1'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`explorer\` DROP COLUMN \`address_prefix\`, + DROP COLUMN \`chain_db\``, + ); + } +} diff --git a/src/migrations/1706172325111-add-public-name-tag-to-explorer.ts b/src/migrations/1706172325111-add-public-name-tag-to-explorer.ts new file mode 100644 index 00000000..16d000a5 --- /dev/null +++ b/src/migrations/1706172325111-add-public-name-tag-to-explorer.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addPublicNameTagToExplorer1706172325111 + implements MigrationInterface +{ + name = 'addPublicNameTagToExplorer1706172325111'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`public_name_tag\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`public_name_tag\` + ADD CONSTRAINT \`FK_0292f6ab7ced48369f4dfb6e763\` + FOREIGN KEY (\`explorer_id\`) + REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + // Update current data explorer_id to 1 (aura network) + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query('UPDATE public_name_tag SET explorer_id = 1'); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`public_name_tag\` DROP FOREIGN KEY \`FK_0292f6ab7ced48369f4dfb6e763\``, + ); + await queryRunner.query( + `ALTER TABLE \`public_name_tag\` DROP COLUMN \`explorer_id\``, + ); + } +} diff --git a/src/migrations/1706250495747-add-unique-public-name-tag.ts b/src/migrations/1706250495747-add-unique-public-name-tag.ts new file mode 100644 index 00000000..b3a17b5b --- /dev/null +++ b/src/migrations/1706250495747-add-unique-public-name-tag.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addUniquePublicNameTag1706250495747 implements MigrationInterface { + name = 'addUniquePublicNameTag1706250495747'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX \`IDX_48232194001651d584dbff792d\` ON \`public_name_tag\``, + ); + + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_c923681fe7f04aa3b9c6de97fc\` ON \`public_name_tag\` (\`name_tag\`, \`explorer_id\`)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX \`IDX_c923681fe7f04aa3b9c6de97fc\` ON \`public_name_tag\``, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_48232194001651d584dbff792d\` ON \`public_name_tag\` (\`name_tag\`)`, + ); + } +} diff --git a/src/migrations/1706502043813-add-chain-info-to-explorer.ts b/src/migrations/1706502043813-add-chain-info-to-explorer.ts new file mode 100644 index 00000000..6cd2c760 --- /dev/null +++ b/src/migrations/1706502043813-add-chain-info-to-explorer.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addChainInfoToExplorer1706502043813 implements MigrationInterface { + name = 'addChainInfoToExplorer1706502043813'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`explorer\` ADD \`minimal_denom\` varchar(255) NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`explorer\` ADD \`decimal\` int NOT NULL`, + ); + + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query( + `UPDATE \`explorer\` SET \`minimal_denom\` = 'uaura', \`decimal\` = 6 WHERE \`id\` = 1`, + ); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`explorer\` DROP COLUMN \`decimal\``); + await queryRunner.query( + `ALTER TABLE \`explorer\` DROP COLUMN \`minimal_denom\``, + ); + } +} diff --git a/src/migrations/1706510718520-add-private-name-tag-to-explorer.ts b/src/migrations/1706510718520-add-private-name-tag-to-explorer.ts new file mode 100644 index 00000000..de67a70e --- /dev/null +++ b/src/migrations/1706510718520-add-private-name-tag-to-explorer.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addPrivateNameTagToExplorer1706510718520 + implements MigrationInterface +{ + name = 'addPrivateNameTagToExplorer1706510718520'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`private_name_tag\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`private_name_tag\` ADD CONSTRAINT \`FK_6e5ec60cb4d1774d28c11e3b41a\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + + // Default update current data to 1 (aura network) + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query(`UPDATE private_name_tag SET explorer_id = 1`); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`private_name_tag\` DROP FOREIGN KEY \`FK_6e5ec60cb4d1774d28c11e3b41a\``, + ); + await queryRunner.query( + `ALTER TABLE \`private_name_tag\` DROP COLUMN \`explorer_id\``, + ); + } +} diff --git a/src/migrations/1706586878483-add-watch-list-to-explorer.ts b/src/migrations/1706586878483-add-watch-list-to-explorer.ts new file mode 100644 index 00000000..6168a0f2 --- /dev/null +++ b/src/migrations/1706586878483-add-watch-list-to-explorer.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addWatchListToExplorer1706586878483 implements MigrationInterface { + name = 'addWatchListToExplorer1706586878483'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`watch_list\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`watch_list\` ADD CONSTRAINT \`FK_45843bf84af3e2e815e7aae4359\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + + // Default set explorer_id = 1 (aura network) + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query(`UPDATE watch_list SET explorer_id = 1`); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`watch_list\` DROP FOREIGN KEY \`FK_45843bf84af3e2e815e7aae4359\``, + ); + await queryRunner.query( + `ALTER TABLE \`watch_list\` DROP COLUMN \`explorer_id\``, + ); + } +} diff --git a/src/migrations/1706864486154-add-notification-to-explorer.ts b/src/migrations/1706864486154-add-notification-to-explorer.ts new file mode 100644 index 00000000..e24cc8d1 --- /dev/null +++ b/src/migrations/1706864486154-add-notification-to-explorer.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationToExplorer1706864486154 + implements MigrationInterface +{ + name = 'addNotificationToExplorer1706864486154'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`sync_point\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`user_activity\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`notification\` ADD \`explorer_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`sync_point\` ADD CONSTRAINT \`FK_0a8e5e1a93242576a21fb84b038\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE \`user_activity\` ADD CONSTRAINT \`FK_c34fa2ce1d5f7ffe4ad345509ed\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE \`notification\` ADD CONSTRAINT \`FK_97548ae97923c455c93140651f3\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + + // Update current data explorer_id to 1 (aura network) + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query('UPDATE sync_point SET explorer_id = 1'); + await queryRunner.query('UPDATE user_activity SET explorer_id = 1'); + await queryRunner.query('UPDATE notification SET explorer_id = 1'); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`notification\` DROP FOREIGN KEY \`FK_97548ae97923c455c93140651f3\``, + ); + await queryRunner.query( + `ALTER TABLE \`user_activity\` DROP FOREIGN KEY \`FK_c34fa2ce1d5f7ffe4ad345509ed\``, + ); + await queryRunner.query( + `ALTER TABLE \`sync_point\` DROP FOREIGN KEY \`FK_0a8e5e1a93242576a21fb84b038\``, + ); + await queryRunner.query( + `ALTER TABLE \`notification\` DROP COLUMN \`explorer_id\``, + ); + await queryRunner.query( + `ALTER TABLE \`user_activity\` DROP COLUMN \`explorer_id\``, + ); + await queryRunner.query( + `ALTER TABLE \`sync_point\` DROP COLUMN \`explorer_id\``, + ); + } +} diff --git a/src/migrations/1707986149669-create-asset.ts b/src/migrations/1707986149669-create-asset.ts new file mode 100644 index 00000000..e6ec1f20 --- /dev/null +++ b/src/migrations/1707986149669-create-asset.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createAsset1707986149669 implements MigrationInterface { + name = 'createAsset1707986149669'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`asset\` ( + \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + \`id\` int NOT NULL AUTO_INCREMENT, + \`coin_id\` varchar(255) NULL DEFAULT '', + \`symbol\` varchar(255) NULL, + \`name\` varchar(255) NULL, + \`image\` varchar(255) NULL, + \`current_price\` decimal(38,6) NULL DEFAULT '0.000000', + \`price_change_percentage_24h\` float NULL DEFAULT '0', + \`verify_status\` varchar(255) NULL, + \`verify_text\` varchar(255) NULL, + \`denom\` varchar(255) NULL, + \`decimal\` int NULL DEFAULT '0', + \`chain_id\` varchar(255) NULL, + \`official_site\` varchar(255) NULL, + \`social_profiles\` json NULL, + \`type\` varchar(255) NULL, + \`total_supply\` decimal(38,6) NULL DEFAULT '0.000000', + \`explorer_id\` int NULL, + PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + await queryRunner.query( + `ALTER TABLE \`token_holder_statistic\` ADD \`asset_id\` int NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`token_holder_statistic\` ADD CONSTRAINT \`FK_80e607266e3b43a73180d3b8d6a\` FOREIGN KEY (\`asset_id\`) REFERENCES \`asset\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` ADD CONSTRAINT \`FK_9c2512328742026fd8d7e37da9c\` FOREIGN KEY (\`explorer_id\`) REFERENCES \`explorer\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_caab7846c80a2a9f5ed9d515d8\` ON \`asset\` (\`denom\`, \`explorer_id\`)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`asset\` DROP FOREIGN KEY \`FK_9c2512328742026fd8d7e37da9c\``, + ); + await queryRunner.query( + `ALTER TABLE \`token_holder_statistic\` DROP COLUMN \`asset_id\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_9075b096ce124cae87af8abff7\` ON \`asset\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_caab7846c80a2a9f5ed9d515d8\` ON \`asset\``, + ); + await queryRunner.query(`DROP TABLE \`asset\``); + } +} diff --git a/src/migrations/1708313977334-migration-data-to-assets.ts b/src/migrations/1708313977334-migration-data-to-assets.ts new file mode 100644 index 00000000..39a811ad --- /dev/null +++ b/src/migrations/1708313977334-migration-data-to-assets.ts @@ -0,0 +1,62 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class migrationDataToAssets1708313977334 implements MigrationInterface { + name = 'migrationDataToAssets1708313977334'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO \`asset\` ( + \`id\` + , \`coin_id\` + , \`symbol\` + , \`name\` + , \`image\` + , \`current_price\` + , \`price_change_percentage_24h\` + , \`verify_status\` + , \`verify_text\` + , \`denom\` + , \`decimal\` + , \`official_site\` + , \`social_profiles\` + ) + SELECT + tm.\`id\` + , tm.\`coin_id\` + , tm.\`symbol\` + , tm.\`name\` + , tm.\`image\` + , tm.\`current_price\` + , tm.\`price_change_percentage_24h\` + , tm.\`verify_status\` + , tm.\`verify_text\` + , CASE + WHEN tm.\`denom\` IS NOT NULL THEN tm.\`denom\` + ELSE tm.\`contract_address\` + END AS \`denom\` + , tm.\`decimal\` + , tm.\`official_site\` + , tm.\`social_profiles\` + FROM + \`token_markets\` \`tm\` + WHERE + tm.\`coin_id\` <> '' + AND tm.\`coin_id\` <> 'bitcoin' + `); + + await queryRunner.query(` + UPDATE \`token_holder_statistic\` + SET \`asset_id\` = \`token_market_id\` + WHERE \`asset_id\` IS NULL + `); + + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query( + `UPDATE \`asset\` SET \`name\` = 'Aura Mainnet', \`symbol\` = 'AURA' WHERE \`coin_id\` = 'aura-network'`, + ); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/src/migrations/1708337885523-remove-explorer-from-asset.ts b/src/migrations/1708337885523-remove-explorer-from-asset.ts new file mode 100644 index 00000000..b9e61ad2 --- /dev/null +++ b/src/migrations/1708337885523-remove-explorer-from-asset.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class removeExplorerFromAsset1708337885523 + implements MigrationInterface +{ + name = 'removeExplorerFromAsset1708337885523'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`asset\` DROP FOREIGN KEY \`FK_9c2512328742026fd8d7e37da9c\``, + ); + await queryRunner.query( + `DROP INDEX \`IDX_caab7846c80a2a9f5ed9d515d8\` ON \`asset\``, + ); + await queryRunner.query(`ALTER TABLE \`asset\` DROP COLUMN \`chain_id\``); + await queryRunner.query( + `ALTER TABLE \`asset\` DROP COLUMN \`explorer_id\``, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` ADD UNIQUE INDEX \`IDX_dfaa8912e3a864dae14becfe74\` (\`denom\`)`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`coin_id\` \`coin_id\` varchar(255) NULL DEFAULT ''`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`symbol\` \`symbol\` varchar(255) NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`name\` \`name\` varchar(255) NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`image\` \`image\` varchar(255) NULL`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`current_price\` \`current_price\` decimal(38,6) NULL DEFAULT '0.000000'`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`price_change_percentage_24h\` \`price_change_percentage_24h\` float NULL DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`total_supply\` \`total_supply\` decimal(60,6) NULL DEFAULT '0.000000'`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` CHANGE \`decimal\` \`decimal\` int NULL DEFAULT '0'`, + ); + } + + public async down(): Promise { + //No action. + } +} diff --git a/src/migrations/1708939691028-add-date-to-token-holder-statisitic.ts b/src/migrations/1708939691028-add-date-to-token-holder-statisitic.ts new file mode 100644 index 00000000..0b7a1a0f --- /dev/null +++ b/src/migrations/1708939691028-add-date-to-token-holder-statisitic.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addDateToTokenHolderStatistic1708939691028 + implements MigrationInterface +{ + name = 'addDateToTokenHolderStatistic1708939691028'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`token_holder_statistic\` ADD \`date\` date NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`asset_id_date\` ON \`token_holder_statistic\` (\`asset_id\`, \`date\`)`, + ); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 0`); + await queryRunner.query( + `UPDATE token_holder_statistic set date = created_at where date is null`, + ); + await queryRunner.query(`SET SQL_SAFE_UPDATES = 1`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX \`asset_id_date\` ON \`token_holder_statistic\``, + ); + await queryRunner.query( + `ALTER TABLE \`token_holder_statistic\` DROP COLUMN \`date\``, + ); + } +} diff --git a/src/migrations/1709173688516-add-coin-info-to-asset.ts b/src/migrations/1709173688516-add-coin-info-to-asset.ts new file mode 100644 index 00000000..f83e58e7 --- /dev/null +++ b/src/migrations/1709173688516-add-coin-info-to-asset.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addCoinInfoToAsset1709173688516 implements MigrationInterface { + name = 'addCoinInfoToAsset1709173688516'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`asset\` ADD \`total_volume\` decimal(38,6) NOT NULL DEFAULT '0.000000'`, + ); + await queryRunner.query( + `ALTER TABLE \`asset\` ADD \`market_cap\` decimal(38,6) NOT NULL DEFAULT '0.000000'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`asset\` DROP COLUMN \`market_cap\``); + await queryRunner.query( + `ALTER TABLE \`asset\` DROP COLUMN \`total_volume\``, + ); + } +} diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index 1c7b4b6c..07aea28b 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -3,7 +3,11 @@ export const VALIDATION_PIPE_OPTIONS = { transform: true }; // eslint-disable-next-line @typescript-eslint/no-var-requires require('dotenv').config(); -const INDEXER_V2_DB = process.env.INDEXER_V2_DB; +export const INDEXER_V2_DB = process.env.INDEXER_V2_DB; + +export const REQUEST_CHAIN_ID_HEADER = 'chain-id'; + +export const DEFAULT_CHAIN_ID_HEADER = process.env.INDEXER_CHAIN_ID; export const REQUEST_ID_TOKEN_HEADER = 'x-request-id'; @@ -58,7 +62,7 @@ export const INDEXER_API_V2 = { VALIDATORS: `query Validators { %s { validator { %s } } }`, CW4973_STATUS: `query QueryCW4973Status($heightGT: Int, $limit: Int) { ${INDEXER_V2_DB} { cw721_activity(where: {cw721_contract: {smart_contract: {name: {_eq: "crates.io:cw4973"}}}, height: {_gt: $heightGT}}, order_by: {height: asc}, limit: $limit) { height tx { transaction_messages { content } } cw721_contract {smart_contract {address}} sender}}}`, TX_EXECUTED: `query QueryTxOfAccount($startTime: timestamptz = null, $endTime: timestamptz = null, $limit: Int = null, $listTxMsgType: [String!] = null, $listTxMsgTypeNotIn: [String!] = null, $heightGT: Int = null, $heightLT: Int = null, $orderHeight: order_by = desc, $address: String = null) { - ${INDEXER_V2_DB} { + %s { transaction(where: {timestamp: {_lte: $endTime, _gte: $startTime}, transaction_messages: {type: {_in: $listTxMsgType, _nin: $listTxMsgTypeNotIn}, sender: {_eq: $address}}, _and: [{height: {_gt: $heightGT, _lt: $heightLT}}]}, limit: $limit, order_by: {height: $orderHeight}) { hash height @@ -73,7 +77,7 @@ export const INDEXER_API_V2 = { } }`, TX_COIN_TRANSFER: `query QueryTxMsgOfAccount($from: String = "_", $to: String = "_", $startTime: timestamptz = null, $endTime: timestamptz = null, $heightGT: Int = null, $heightLT: Int = null, $limit: Int = null) { - ${INDEXER_V2_DB} { + %s { transaction(where: {coin_transfers: {_or: [{from: {_eq: $from}}, {to: {_eq: $to}}], block_height: {_lt: $heightLT, _gt: $heightGT}}, timestamp: {_lte: $endTime, _gte: $startTime}}, limit: $limit, order_by: {height: desc}) { hash height @@ -95,7 +99,7 @@ export const INDEXER_API_V2 = { } }`, TX_TOKEN_TRANSFER: `query Cw20TXMultilCondition($receiver: String = null, $sender: String = null, $heightGT: Int = null, $heightLT: Int = null, $limit: Int = 100, $actionIn: [String!] = null, $startTime: timestamptz = null, $endTime: timestamptz = null) { - ${INDEXER_V2_DB} { + %s { transaction: cw20_activity(where: {_or: [{to: {_eq: $receiver}}, {from: {_eq: $sender}}], cw20_contract: {}, action: {_in: $actionIn}, height: {_gt: $heightGT, _lt: $heightLT}, tx: {timestamp: {_lte: $endTime, _gte: $startTime}}}, order_by: {height: desc}, limit: $limit) { action amount @@ -132,7 +136,7 @@ export const INDEXER_API_V2 = { $startTime: timestamptz = null $endTime: timestamptz = null ) { - ${INDEXER_V2_DB} { + %s { transaction: cw721_activity( where: { _or: [{ to: { _eq: $receiver } }, { from: { _eq: $sender } }] @@ -174,7 +178,7 @@ export const INDEXER_API_V2 = { } `, EXECUTED_NOTIFICATION: `query ExecutedNotification($heightGT: Int, $heightLT: Int) { - ${INDEXER_V2_DB} { + %s { executed: transaction(where: {height: {_gt: $heightGT, _lt: $heightLT}, code: {_eq: 0}}, order_by: {height: desc}, limit: 100) { height hash @@ -188,7 +192,7 @@ export const INDEXER_API_V2 = { } `, COIN_TRANSFER_NOTIFICATION: `query CoinTransferNotification($heightGT: Int = null, $heightLT: Int = null) { - ${INDEXER_V2_DB} { + %s { coin_transfer: transaction(where: {coin_transfers: {block_height: {_lt: $heightLT, _gt: $heightGT}}}, limit: 100, order_by: {height: desc}) { hash height @@ -207,7 +211,7 @@ export const INDEXER_API_V2 = { } `, TOKEN_TRANSFER_NOTIFICATION: `query TokenTransferNotification($heightGT: Int, $heightLT: Int, $listFilterCW20: [String!] = null) { - ${INDEXER_V2_DB} { + %s { token_transfer: cw20_activity(where: {height: {_gt: $heightGT, _lt: $heightLT}, amount: {_is_null: false}, action: {_in: $listFilterCW20}}, order_by: {height: desc}, limit: 100) { height tx_hash @@ -226,7 +230,7 @@ export const INDEXER_API_V2 = { } `, NFT_TRANSFER_NOTIFICATION: `query NftTransferNotification($heightGT: Int, $heightLT: Int, $listFilterCW721: [String!] = null) { - ${INDEXER_V2_DB} { + %s { nft_transfer: cw721_activity(where: {action: {_in: $listFilterCW721}, cw721_token: {token_id: {_is_null: false}}, cw721_contract: {smart_contract: {name: {_neq: "crates.io:cw4973"}}}, height: {_gt: $heightGT, _lt: $heightLT}}, order_by: {height: desc}, limit: 100) { tx_hash height @@ -251,9 +255,9 @@ export const INDEXER_API_V2 = { } }`, BASE_QUERY: `query BaseQuery { - ${INDEXER_V2_DB} { %s } }`, + %s { %s } }`, LIST_VALIDATOR: `query ListValidator($address: [String!] = null) { - ${INDEXER_V2_DB} { + %s { validator(where: {account_address: {_in: $address}}) { account_address operator_address @@ -261,14 +265,59 @@ export const INDEXER_API_V2 = { } }`, LIST_ACCOUNT: `query ListAccount($address: [String!] = null) { - ${INDEXER_V2_DB} { - account(where: {address: {_in: $address}}) { + %s { + account(where: {address: {_in: $address}}) { spendable_balances balances - address - } + address + } } }`, + + ASSETS: `query Assets( + $from: timestamptz = null + $id_gt: Int = null + ) { + ${INDEXER_V2_DB} { + asset( + where: { updated_at: { _gte: $from }, id: { _gt: $id_gt } } + order_by: { id: asc } + ) { + decimal + denom + name + total_supply + type + updated_at + id + } + } + } + `, + CW20_HOLDER_STAT: `query Cw20HolderStat($date_eq: date = null, $id_gt: Int = null) { + ${INDEXER_V2_DB} { + cw20_contract( + where: { track: { _eq: true }, id: { _gt: $id_gt } } + order_by: { id: asc } + ) { + smart_contract { + address + } + cw20_total_holder_stats( + where: { date: { _eq: $date_eq } } + limit: 1 + order_by: { date: desc } + ) { + total_holder + date + } + marketing_info + symbol + id + } + } + } + `, }, OPERATION_NAME: { PROPOSAL_COUNT: 'CountProposal', @@ -295,7 +344,10 @@ export const INDEXER_API_V2 = { BASE_QUERY: 'BaseQuery', LIST_VALIDATOR: 'ListValidator', LIST_ACCOUNT: 'ListAccount', + ASSETS: 'Assets', + CW20_HOLDER_STAT: 'Cw20HolderStat', }, + MAX_REQUEST: 100, }; export enum AURA_INFO { @@ -410,7 +462,7 @@ export const ADMIN_ERROR_MAP = { }, INVALID_FORMAT: { Code: 'E003', - Message: 'Invalid aura address format', + Message: `Invalid %s address format`, }, INVALID_NAME_TAG: { Code: 'E004', @@ -436,6 +488,12 @@ export const PAGE_REQUEST = { MAX_500: 500, }; +export enum ASSETS_TYPE { + IBC = 'IBC_TOKEN', + CW20 = 'CW20_TOKEN', + NATIVE = 'NATIVE', +} + export enum SOULBOUND_TOKEN_STATUS { UNCLAIM = 'Unclaimed', EQUIPPED = 'Equipped', @@ -539,10 +597,13 @@ export const QUEUES = { JOB: 'job-send-mail', }, TOKEN: { - QUEUE_NAME: 'token-price-queue', + QUEUE_NAME: 'asset', JOB_SYNC_TOKEN_PRICE: 'sync-token-price', JOB_SYNC_CW20_PRICE: 'sync-cw20-price', - JOB_SYNC_TOKEN_HOLDER: 'sync-token-holder', + JOB_SYNC_ASSET: 'sync-asset', + JOB_SYNC_NATIVE_ASSET_HOLDER: 'sync-native-asset-holder', + JOB_SYNC_CW20_ASSET_HOLDER: 'sync-cw20-asset-holder', + JOB_CLEAN_ASSET_HOLDER: 'clean-asset-holder', }, CW4973: { QUEUE_NAME: 'cw4973', @@ -586,6 +647,7 @@ export enum SYNC_POINT_TYPE { COIN_TRANSFER_HEIGHT = 'COIN_TRANSFER_HEIGHT', TOKEN_TRANSFER_HEIGHT = 'TOKEN_TRANSFER_HEIGHT', NFT_TRANSFER_HEIGHT = 'NFT_TRANSFER_HEIGHT', + FIRST_TIME_SYNC_ASSETS = 'FIRST_TIME_SYNC_ASSETS', } export const TX_HEADER = { @@ -755,6 +817,7 @@ export const WATCH_LIST = { ERR_LIMIT_ADDRESS: `You have reached out of ${ process.env.WATCH_LIST_LIMIT_ADDRESS || 20 } max limitation of address.`, + ERR_INVALID_ADDRESS: `Invalid %s format address.`, }, }; @@ -767,3 +830,14 @@ export const RPC_QUERY_URL = { VALIDATOR_COMMISSION: '/cosmos.distribution.v1beta1.Query/ValidatorCommission', }; + +export const TYPE_ORM_ERROR_CODE = { + ER_DUP_ENTRY: 'ER_DUP_ENTRY', +}; + +export const COSMOS = { + ADDRESS_LENGTH: { + ACCOUNT_HEX: 39, + CONTRACT_HEX: 59, + }, +}; diff --git a/src/shared/entities/asset.entity.ts b/src/shared/entities/asset.entity.ts new file mode 100644 index 00000000..17ffaf36 --- /dev/null +++ b/src/shared/entities/asset.entity.ts @@ -0,0 +1,92 @@ +import { Column, Entity, OneToMany, Unique } from 'typeorm'; +import { BaseEntityIncrementId } from './base/base.entity'; +import { TokenHolderStatistic } from './token-holder-statistic.entity'; + +@Entity('asset') +@Unique(['denom']) +export class Asset extends BaseEntityIncrementId { + @Column({ name: 'coin_id', nullable: true, default: '' }) + coinId: string; + + @Column({ nullable: true }) + symbol: string; + + @Column({ nullable: true }) + name: string; + + @Column({ nullable: true }) + image: string; + + @Column({ + name: 'current_price', + type: 'decimal', + precision: 38, + scale: 6, + default: 0, + nullable: true, + }) + currentPrice: number; + + @Column({ + name: 'price_change_percentage_24h', + type: 'float', + default: 0, + nullable: true, + }) + priceChangePercentage24h: number; + + @Column({ name: 'verify_status', nullable: true }) + verifyStatus: string; + + @Column({ name: 'verify_text', nullable: true }) + verifyText: string; + + @Column({ name: 'denom', nullable: true }) + denom: string; + + @Column({ name: 'decimal', default: 0, nullable: true }) + decimal: number; + + @Column({ name: 'official_site', nullable: true }) + officialSite: string; + + @Column({ name: 'social_profiles', nullable: true, type: 'json' }) + socialProfiles: JSON; + + @Column({ name: 'type', nullable: true }) + type: string; + + @Column({ + name: 'total_supply', + type: 'decimal', + precision: 60, + scale: 6, + default: 0, + nullable: true, + }) + totalSupply: number; + + @Column({ + name: 'total_volume', + type: 'decimal', + precision: 38, + scale: 6, + default: 0, + }) + totalVolume: number; + + @Column({ + name: 'market_cap', + type: 'decimal', + precision: 38, + scale: 6, + default: 0, + }) + marketCap: number; + + @OneToMany( + () => TokenHolderStatistic, + (tokenHolderStatistic) => tokenHolderStatistic.asset, + ) + tokenHolderStatistics: TokenHolderStatistic[]; +} diff --git a/src/shared/entities/explorer.entity.ts b/src/shared/entities/explorer.entity.ts new file mode 100644 index 00000000..9f44f961 --- /dev/null +++ b/src/shared/entities/explorer.entity.ts @@ -0,0 +1,52 @@ +import { Column, Entity, OneToMany } from 'typeorm'; +import { BaseEntityIncrementId } from './base/base.entity'; +import { PublicNameTag } from './public-name-tag.entity'; +import { TokenMarkets } from './token-markets.entity'; +import { PrivateNameTag } from './private-name-tag.entity'; +import { SyncPoint } from './sync-point.entity'; +import { UserActivity } from './user-activity.entity'; +import { Notification } from './notification.entity'; +import { WatchList } from './watch-list.entity'; +import { Asset } from './asset.entity'; + +@Entity('explorer') +export class Explorer extends BaseEntityIncrementId { + @Column({ name: 'chain_id' }) + chainId: string; + + @Column() + name: string; + + @Column({ name: 'address_prefix' }) + addressPrefix: string; + + @Column({ name: 'chain_db' }) + chainDb: string; + + @Column({ name: 'minimal_denom' }) + minimalDenom: string; + + @Column({ name: 'decimal' }) + decimal: number; + + @OneToMany(() => PublicNameTag, (publicNameTag) => publicNameTag.explorer) + publicNameTags: PublicNameTag[]; + + @OneToMany(() => TokenMarkets, (token) => token.explorer) + tokenMarkets: TokenMarkets[]; + + @OneToMany(() => PrivateNameTag, (privateNameTag) => privateNameTag.explorer) + privateNameTags: PrivateNameTag[]; + + @OneToMany(() => WatchList, (watchList) => watchList.explorer) + watchLists: WatchList[]; + + @OneToMany(() => SyncPoint, (syncPoint) => syncPoint.explorer) + syncPoints: SyncPoint[]; + + @OneToMany(() => UserActivity, (userActivity) => userActivity.explorer) + userActivities: UserActivity[]; + + @OneToMany(() => Notification, (userActivity) => userActivity.explorer) + notifications: Notification[]; +} diff --git a/src/shared/entities/notification.entity.ts b/src/shared/entities/notification.entity.ts index 7178980c..75762844 100644 --- a/src/shared/entities/notification.entity.ts +++ b/src/shared/entities/notification.entity.ts @@ -1,5 +1,6 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; +import { Explorer } from './explorer.entity'; @Entity('notification') export class Notification extends BaseEntityIncrementId { @@ -30,4 +31,10 @@ export class Notification extends BaseEntityIncrementId { @Column({ default: false }) is_read: boolean; + + @ManyToOne(() => Explorer, (explorer) => explorer.notifications) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/private-name-tag.entity.ts b/src/shared/entities/private-name-tag.entity.ts index 735b569b..01ca50b0 100644 --- a/src/shared/entities/private-name-tag.entity.ts +++ b/src/shared/entities/private-name-tag.entity.ts @@ -3,10 +3,13 @@ import { CreateDateColumn, Entity, Index, + JoinColumn, + ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; import { NAME_TAG_TYPE } from '../constants/common'; +import { Explorer } from './explorer.entity'; @Entity('private_name_tag') @Index(['address', 'createdBy'], { unique: true }) @@ -43,4 +46,10 @@ export class PrivateNameTag { name: 'updated_at', }) updatedAt: Date; + + @ManyToOne(() => Explorer, (explorer) => explorer.privateNameTags) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/public-name-tag.entity.ts b/src/shared/entities/public-name-tag.entity.ts index f9252fb1..21c807c4 100644 --- a/src/shared/entities/public-name-tag.entity.ts +++ b/src/shared/entities/public-name-tag.entity.ts @@ -1,10 +1,11 @@ -import { Column, Entity, Unique } from 'typeorm'; +import { Column, Entity, Index, JoinColumn, ManyToOne, Unique } from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; import { NAME_TAG_TYPE } from '../constants/common'; +import { Explorer } from './explorer.entity'; @Entity('public_name_tag') -@Unique(['name_tag']) @Unique(['address']) +@Index(['name_tag', 'explorer'], { unique: true }) export class PublicNameTag extends BaseEntityIncrementId { @Column() type: NAME_TAG_TYPE; @@ -20,4 +21,10 @@ export class PublicNameTag extends BaseEntityIncrementId { @Column({ nullable: true, name: 'enterprise_url' }) enterpriseUrl: string; + + @ManyToOne(() => Explorer, (explorer) => explorer.publicNameTags) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/sync-point.entity.ts b/src/shared/entities/sync-point.entity.ts index bc2575e8..b357bb1e 100644 --- a/src/shared/entities/sync-point.entity.ts +++ b/src/shared/entities/sync-point.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; import { SYNC_POINT_TYPE } from '../constants/common'; +import { Explorer } from './explorer.entity'; @Entity('sync_point') export class SyncPoint extends BaseEntityIncrementId { @@ -9,4 +10,10 @@ export class SyncPoint extends BaseEntityIncrementId { @Column({ nullable: true }) point: number; + + @ManyToOne(() => Explorer, (explorer) => explorer.syncPoints) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/token-holder-statistic.entity.ts b/src/shared/entities/token-holder-statistic.entity.ts index 4523d0dd..6400188d 100644 --- a/src/shared/entities/token-holder-statistic.entity.ts +++ b/src/shared/entities/token-holder-statistic.entity.ts @@ -1,12 +1,17 @@ -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, Unique } from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; import { TokenMarkets } from './token-markets.entity'; +import { Asset } from './asset.entity'; @Entity('token_holder_statistic') +@Unique('asset_id_date', ['asset', 'date']) export class TokenHolderStatistic extends BaseEntityIncrementId { @Column({ nullable: true, name: 'total_holder' }) totalHolder: number; + @Column({ nullable: true, name: 'date', type: 'date' }) + date: Date; + @ManyToOne( () => TokenMarkets, (tokenMarket) => tokenMarket.tokenHolderStatistics, @@ -15,4 +20,10 @@ export class TokenHolderStatistic extends BaseEntityIncrementId { name: 'token_market_id', }) tokenMarket: TokenMarkets; + + @ManyToOne(() => Asset, (asset) => asset.tokenHolderStatistics) + @JoinColumn({ + name: 'asset_id', + }) + asset: Asset; } diff --git a/src/shared/entities/token-markets.entity.ts b/src/shared/entities/token-markets.entity.ts index 1e167415..f6fdf01c 100644 --- a/src/shared/entities/token-markets.entity.ts +++ b/src/shared/entities/token-markets.entity.ts @@ -1,6 +1,15 @@ -import { Column, Entity, Index, OneToMany, Unique } from 'typeorm'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + Unique, +} from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; import { TokenHolderStatistic } from './token-holder-statistic.entity'; +import { Explorer } from './explorer.entity'; @Entity('token_markets') @Unique(['contract_address']) @@ -124,4 +133,10 @@ export class TokenMarkets extends BaseEntityIncrementId { }, ) tokenHolderStatistics: TokenHolderStatistic[]; + + @ManyToOne(() => Explorer, (explorer) => explorer.tokenMarkets) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/user-activity.entity.ts b/src/shared/entities/user-activity.entity.ts index 5f4893b7..8986d1c7 100644 --- a/src/shared/entities/user-activity.entity.ts +++ b/src/shared/entities/user-activity.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { USER_ACTIVITIES } from '../constants'; import { BaseEntityIncrementId } from './base/base.entity'; import { User } from './user.entity'; +import { Explorer } from './explorer.entity'; @Entity('user_activity') export class UserActivity extends BaseEntityIncrementId { @@ -22,4 +23,10 @@ export class UserActivity extends BaseEntityIncrementId { name: 'user_id', }) user: User; + + @ManyToOne(() => Explorer, (explorer) => explorer.userActivities) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/entities/watch-list.entity.ts b/src/shared/entities/watch-list.entity.ts index e898ded4..a6ef2e0c 100644 --- a/src/shared/entities/watch-list.entity.ts +++ b/src/shared/entities/watch-list.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; import { BaseEntityIncrementId } from './base/base.entity'; import { User } from './user.entity'; import { WATCH_LIST } from '../constants/common'; +import { Explorer } from './explorer.entity'; @Entity('watch_list') @Index(['address', 'user'], { unique: true }) @@ -29,4 +30,10 @@ export class WatchList extends BaseEntityIncrementId { name: 'user_id', }) user: User; + + @ManyToOne(() => Explorer, (explorer) => explorer.watchLists) + @JoinColumn({ + name: 'explorer_id', + }) + explorer: Explorer; } diff --git a/src/shared/helpers/transaction.helper.ts b/src/shared/helpers/transaction.helper.ts index b95233df..9f244f53 100644 --- a/src/shared/helpers/transaction.helper.ts +++ b/src/shared/helpers/transaction.helper.ts @@ -9,11 +9,12 @@ import { TypeTransaction, } from '../constants/transaction'; import BigNumber from 'bignumber.js'; +import { Explorer } from '../entities/explorer.entity'; export class TransactionHelper { static convertDataAccountTransaction( data, - coinInfo, + coinInfo: Explorer, modeQuery, currentAddress, coinConfig = null, @@ -38,7 +39,7 @@ export class TransactionHelper { } const lstType = this.getTypeTxMsg(lstTypeTemp); - let denom = coinInfo.coinDenom; + let denom = coinInfo.minimalDenom; const _amount = _.get(element, 'events[0].event_attributes[2].value'); const value = _amount?.match(/\d+/g); let amount = this.balanceOf(value?.length > 0 ? value[0] : 0); @@ -50,8 +51,8 @@ export class TransactionHelper { const fee = this.balanceOf( _.get(element, 'fee[0].amount') || 0, - coinInfo.coinDecimals, - ).toFixed(coinInfo.coinDecimals); + coinInfo.decimal, + ).toFixed(coinInfo.decimal); const height = _.get(element, 'height'); const timestamp = _.get(element, 'timestamp') || _.get(element, 'tx.timestamp'); @@ -80,7 +81,7 @@ export class TransactionHelper { // Get denom ibc not find in config or denom is native const denom = coin.denom?.indexOf('ibc') === -1 - ? coinInfo.coinDenom + ? coinInfo.minimalDenom : coin.denom; if (coin.to === currentAddress || coin.from === currentAddress) { @@ -91,14 +92,14 @@ export class TransactionHelper { fromAddress: coin.from, amount: this.balanceOf( Number(coin.amount) || 0, - dataIBC['decimal'] || coinInfo.coinDecimals, + dataIBC['decimal'] || coinInfo.decimal, ), denom: denomIBC || denom, action, denomOrigin: coin.denom?.indexOf('ibc') === -1 ? '' : coin.denom, amountTemp: coin.amount, - decimal: dataIBC['decimal'] || coinInfo.coinDecimals, + decimal: dataIBC['decimal'] || coinInfo.decimal, }; arrTemp.push(result); } diff --git a/src/shared/index.ts b/src/shared/index.ts index 59aed7ba..492a7091 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -19,3 +19,4 @@ export * from './auth/constants/role.constant'; export * from './entities/token-markets.entity'; export * from './entities/soulbound-token.entity'; +export * from './entities/asset.entity'; diff --git a/src/shared/request-context/request-context.dto.ts b/src/shared/request-context/request-context.dto.ts index 6c5a95d9..39587b7d 100644 --- a/src/shared/request-context/request-context.dto.ts +++ b/src/shared/request-context/request-context.dto.ts @@ -8,4 +8,6 @@ export class RequestContext { public ip: string; public user: UserAccessTokenClaims; + + public chainId: string; } diff --git a/src/shared/request-context/utils.ts b/src/shared/request-context/utils.ts index a3a6bf92..6d8f5a02 100644 --- a/src/shared/request-context/utils.ts +++ b/src/shared/request-context/utils.ts @@ -1,5 +1,7 @@ import { + DEFAULT_CHAIN_ID_HEADER, FORWARDED_FOR_TOKEN_HEADER, + REQUEST_CHAIN_ID_HEADER, REQUEST_ID_TOKEN_HEADER, } from '../constants'; @@ -7,6 +9,8 @@ import { RequestContext } from './request-context.dto'; export function createRequestContext(request: any): RequestContext { const ctx = new RequestContext(); + ctx.chainId = + request.header(REQUEST_CHAIN_ID_HEADER) || DEFAULT_CHAIN_ID_HEADER; ctx.requestId = request.header(REQUEST_ID_TOKEN_HEADER); ctx.url = request.url; ctx.ip = request.header(FORWARDED_FOR_TOKEN_HEADER) diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts index c8f2d03b..fc0e677d 100644 --- a/src/shared/shared.module.ts +++ b/src/shared/shared.module.ts @@ -17,10 +17,11 @@ import { AkcLoggerModule } from './logger/logger.module'; import { RedisUtil } from './utils/redis.util'; import { EncryptionModule } from '../components/encryption/encryption.module'; import { CipherKey } from './entities/cipher-key.entity'; +import { Explorer } from './entities/explorer.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([CipherKey]), + TypeOrmModule.forFeature([CipherKey, Explorer]), ConfigModule.forRoot(configModuleOptions), TypeOrmModule.forRootAsync({ imports: [ConfigModule], diff --git a/src/shared/utils/rpc.util.ts b/src/shared/utils/rpc.util.ts new file mode 100644 index 00000000..73cecf9b --- /dev/null +++ b/src/shared/utils/rpc.util.ts @@ -0,0 +1,75 @@ +import { + Injectable, + InternalServerErrorException, + OnModuleInit, +} from '@nestjs/common'; +import { AkcLogger } from '../logger/logger.service'; +import { ConfigService } from '@nestjs/config'; +import { AURA_INFO } from '../constants'; +import { HttpBatchClient } from '@cosmjs/tendermint-rpc'; +import { toHex } from '@cosmjs/encoding'; +import { JsonRpcRequest } from '@cosmjs/json-rpc'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Explorer } from '../entities/explorer.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class RpcUtil implements OnModuleInit { + private listBatch: Array = []; + + constructor( + private readonly logger: AkcLogger, + private configService: ConfigService, + @InjectRepository(Explorer) + private readonly explorerRepository: Repository, + ) {} + + async onModuleInit() { + try { + const explorer = await this.explorerRepository.find(); + explorer?.forEach((item) => { + this.listBatch.push({ + chainId: item.chainId, + batchClient: new HttpBatchClient( + this.configService.get( + item.addressPrefix === AURA_INFO.ADDRESS_PREFIX + ? 'RPC' + : `${item.addressPrefix.toUpperCase()}_RPC`, + ), + { + batchSizeLimit: 100, + dispatchInterval: 100, // millisec + }, + ), + }); + }); + } catch (error) { + this.logger.error( + null, + `Error while create instance batchClient from RPC! ${error}`, + ); + } + } + + async queryComosRPC(path: string, data: Uint8Array, chainId: string) { + try { + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id: Math.floor(Math.random() * 10000000), + method: 'abci_query', + params: { + path: path, + data: toHex(data), + }, + }; + const batch = this.listBatch.find((item) => item.chainId === chainId); + return await batch?.batchClient.execute(request); + } catch (error) { + this.logger.error( + null, + `Error while querying ${path} from RPC! ${error}`, + ); + throw new InternalServerErrorException(error); + } + } +} diff --git a/src/shared/utils/service.util.ts b/src/shared/utils/service.util.ts index 85ba9858..d6852d9d 100644 --- a/src/shared/utils/service.util.ts +++ b/src/shared/utils/service.util.ts @@ -5,26 +5,24 @@ import { lastValueFrom } from 'rxjs'; import axios from 'axios'; import { bech32 } from 'bech32'; import { ConfigService } from '@nestjs/config'; -import { AURA_INFO, CW4973_CONTRACT, DEFAULT_IPFS } from '../constants'; +import { + AURA_INFO, + COSMOS, + CW4973_CONTRACT, + DEFAULT_IPFS, + NAME_TAG_TYPE, +} from '../constants'; import { sha256 } from 'js-sha256'; -import { HttpBatchClient } from '@cosmjs/tendermint-rpc'; -import { toHex } from '@cosmjs/encoding'; -import { JsonRpcRequest } from '@cosmjs/json-rpc'; @Injectable() export class ServiceUtil { private readonly indexerV2; - private batchClient: HttpBatchClient; constructor( private readonly logger: AkcLogger, private httpService: HttpService, private configService: ConfigService, ) { this.indexerV2 = this.configService.get('indexerV2'); - this.batchClient = new HttpBatchClient(this.configService.get('node.rpc'), { - batchSizeLimit: 100, - dispatchInterval: 100, // millisec - }); } /** @@ -70,7 +68,6 @@ export class ServiceUtil { } async fetchDataFromGraphQL(query, endpoint?, method?) { - this.logger.log(query, `${this.fetchDataFromGraphQL.name} was called`); endpoint = endpoint ? endpoint : this.indexerV2.graphQL; method = method ? method : 'POST'; @@ -164,27 +161,6 @@ export class ServiceUtil { return value.replace(DEFAULT_IPFS, ipfsUrl); } } - - async queryComosRPC(path: string, data: Uint8Array) { - try { - const request: JsonRpcRequest = { - jsonrpc: '2.0', - id: Math.floor(Math.random() * 10000000), - method: 'abci_query', - params: { - path: path, - data: toHex(data), - }, - }; - return await this.batchClient.execute(request); - } catch (error) { - this.logger.error( - null, - `Error while querying ${path} from RPC! ${error}`, - ); - return null; - } - } } export function secondsToDate(seconds: number): Date { @@ -192,9 +168,11 @@ export function secondsToDate(seconds: number): Date { return new Date(seconds * secondsToMilliseconds); } -export async function isValidBench32Address(address: string): Promise { - const prefix = AURA_INFO.ADDRESS_PREFIX; - +export function isValidBench32Address( + address: string, + prefix = AURA_INFO.ADDRESS_PREFIX.toString(), + type?: string, +): boolean { if (!address) { return false; } @@ -208,6 +186,15 @@ export async function isValidBench32Address(address: string): Promise { ); } + const addressHexLength = address.length - decodedPrefix.length; + + switch (type) { + case NAME_TAG_TYPE.ACCOUNT: + return addressHexLength === COSMOS.ADDRESS_LENGTH.ACCOUNT_HEX; + case NAME_TAG_TYPE.CONTRACT: + return addressHexLength === COSMOS.ADDRESS_LENGTH.CONTRACT_HEX; + } + return true; } catch (error) { return false;