Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /checkinterface API #106

Merged
merged 4 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/tokens/abimapper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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) {
Expand Down
29 changes: 20 additions & 9 deletions src/tokens/tokens.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Res } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Response } from 'express';
import { RequestContext } from '../request-context/request-context.decorator';
import { Context, RequestContext } from '../request-context/request-context.decorator';
import { EventStreamReply } from '../event-stream/event-stream.interfaces';
import { BlockchainConnectorService } from './blockchain.service';
import {
AsyncResponse,
CheckInterfaceRequest,
TokenApproval,
TokenBurn,
TokenMint,
Expand All @@ -39,7 +40,7 @@ export class TokensController {
@Post('init')
@HttpCode(204)
@ApiOperation({ summary: 'Perform one-time initialization (if not auto-initialized)' })
init(@RequestContext() ctx) {
init(@RequestContext() ctx: Context) {
return this.service.init(ctx);
}

Expand All @@ -53,7 +54,7 @@ export class TokensController {
@ApiResponse({ status: 200, type: TokenPoolEvent })
@ApiResponse({ status: 202, type: AsyncResponse })
async createPool(
@RequestContext() ctx,
@RequestContext() ctx: Context,
@Body() dto: TokenPool,
@Res({ passthrough: true }) res: Response,
) {
Expand All @@ -72,10 +73,20 @@ export class TokensController {
summary: 'Activate a token pool to begin receiving transfer events',
})
@ApiBody({ type: TokenPoolActivate })
activatePool(@RequestContext() ctx, @Body() dto: TokenPoolActivate) {
activatePool(@RequestContext() ctx: Context, @Body() dto: TokenPoolActivate) {
return this.service.activatePool(ctx, 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('mint')
@HttpCode(202)
@ApiOperation({
Expand All @@ -85,7 +96,7 @@ export class TokensController {
})
@ApiBody({ type: TokenMint })
@ApiResponse({ status: 202, type: AsyncResponse })
mint(@RequestContext() ctx, @Body() dto: TokenMint) {
mint(@RequestContext() ctx: Context, @Body() dto: TokenMint) {
return this.service.mint(ctx, dto);
}

Expand All @@ -98,7 +109,7 @@ export class TokensController {
})
@ApiBody({ type: TokenTransfer })
@ApiResponse({ status: 202, type: AsyncResponse })
transfer(@RequestContext() ctx, @Body() dto: TokenTransfer) {
transfer(@RequestContext() ctx: Context, @Body() dto: TokenTransfer) {
return this.service.transfer(ctx, dto);
}

Expand All @@ -110,7 +121,7 @@ export class TokensController {
})
@ApiBody({ type: TokenApproval })
@ApiResponse({ status: 202, type: AsyncResponse })
approve(@RequestContext() ctx, @Body() dto: TokenApproval) {
approve(@RequestContext() ctx: Context, @Body() dto: TokenApproval) {
return this.service.approval(ctx, dto);
}

Expand All @@ -123,14 +134,14 @@ export class TokensController {
})
@ApiBody({ type: TokenBurn })
@ApiResponse({ status: 202, type: AsyncResponse })
burn(@RequestContext() ctx, @Body() dto: TokenBurn) {
burn(@RequestContext() ctx: Context, @Body() dto: TokenBurn) {
return this.service.burn(ctx, dto);
}

@Get('receipt/:id')
@ApiOperation({ summary: 'Retrieve the result of an async operation' })
@ApiResponse({ status: 200, type: EventStreamReply })
getReceipt(@RequestContext() ctx, @Param('id') id: string) {
getReceipt(@RequestContext() ctx: Context, @Param('id') id: string) {
return this.blockchain.getReceipt(ctx, id);
}
}
42 changes: 39 additions & 3 deletions src/tokens/tokens.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// limitations under the License.

import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { Equals, IsDefined, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { Event } from '../event-stream/event-stream.interfaces';

// Ethconnect interfaces
Expand Down Expand Up @@ -95,6 +95,11 @@ export enum TokenType {
NONFUNGIBLE = 'nonfungible',
}

export enum InterfaceFormat {
ABI = 'abi',
FFI = 'ffi',
}

export interface IPoolLocator {
address: string | null;
schema: string | null;
Expand Down Expand Up @@ -221,9 +226,37 @@ export class TokenPoolActivate {
}

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 {
Expand Down Expand Up @@ -353,6 +386,9 @@ export class TokenPoolEvent extends tokenEventBase {
@ApiProperty()
standard: string;

@ApiProperty()
interfaceFormat: InterfaceFormat;

@ApiProperty()
symbol: string;

Expand Down
2 changes: 2 additions & 0 deletions src/tokens/tokens.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
TokenTransferEvent,
TokenType,
TransferEvent,
InterfaceFormat,
} from './tokens.interfaces';
import {
decodeHex,
Expand Down Expand Up @@ -150,6 +151,7 @@ export class TokenListener implements EventListener {
event: 'token-pool',
data: <TokenPoolEvent>{
standard: type === TokenType.FUNGIBLE ? 'ERC20' : 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
poolLocator: packPoolLocator(poolLocator),
type,
signer: event.inputSigner,
Expand Down
11 changes: 11 additions & 0 deletions src/tokens/tokens.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
EthConnectMsgRequest,
EthConnectReturn,
IAbiMethod,
InterfaceFormat,
TokenBurn,
TokenMint,
TokenPool,
Expand Down Expand Up @@ -267,6 +268,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC20_NO_DATA_POOL_ID,
standard: 'ERC20',
interfaceFormat: InterfaceFormat.ABI,
type: 'fungible',
symbol: SYMBOL,
decimals: 18,
Expand Down Expand Up @@ -295,6 +297,7 @@ describe('TokensService', () => {
const response: TokenPoolEvent = {
poolLocator: ERC20_NO_DATA_POOL_ID,
standard: 'ERC20',
interfaceFormat: InterfaceFormat.ABI,
type: TokenType.FUNGIBLE,
symbol: SYMBOL,
decimals: 18,
Expand Down Expand Up @@ -458,6 +461,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC20_WITH_DATA_POOL_ID,
standard: 'ERC20',
interfaceFormat: InterfaceFormat.ABI,
type: 'fungible',
symbol: SYMBOL,
decimals: 18,
Expand Down Expand Up @@ -490,6 +494,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC20_WITH_DATA_POOL_ID,
standard: 'ERC20',
interfaceFormat: InterfaceFormat.ABI,
type: 'fungible',
symbol: SYMBOL,
decimals: 18,
Expand Down Expand Up @@ -518,6 +523,7 @@ describe('TokensService', () => {
const response: TokenPoolEvent = {
poolLocator: ERC20_WITH_DATA_POOL_ID,
standard: 'ERC20',
interfaceFormat: InterfaceFormat.ABI,
type: TokenType.FUNGIBLE,
symbol: SYMBOL,
decimals: 18,
Expand Down Expand Up @@ -680,6 +686,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC721_NO_DATA_POOL_ID,
standard: 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: 'nonfungible',
symbol: SYMBOL,
decimals: 0,
Expand Down Expand Up @@ -708,6 +715,7 @@ describe('TokensService', () => {
const response: TokenPoolEvent = {
poolLocator: ERC721_NO_DATA_POOL_ID,
standard: 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: TokenType.NONFUNGIBLE,
symbol: SYMBOL,
decimals: 0,
Expand Down Expand Up @@ -888,6 +896,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC721_WITH_DATA_POOL_ID,
standard: 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: 'nonfungible',
symbol: SYMBOL,
decimals: 0,
Expand Down Expand Up @@ -921,6 +930,7 @@ describe('TokensService', () => {
data: `{"tx":${TX}}`,
poolLocator: ERC721_WITH_DATA_POOL_ID,
standard: 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: 'nonfungible',
symbol: SYMBOL,
decimals: 0,
Expand Down Expand Up @@ -949,6 +959,7 @@ describe('TokensService', () => {
const response: TokenPoolEvent = {
poolLocator: ERC721_WITH_DATA_POOL_ID,
standard: 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: TokenType.NONFUNGIBLE,
symbol: SYMBOL,
decimals: 0,
Expand Down
36 changes: 32 additions & 4 deletions src/tokens/tokens.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ import { EventStreamProxyGateway } from '../eventstream-proxy/eventstream-proxy.
import { Context, newContext } from '../request-context/request-context.decorator';
import {
AsyncResponse,
CheckInterfaceRequest,
CheckInterfaceResponse,
IAbiMethod,
InterfaceFormat,
IPoolLocator,
IValidPoolLocator,
TokenApproval,
TokenBurn,
TokenInterface,
TokenMint,
TokenPool,
TokenPoolActivate,
Expand All @@ -48,13 +53,15 @@ import {
Transfer as ERC20Transfer,
Name as ERC20Name,
Symbol as ERC20Symbol,
DynamicMethods as ERC20Methods,
} from './erc20';
import {
Approval as ERC721Approval,
ApprovalForAll as ERC721ApprovalForAll,
Transfer as ERC721Transfer,
Name as ERC721Name,
Symbol as ERC721Symbol,
DynamicMethods as ERC721Methods,
} from './erc721';

@Injectable()
Expand Down Expand Up @@ -263,6 +270,7 @@ export class TokensService {
data: dto.data,
poolLocator: packPoolLocator(poolLocator),
standard: dto.type === TokenType.FUNGIBLE ? 'ERC20' : 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: dto.type,
symbol: poolInfo.symbol,
decimals: poolInfo.decimals,
Expand Down Expand Up @@ -363,6 +371,7 @@ export class TokensService {
const tokenPoolEvent: TokenPoolEvent = {
poolLocator: dto.poolLocator,
standard: poolLocator.type === TokenType.FUNGIBLE ? 'ERC20' : 'ERC721',
interfaceFormat: InterfaceFormat.ABI,
type: poolLocator.type,
symbol: poolInfo.symbol,
decimals: poolInfo.decimals,
Expand All @@ -376,6 +385,25 @@ export class TokensService {
return tokenPoolEvent;
}

checkInterface(dto: CheckInterfaceRequest): CheckInterfaceResponse {
const poolLocator = unpackPoolLocator(dto.poolLocator);
if (!validatePoolLocator(poolLocator)) {
throw new BadRequestException('Invalid pool locator');
}

const wrapMethods = (methods: IAbiMethod[]): TokenInterface => {
return { format: InterfaceFormat.ABI, methods };
};

const methods = poolLocator.type === TokenType.FUNGIBLE ? ERC20Methods : ERC721Methods;
return {
approval: wrapMethods(this.mapper.getAllMethods(dto.methods, methods.approval)),
burn: wrapMethods(this.mapper.getAllMethods(dto.methods, methods.burn)),
mint: wrapMethods(this.mapper.getAllMethods(dto.methods, methods.mint)),
transfer: wrapMethods(this.mapper.getAllMethods(dto.methods, methods.transfer)),
};
}

private async getAbiForMint(ctx: Context, poolLocator: IValidPoolLocator, dto: TokenMint) {
const supportsUri =
dto.uri !== undefined && (await this.mapper.supportsMintWithUri(ctx, poolLocator.address));
Expand All @@ -388,7 +416,7 @@ export class TokensService {
throw new BadRequestException('Invalid pool locator');
}

const abi = dto.interface?.abi || (await this.getAbiForMint(ctx, poolLocator, dto));
const abi = dto.interface?.methods || (await this.getAbiForMint(ctx, poolLocator, dto));
const { method, params } = this.mapper.getMethodAndParams(
abi,
poolLocator.type === TokenType.FUNGIBLE,
Expand All @@ -412,7 +440,7 @@ export class TokensService {
throw new BadRequestException('Invalid pool locator');
}

const abi = dto.interface?.abi || this.mapper.getAbi(poolLocator.schema);
const abi = dto.interface?.methods || this.mapper.getAbi(poolLocator.schema);
const { method, params } = this.mapper.getMethodAndParams(
abi,
poolLocator.type === TokenType.FUNGIBLE,
Expand All @@ -436,7 +464,7 @@ export class TokensService {
throw new BadRequestException('Invalid pool locator');
}

const abi = dto.interface?.abi || this.mapper.getAbi(poolLocator.schema);
const abi = dto.interface?.methods || this.mapper.getAbi(poolLocator.schema);
const { method, params } = this.mapper.getMethodAndParams(
abi,
poolLocator.type === TokenType.FUNGIBLE,
Expand All @@ -460,7 +488,7 @@ export class TokensService {
throw new BadRequestException('Invalid pool locator');
}

const abi = dto.interface?.abi || this.mapper.getAbi(poolLocator.schema);
const abi = dto.interface?.methods || this.mapper.getAbi(poolLocator.schema);
const { method, params } = this.mapper.getMethodAndParams(
abi,
poolLocator.type === TokenType.FUNGIBLE,
Expand Down
Loading