Skip to content

Commit

Permalink
Add /checkinterface API
Browse files Browse the repository at this point in the history
This can be queried with a full ABI to determine which methods are
understood by this token connector.

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
  • Loading branch information
awrichar committed Jan 5, 2023
1 parent 7589280 commit f492094
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 4 deletions.
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
11 changes: 11 additions & 0 deletions src/tokens/tokens.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EventStreamReply } from '../event-stream/event-stream.interfaces';
import { BlockchainConnectorService } from './blockchain.service';
import {
AsyncResponse,
CheckInterfaceRequest,
TokenApproval,
TokenBurn,
TokenMint,
Expand Down Expand Up @@ -76,6 +77,16 @@ export class TokensController {
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 Down
34 changes: 31 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 @@ -226,9 +226,37 @@ export class TokenPoolActivate {
}

export class TokenInterface {
@ApiProperty({ enum: InterfaceFormat })
@Equals(InterfaceFormat.ABI)
format: InterfaceFormat;

@ApiProperty({ isArray: true })
@IsOptional()
abi?: IAbiMethod[];
@IsDefined()
abi: 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
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
25 changes: 25 additions & 0 deletions src/tokens/tokens.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +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 @@ -49,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 @@ -379,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, abi: methods };
};

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

private async getAbiForMint(ctx: Context, poolLocator: IValidPoolLocator, dto: TokenMint) {
const supportsUri =
dto.uri !== undefined && (await this.mapper.supportsMintWithUri(ctx, poolLocator.address));
Expand Down
41 changes: 41 additions & 0 deletions test/suites/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
import ERC20WithDataABI from '../../src/abi/ERC20WithData.json';
import ERC20NoDataABI from '../../src/abi/ERC20NoData.json';
import {
CheckInterfaceRequest,
CheckInterfaceResponse,
EthConnectAsyncResponse,
EthConnectMsgRequest,
EthConnectReturn,
IAbiMethod,
InterfaceFormat,
TokenApproval,
TokenBurn,
TokenMint,
Expand Down Expand Up @@ -50,7 +53,9 @@ const ERC20_WITH_DATA_SCHEMA = 'ERC20WithData';
const ERC20_WITH_DATA_POOL_ID = `address=${CONTRACT_ADDRESS}&schema=${ERC20_WITH_DATA_SCHEMA}&type=${TokenType.FUNGIBLE}`;

const MINT_NO_DATA = 'mint';
const TRANSFER_FROM_NO_DATA = 'transferFrom';
const TRANSFER_NO_DATA = 'transfer';
const BURN_FROM_NO_DATA = 'burnFrom';
const BURN_NO_DATA = 'burn';
const APPROVE_NO_DATA = 'approve';
const MINT_WITH_DATA = 'mintWithData';
Expand Down Expand Up @@ -559,6 +564,7 @@ export default (context: TestContext) => {
poolLocator: ERC20_NO_DATA_POOL_ID,
from: IDENTITY,
interface: {
format: InterfaceFormat.ABI,
abi: burnMethods,
},
};
Expand Down Expand Up @@ -627,6 +633,7 @@ export default (context: TestContext) => {
poolLocator: ERC20_NO_DATA_POOL_ID,
from: '0x2',
interface: {
format: InterfaceFormat.ABI,
abi: burnMethods,
},
};
Expand All @@ -653,5 +660,39 @@ export default (context: TestContext) => {
expect(context.http.post).toHaveBeenCalledTimes(1);
expect(context.http.post).toHaveBeenCalledWith(BASE_URL, mockEthConnectRequest, OPTIONS);
});

it('Check interface', async () => {
const request: CheckInterfaceRequest = {
poolLocator: ERC20_NO_DATA_POOL_ID,
format: InterfaceFormat.ABI,
abi: ERC20NoDataABI.abi,
};

const response: CheckInterfaceResponse = {
approval: {
format: InterfaceFormat.ABI,
abi: ERC20NoDataABI.abi.filter(m => m.name === APPROVE_NO_DATA),
},
burn: {
format: InterfaceFormat.ABI,
abi: ERC20NoDataABI.abi.filter(
m => m.name === BURN_NO_DATA || m.name === BURN_FROM_NO_DATA,
),
},
mint: {
format: InterfaceFormat.ABI,
abi: ERC20NoDataABI.abi.filter(m => m.name === MINT_NO_DATA),
},
transfer: {
format: InterfaceFormat.ABI,
abi: [
...ERC20NoDataABI.abi.filter(m => m.name === TRANSFER_NO_DATA),
...ERC20NoDataABI.abi.filter(m => m.name === TRANSFER_FROM_NO_DATA),
],
},
};

await context.server.post('/checkinterface').send(request).expect(200).expect(response);
});
});
};
43 changes: 43 additions & 0 deletions test/suites/erc721.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
import ERC721NoDataABI from '../../src/abi/ERC721NoData.json';
import ERC721WithDataABI from '../../src/abi/ERC721WithData.json';
import {
CheckInterfaceRequest,
CheckInterfaceResponse,
EthConnectAsyncResponse,
EthConnectMsgRequest,
EthConnectReturn,
IAbiMethod,
InterfaceFormat,
TokenApproval,
TokenBurn,
TokenMint,
Expand Down Expand Up @@ -540,6 +543,7 @@ export default (context: TestContext) => {
poolLocator: ERC721_NO_DATA_POOL_ID,
to: '0x123',
interface: {
format: InterfaceFormat.ABI,
abi: [safeMintAutoIndex],
},
};
Expand All @@ -566,5 +570,44 @@ export default (context: TestContext) => {
expect(context.http.post).toHaveBeenCalledTimes(1);
expect(context.http.post).toHaveBeenCalledWith(BASE_URL, mockEthConnectRequest, OPTIONS);
});

it('Check interface', async () => {
const request: CheckInterfaceRequest = {
poolLocator: ERC721_NO_DATA_POOL_ID,
format: InterfaceFormat.ABI,
abi: ERC721NoDataABI.abi,
};

const response: CheckInterfaceResponse = {
approval: {
format: InterfaceFormat.ABI,
abi: [
...ERC721NoDataABI.abi.filter(m => m.name === APPROVE_NO_DATA),
...ERC721NoDataABI.abi.filter(m => m.name === APPROVE_FOR_ALL_NO_DATA),
],
},
burn: {
format: InterfaceFormat.ABI,
abi: ERC721NoDataABI.abi.filter(m => m.name === BURN_NO_DATA),
},
mint: {
format: InterfaceFormat.ABI,
abi: ERC721NoDataABI.abi.filter(m => m.name === MINT_NO_DATA),
},
transfer: {
format: InterfaceFormat.ABI,
abi: [
...ERC721NoDataABI.abi.filter(
m => m.name === TRANSFER_NO_DATA && m.inputs.length === 4,
),
...ERC721NoDataABI.abi.filter(
m => m.name === TRANSFER_NO_DATA && m.inputs.length === 3,
),
],
},
};

await context.server.post('/checkinterface').send(request).expect(200).expect(response);
});
});
};

0 comments on commit f492094

Please sign in to comment.