diff --git a/src/tokens/abimapper.service.ts b/src/tokens/abimapper.service.ts index dea549d..7321411 100644 --- a/src/tokens/abimapper.service.ts +++ b/src/tokens/abimapper.service.ts @@ -77,7 +77,7 @@ export class AbiMapperService { return true; } - private getAllMethods(abi: IAbiMethod[], signatures: MethodSignature[]) { + getAllMethods(abi: IAbiMethod[], signatures: MethodSignature[]) { const methods: IAbiMethod[] = []; for (const signature of signatures) { for (const method of abi) { diff --git a/src/tokens/tokens.controller.ts b/src/tokens/tokens.controller.ts index a06cc29..02b55d6 100644 --- a/src/tokens/tokens.controller.ts +++ b/src/tokens/tokens.controller.ts @@ -20,6 +20,7 @@ import { EventStreamReply } from '../event-stream/event-stream.interfaces'; import { BlockchainConnectorService } from './blockchain.service'; import { AsyncResponse, + CheckInterfaceRequest, TokenApproval, TokenBalance, TokenBalanceQuery, @@ -79,6 +80,16 @@ export class TokensController { return this.service.mint(dto); } + @Post('checkinterface') + @HttpCode(200) + @ApiOperation({ + summary: 'Check which interface methods are supported by this connector', + }) + @ApiBody({ type: CheckInterfaceRequest }) + checkInterface(@Body() dto: CheckInterfaceRequest) { + return this.service.checkInterface(dto); + } + @Post('approval') @HttpCode(202) @ApiOperation({ diff --git a/src/tokens/tokens.interfaces.ts b/src/tokens/tokens.interfaces.ts index 4f36dbf..9701e4b 100644 --- a/src/tokens/tokens.interfaces.ts +++ b/src/tokens/tokens.interfaces.ts @@ -15,7 +15,7 @@ // limitations under the License. import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { IsDefined, IsNotEmpty, IsOptional } from 'class-validator'; +import { Equals, IsDefined, IsNotEmpty, IsOptional } from 'class-validator'; import { Event } from '../event-stream/event-stream.interfaces'; // Internal types @@ -89,6 +89,11 @@ export enum TokenType { NONFUNGIBLE = 'nonfungible', } +export enum InterfaceFormat { + ABI = 'abi', + FFI = 'ffi', +} + const requestIdDescription = 'Optional ID to identify this request. Must be unique for every request. ' + 'If none is provided, one will be assigned and returned in the 202 response.'; @@ -213,9 +218,37 @@ export class TokenBalance { } export class TokenInterface { + @ApiProperty({ enum: InterfaceFormat }) + @Equals(InterfaceFormat.ABI) + format: InterfaceFormat; + @ApiProperty({ isArray: true }) - @IsOptional() - abi?: IAbiMethod[]; + @IsDefined() + methods: IAbiMethod[]; +} + +export class CheckInterfaceRequest extends TokenInterface { + @ApiProperty() + @IsNotEmpty() + poolLocator: string; +} + +type TokenAbi = { + [op in TokenOperation]: TokenInterface; +}; + +export class CheckInterfaceResponse implements TokenAbi { + @ApiProperty() + approval: TokenInterface; + + @ApiProperty() + burn: TokenInterface; + + @ApiProperty() + mint: TokenInterface; + + @ApiProperty() + transfer: TokenInterface; } export class TokenTransfer { @@ -333,6 +366,9 @@ export class TokenPoolEvent extends tokenEventBase { @ApiProperty() standard: string; + @ApiProperty() + interfaceFormat: InterfaceFormat; + @ApiProperty() @IsOptional() symbol?: string; diff --git a/src/tokens/tokens.listener.ts b/src/tokens/tokens.listener.ts index 993e466..c00b84a 100644 --- a/src/tokens/tokens.listener.ts +++ b/src/tokens/tokens.listener.ts @@ -30,6 +30,7 @@ import { TransferBatchEvent, TransferSingleEvent, TokenPoolEventInfo, + InterfaceFormat, } from './tokens.interfaces'; import { decodeHex, @@ -142,6 +143,7 @@ export class TokenListener implements EventListener { event: 'token-pool', data: { standard: TOKEN_STANDARD, + interfaceFormat: InterfaceFormat.ABI, poolLocator: packedPoolLocator, type: unpackedId.isFungible ? TokenType.FUNGIBLE : TokenType.NONFUNGIBLE, signer: output.operator, diff --git a/src/tokens/tokens.service.ts b/src/tokens/tokens.service.ts index 06c67bd..10cada5 100644 --- a/src/tokens/tokens.service.ts +++ b/src/tokens/tokens.service.ts @@ -20,10 +20,15 @@ import { EventStream, EventStreamSubscription } from '../event-stream/event-stre import { EventStreamProxyGateway } from '../eventstream-proxy/eventstream-proxy.gateway'; import { AsyncResponse, + CheckInterfaceRequest, + CheckInterfaceResponse, + IAbiMethod, + InterfaceFormat, TokenApproval, TokenBalance, TokenBalanceQuery, TokenBurn, + TokenInterface, TokenMint, TokenPool, TokenPoolActivate, @@ -39,7 +44,14 @@ import { import { TokenListener } from './tokens.listener'; import { BlockchainConnectorService } from './blockchain.service'; import { AbiMapperService } from './abimapper.service'; -import { AllEvents, ApprovalForAll, BalanceOf, TransferBatch, TransferSingle } from './erc1155'; +import { + AllEvents, + ApprovalForAll, + BalanceOf, + DynamicMethods, + TransferBatch, + TransferSingle, +} from './erc1155'; export const BASE_SUBSCRIPTION_NAME = 'base'; @@ -306,6 +318,19 @@ export class TokensService { await Promise.all(promises); } + checkInterface(dto: CheckInterfaceRequest): CheckInterfaceResponse { + const wrapMethods = (methods: IAbiMethod[]): TokenInterface => { + return { format: InterfaceFormat.ABI, methods }; + }; + + return { + approval: wrapMethods(this.mapper.getAllMethods(dto.methods, DynamicMethods.approval)), + burn: wrapMethods(this.mapper.getAllMethods(dto.methods, DynamicMethods.burn)), + mint: wrapMethods(this.mapper.getAllMethods(dto.methods, DynamicMethods.mint)), + transfer: wrapMethods(this.mapper.getAllMethods(dto.methods, DynamicMethods.transfer)), + }; + } + private async getAbiForMint(address: string, dto: TokenMint) { const supportsUri = dto.uri !== undefined && (await this.mapper.supportsMintWithUri(address)); return this.mapper.getAbi(supportsUri); @@ -314,7 +339,7 @@ export class TokensService { async mint(dto: TokenMint): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); const address = poolLocator.address ?? (await this.getContractAddress()); - const abi = dto.interface?.abi || (await this.getAbiForMint(address, dto)); + const abi = dto.interface?.methods || (await this.getAbiForMint(address, dto)); const { method, params } = this.mapper.getMethodAndParams(abi, poolLocator, 'mint', dto); const response = await this.blockchain.sendTransaction( dto.signer, @@ -329,7 +354,7 @@ export class TokensService { async transfer(dto: TokenTransfer): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); const address = poolLocator.address ?? (await this.getContractAddress()); - const abi = dto.interface?.abi || this.mapper.getAbi(); + const abi = dto.interface?.methods || this.mapper.getAbi(); const { method, params } = this.mapper.getMethodAndParams(abi, poolLocator, 'transfer', dto); const response = await this.blockchain.sendTransaction( dto.signer, @@ -344,7 +369,7 @@ export class TokensService { async burn(dto: TokenBurn): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); const address = poolLocator.address ?? (await this.getContractAddress()); - const abi = dto.interface?.abi || this.mapper.getAbi(); + const abi = dto.interface?.methods || this.mapper.getAbi(); const { method, params } = this.mapper.getMethodAndParams(abi, poolLocator, 'burn', dto); const response = await this.blockchain.sendTransaction( dto.signer, @@ -360,7 +385,7 @@ export class TokensService { async approval(dto: TokenApproval): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); const address = poolLocator.address ?? (await this.getContractAddress()); - const abi = dto.interface?.abi || this.mapper.getAbi(); + const abi = dto.interface?.methods || this.mapper.getAbi(); const { method, params } = this.mapper.getMethodAndParams(abi, poolLocator, 'approval', dto); const response = await this.blockchain.sendTransaction( dto.signer, diff --git a/test/suites/api.ts b/test/suites/api.ts index 285f543..6920a3d 100644 --- a/test/suites/api.ts +++ b/test/suites/api.ts @@ -9,6 +9,9 @@ import { EthConnectReturn, TokenBalance, TokenBalanceQuery, + CheckInterfaceRequest, + InterfaceFormat, + CheckInterfaceResponse, } from '../../src/tokens/tokens.interfaces'; import { TestContext, FakeObservable, BASE_URL, CONTRACT_ADDRESS } from '../app.e2e-context'; import { abi as ERC1155MixedFungibleAbi } from '../../src/abi/ERC1155MixedFungible.json'; @@ -357,4 +360,40 @@ export default (context: TestContext) => { {}, ); }); + + it('Check interface', async () => { + const request: CheckInterfaceRequest = { + poolLocator: 'F1', + format: InterfaceFormat.ABI, + methods: ERC1155MixedFungibleAbi, + }; + + const response: CheckInterfaceResponse = { + approval: { + format: InterfaceFormat.ABI, + methods: [ + ...ERC1155MixedFungibleAbi.filter(m => m.name === 'setApprovalForAllWithData'), + ...ERC1155MixedFungibleAbi.filter(m => m.name === 'setApprovalForAll'), + ], + }, + burn: { + format: InterfaceFormat.ABI, + methods: ERC1155MixedFungibleAbi.filter(m => m.name === 'burn'), + }, + mint: { + format: InterfaceFormat.ABI, + methods: [ + ...ERC1155MixedFungibleAbi.filter(m => m.name === 'mintFungible'), + ...ERC1155MixedFungibleAbi.filter(m => m.name === 'mintNonFungibleWithURI'), + ...ERC1155MixedFungibleAbi.filter(m => m.name === 'mintNonFungible'), + ], + }, + transfer: { + format: InterfaceFormat.ABI, + methods: ERC1155MixedFungibleAbi.filter(m => m.name === 'safeTransferFrom'), + }, + }; + + await context.server.post('/checkinterface').send(request).expect(200).expect(response); + }); }; diff --git a/test/suites/websocket.ts b/test/suites/websocket.ts index 7738dd0..85ea95b 100644 --- a/test/suites/websocket.ts +++ b/test/suites/websocket.ts @@ -81,6 +81,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', + interfaceFormat: 'abi', poolLocator: 'F1', type: 'fungible', signer: 'bob', @@ -150,6 +151,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', + interfaceFormat: 'abi', poolLocator: 'address=0x00001&id=F1&block=1', type: 'fungible', signer: 'bob', @@ -219,6 +221,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', + interfaceFormat: 'abi', poolLocator: 'address=0x00001&id=F1&block=1', type: 'fungible', signer: 'bob',