diff --git a/src/tokens/tokens.interfaces.ts b/src/tokens/tokens.interfaces.ts index d9805a4..09a8ac5 100644 --- a/src/tokens/tokens.interfaces.ts +++ b/src/tokens/tokens.interfaces.ts @@ -317,6 +317,9 @@ export class TokenPoolEvent extends tokenEventBase { @ApiProperty() symbol: string; + @ApiProperty({ type: 'integer' }) + decimals: number; + @ApiProperty() info: TokenPoolEventInfo; } diff --git a/src/tokens/tokens.service.spec.ts b/src/tokens/tokens.service.spec.ts index 6d330e8..019c3d5 100644 --- a/src/tokens/tokens.service.spec.ts +++ b/src/tokens/tokens.service.spec.ts @@ -85,6 +85,7 @@ const ERC721_TRANSFER_NO_DATA = 'safeTransferFrom'; const BURN_NO_DATA = 'burn'; const APPROVE_NO_DATA = 'approve'; const APPROVE_ALL_NO_DATA = 'setApprovalForAll'; +const DECIMALS = 'decimals'; const MINT_WITH_DATA = 'mintWithData'; const TRANSFER_WITH_DATA = 'transferWithData'; @@ -92,14 +93,7 @@ const BURN_WITH_DATA = 'burnWithData'; const APPROVE_WITH_DATA = 'approveWithData'; const APPROVE_ALL_WITH_DATA = 'setApprovalForAllWithData'; -const METHODS_NO_DATA = [ - MINT_NO_DATA, - BURN_NO_DATA, - APPROVE_NO_DATA, - APPROVE_ALL_NO_DATA, - 'name', - 'symbol', -]; +const METHODS_NO_DATA = [MINT_NO_DATA, BURN_NO_DATA, APPROVE_NO_DATA, APPROVE_ALL_NO_DATA]; const METHODS_WITH_DATA = [ MINT_WITH_DATA, @@ -107,8 +101,6 @@ const METHODS_WITH_DATA = [ TRANSFER_WITH_DATA, APPROVE_WITH_DATA, APPROVE_ALL_WITH_DATA, - 'name', - 'symbol', ]; const TRANSFER_EVENT = 'Transfer'; @@ -148,7 +140,7 @@ describe('TokensService', () => { getOrCreateSubscription: jest.fn(), }; - const mockNameAndSymbolQuery = () => { + const mockPoolQuery = (withDecimals: boolean) => { http.post .mockReturnValueOnce( new FakeObservable({ @@ -160,6 +152,13 @@ describe('TokensService', () => { output: SYMBOL, }), ); + if (withDecimals) { + http.post.mockReturnValueOnce( + new FakeObservable({ + output: '18', + }), + ); + } }; beforeEach(async () => { @@ -213,7 +212,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(true); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -222,6 +221,7 @@ describe('TokensService', () => { standard: 'ERC20', type: 'fungible', symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -246,6 +246,7 @@ describe('TokensService', () => { standard: 'ERC20', type: TokenType.FUNGIBLE, symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -253,7 +254,7 @@ describe('TokensService', () => { }, }; - mockNameAndSymbolQuery(); + mockPoolQuery(true); eventstream.createOrUpdateStream = jest.fn(() => mockEventStream); eventstream.getOrCreateSubscription = jest.fn(() => new FakeObservable(undefined)); @@ -382,7 +383,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(true); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -391,6 +392,7 @@ describe('TokensService', () => { standard: 'ERC20', type: 'fungible', symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -411,7 +413,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(true); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -420,6 +422,7 @@ describe('TokensService', () => { standard: 'ERC20', type: 'fungible', symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -444,6 +447,7 @@ describe('TokensService', () => { standard: 'ERC20', type: TokenType.FUNGIBLE, symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -451,7 +455,7 @@ describe('TokensService', () => { }, }; - mockNameAndSymbolQuery(); + mockPoolQuery(true); eventstream.createOrUpdateStream = jest.fn(() => mockEventStream); eventstream.getOrCreateSubscription = jest.fn(() => new FakeObservable(undefined)); @@ -576,7 +580,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(false); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -585,6 +589,7 @@ describe('TokensService', () => { standard: 'ERC721', type: 'nonfungible', symbol: SYMBOL, + decimals: 0, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -609,6 +614,7 @@ describe('TokensService', () => { standard: 'ERC721', type: TokenType.NONFUNGIBLE, symbol: SYMBOL, + decimals: 0, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -616,7 +622,7 @@ describe('TokensService', () => { }, }; - mockNameAndSymbolQuery(); + mockPoolQuery(false); eventstream.createOrUpdateStream = jest.fn(() => mockEventStream); eventstream.getOrCreateSubscription = jest.fn(() => new FakeObservable(undefined)); @@ -758,7 +764,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(false); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -767,6 +773,7 @@ describe('TokensService', () => { standard: 'ERC721', type: 'nonfungible', symbol: SYMBOL, + decimals: 0, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -787,7 +794,7 @@ describe('TokensService', () => { symbol: SYMBOL, }; - mockNameAndSymbolQuery(); + mockPoolQuery(false); await service.createPool(request).then(resp => { expect(resp).toEqual({ @@ -796,6 +803,7 @@ describe('TokensService', () => { standard: 'ERC721', type: 'nonfungible', symbol: SYMBOL, + decimals: 0, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -820,6 +828,7 @@ describe('TokensService', () => { standard: 'ERC721', type: TokenType.NONFUNGIBLE, symbol: SYMBOL, + decimals: 0, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -827,7 +836,7 @@ describe('TokensService', () => { }, }; - mockNameAndSymbolQuery(); + mockPoolQuery(false); eventstream.createOrUpdateStream = jest.fn(() => mockEventStream); eventstream.getOrCreateSubscription = jest.fn(() => new FakeObservable(undefined)); diff --git a/src/tokens/tokens.service.ts b/src/tokens/tokens.service.ts index be6dd7e..5dc2443 100644 --- a/src/tokens/tokens.service.ts +++ b/src/tokens/tokens.service.ts @@ -77,6 +77,7 @@ export interface AbiMethods { SYMBOL: string; APPROVE: string; APPROVEFORALL: string | null; + DECIMALS: string | null; } export interface AbiEvents { @@ -94,6 +95,7 @@ abiMethodMap.set('ERC20NoData', { APPROVEFORALL: null, NAME: 'name', SYMBOL: 'symbol', + DECIMALS: 'decimals', }); abiMethodMap.set('ERC20WithData', { MINT: 'mintWithData', @@ -103,6 +105,7 @@ abiMethodMap.set('ERC20WithData', { APPROVEFORALL: null, NAME: 'name', SYMBOL: 'symbol', + DECIMALS: 'decimals', }); abiMethodMap.set('ERC721WithData', { MINT: 'mintWithData', @@ -112,6 +115,7 @@ abiMethodMap.set('ERC721WithData', { APPROVEFORALL: 'setApprovalForAllWithData', NAME: 'name', SYMBOL: 'symbol', + DECIMALS: null, }); abiMethodMap.set('ERC721NoData', { MINT: 'mint', @@ -121,6 +125,7 @@ abiMethodMap.set('ERC721NoData', { APPROVEFORALL: 'setApprovalForAll', NAME: 'name', SYMBOL: 'symbol', + DECIMALS: null, }); const abiEventMap = new Map(); @@ -197,7 +202,11 @@ export class TokensService { if (contractAbi === undefined || abiMethods === undefined) { return undefined; } - return contractAbi.find(abi => abi.name === abiMethods[operation]); + const name = abiMethods[operation] ?? undefined; + if (name === undefined) { + return undefined; + } + return contractAbi.find(abi => abi.name === name); } private getEventAbi( @@ -350,6 +359,7 @@ export class TokensService { private async queryPool(poolLocator: IValidPoolLocator) { const schema = poolLocator.schema as ContractSchemaStrings; + const nameResponse = await lastValueFrom( this.http.post( `${this.baseUrl}`, @@ -364,6 +374,7 @@ export class TokensService { this.queryOptions(), ), ); + const symbolResponse = await lastValueFrom( this.http.post( `${this.baseUrl}`, @@ -378,9 +389,34 @@ export class TokensService { this.queryOptions(), ), ); + + let decimals = 0; + const decimalsMethod = this.getMethodAbi(schema, 'DECIMALS'); + if (decimalsMethod !== undefined) { + const decimalsResponse = await lastValueFrom( + this.http.post( + `${this.baseUrl}`, + { + headers: { + type: queryHeader, + }, + to: poolLocator.address, + method: decimalsMethod, + params: [], + } as EthConnectMsgRequest, + this.queryOptions(), + ), + ); + decimals = parseInt(decimalsResponse.data.output); + if (isNaN(decimals)) { + decimals = 0; + } + } + return { name: nameResponse.data.output, symbol: symbolResponse.data.output, + decimals, }; } @@ -394,10 +430,11 @@ export class TokensService { if (!this.validatePoolLocator(poolLocator)) { throw new BadRequestException('Invalid pool locator'); } - const nameAndSymbol = await this.queryPool(poolLocator); - if (dto.symbol !== undefined && dto.symbol !== '' && dto.symbol !== nameAndSymbol.symbol) { + + const poolInfo = await this.queryPool(poolLocator); + if (dto.symbol !== undefined && dto.symbol !== '' && dto.symbol !== poolInfo.symbol) { throw new BadRequestException( - `Supplied symbol '${dto.symbol}' does not match expected '${nameAndSymbol.symbol}'`, + `Supplied symbol '${dto.symbol}' does not match expected '${poolInfo.symbol}'`, ); } @@ -406,9 +443,10 @@ export class TokensService { poolLocator: packPoolLocator(poolLocator), standard: dto.type === TokenType.FUNGIBLE ? 'ERC20' : 'ERC721', type: dto.type, - symbol: nameAndSymbol.symbol, + symbol: poolInfo.symbol, + decimals: poolInfo.decimals, info: { - name: nameAndSymbol.name, + name: poolInfo.name, address: dto.config.address, schema, }, @@ -449,7 +487,9 @@ export class TokensService { throw new BadRequestException(`Unknown schema: ${poolLocator.schema}`); } - const possibleMethods: string[] = Object.values(abiMethods); + const possibleMethods: string[] = Object.values(abiMethods).filter( + m => !['name', 'symbol', 'decimals'].includes(m), + ); const methodsToSubTo: IAbiMethod[] = contractAbi.filter( method => method.name !== undefined && possibleMethods.includes(method.name), ); @@ -496,14 +536,15 @@ export class TokensService { } await Promise.all(promises); - const nameAndSymbol = await this.queryPool(poolLocator); + const poolInfo = await this.queryPool(poolLocator); const tokenPoolEvent: TokenPoolEvent = { poolLocator: dto.poolLocator, standard: poolLocator.type === TokenType.FUNGIBLE ? 'ERC20' : 'ERC721', type: poolLocator.type, - symbol: nameAndSymbol.symbol, + symbol: poolInfo.symbol, + decimals: poolInfo.decimals, info: { - name: nameAndSymbol.name, + name: poolInfo.name, address: poolLocator.address, schema: poolLocator.schema, }, diff --git a/test/suites/erc20.ts b/test/suites/erc20.ts index 8f440d1..bd551b6 100644 --- a/test/suites/erc20.ts +++ b/test/suites/erc20.ts @@ -66,7 +66,7 @@ const abiMethodMap = { }; export default (context: TestContext) => { - const mockNameAndSymbolQuery = () => { + const mockPoolQuery = () => { context.http.post .mockReturnValueOnce( new FakeObservable({ @@ -77,6 +77,11 @@ export default (context: TestContext) => { new FakeObservable({ output: SYMBOL, }), + ) + .mockReturnValueOnce( + new FakeObservable({ + output: '18', + }), ); }; @@ -99,6 +104,7 @@ export default (context: TestContext) => { standard: 'ERC20', type: TokenType.FUNGIBLE, symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -106,7 +112,7 @@ export default (context: TestContext) => { }, }); - mockNameAndSymbolQuery(); + mockPoolQuery(); context.http.get = jest.fn(() => new FakeObservable(expectedResponse)); const response = await context.server.post('/createpool').send(request).expect(200); @@ -151,6 +157,7 @@ export default (context: TestContext) => { standard: 'ERC20', type: TokenType.FUNGIBLE, symbol: SYMBOL, + decimals: 18, info: { name: NAME, address: CONTRACT_ADDRESS, @@ -158,7 +165,7 @@ export default (context: TestContext) => { }, }); - mockNameAndSymbolQuery(); + mockPoolQuery(); context.http.get = jest.fn(() => new FakeObservable(expectedResponse)); const response = await context.server.post('/createpool').send(request).expect(200); @@ -189,7 +196,7 @@ export default (context: TestContext) => { }, }); - mockNameAndSymbolQuery(); + mockPoolQuery(); context.http.get = jest.fn(() => new FakeObservable(expectedResponse)); const response = await context.server.post('/createpool').send(request).expect(200); @@ -353,7 +360,7 @@ export default (context: TestContext) => { }, }); - mockNameAndSymbolQuery(); + mockPoolQuery(); context.http.get = jest.fn(() => new FakeObservable(expectedResponse)); const response = await context.server.post('/createpool').send(request).expect(200); @@ -404,7 +411,7 @@ export default (context: TestContext) => { }, }); - mockNameAndSymbolQuery(); + mockPoolQuery(); context.http.get = jest.fn(() => new FakeObservable(expectedResponse)); const response = await context.server.post('/createpool').send(request).expect(200);