Skip to content

Commit

Permalink
feat: add support for ink! contracts (#1015)
Browse files Browse the repository at this point in the history
* feat: contracts endpoint boilerplate

* add @polkadot/api-contracts

* set this as a post request

* modify post request handler type

* cleanup service

* add contracts controller to the default controller

* boilerplate tests

* set mock data for contracts

* expand request types to include generic P type

* cleanup controlelr with expanded types and clean naming. Add query params

* add query params

* comment out some tests

* expand query params, and fix logic

* fix small grumble

* add validateAddress middleware

* pass the ContractPromise into the service to make testing possible

* add test for contracts ink

* fix versions

* cleanup contract query calls

* fix inline comments, and cleanup code

* remove parseBNorThrow (only needed for contrcuting contract txs)

* docs

* update 400 errors

Co-authored-by: marshacb <cameron.marshall12@gmail.com>
  • Loading branch information
TarikGul and marshacb committed Nov 22, 2022
1 parent 71456c4 commit f6499fa
Show file tree
Hide file tree
Showing 15 changed files with 505 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/dist/app.bundle.js

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions docs/src/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ info:
tags:
- name: accounts
- name: blocks
- name: contracts
- name: node
description: node connected to sidecar
- name: pallets
Expand Down Expand Up @@ -669,6 +670,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/contracts/ink/{address}/query:
post:
tags:
- contracts
summary: Query an !Ink contract with a given message (method).
description: Will return a valid or invalid result.
operationId: callContractQuery
requestBody:
$ref: '#/components/requestBodies/ContractMetadata'
parameters:
- name: address
in: path
description: SS58 or Hex address of the account associated with the contract.
required: true
schema:
pattern: a-km-zA-HJ-NP-Z1-9{8,64}
type: string
- name: method
in: query
description: The message or method used to query.
required: false
schema:
type: string
default: 'get'
- name: gasLimit
in: query
description: The gas limit to be used as an option for the queried message.
required: false
schema:
default: -1
type: number
- name: storageDepositLimit
in: query
description: The storage deposit limit to be used as an option for the queried message.
required: false
schema:
deafult: null
type: number
- name: args
in: query
description: Abi params used as args specified in the metadata to be passed into a query.
The format to use this query param is ?args[]=1&args[]=2&args[]=3.
required: false
responses:
"200":
description: succesful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ContractsInkQuery'
"400":
description: Invalid Method
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/node/network:
get:
tags:
Expand Down Expand Up @@ -1888,6 +1945,27 @@ components:
custom:
type: string
example: "{\"live\": null}"
ContractsInkQuery:
type: object
description: Result from calling a query to a Ink contract.
properties:
debugMessage:
type: string
gasConsumed:
type: string
gasRequired:
type: string
output:
type: boolean
result:
type: object
description: Will result in an Ok or Err object depending on the result of the query.
storageDeposit:
type: object
ContractMetata:
type: object
description: Metadata used to instantiate a ContractPromise. This metadata can be generated
by compiling the contract you are querying.
DigestItem:
type: object
properties:
Expand Down Expand Up @@ -3008,3 +3086,9 @@ components:
schema:
$ref: '#/components/schemas/Transaction'
required: true
ContractMetadata:
content:
application/json:
scehma:
$ref: '#/components/schemas/ContractMetadata'

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"dependencies": {
"@polkadot/api": "^9.9.1",
"@polkadot/api-contract": "^9.9.1",
"@polkadot/util-crypto": "^10.1.13",
"@substrate/calc": "^0.3.0",
"argparse": "^2.0.1",
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/defaultControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const defaultControllers: ControllerConfig = {
'AccountsVestingInfo',
'Blocks',
'BlocksExtrinsics',
'ContractsInk',
'NodeNetwork',
'NodeTransactionPool',
'NodeVersion',
Expand Down
20 changes: 20 additions & 0 deletions src/controllers/AbstractController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ export default abstract class AbstractController<T extends AbstractService> {
}
}

/**
* Safely mount async POST routes by wrapping them with an express
* handler friendly try / catch block and then mounting on the controllers
* router.
*
* @param pathsAndHandlers array of tuples containing the suffix to the controller
* base path (use empty string if no suffix) and the get request handler function.
*/
protected safeMountAsyncPostHandlers(
pathsAndHandlers: [string, SidecarRequestHandler][]
): void {
for (const pathAndHandler of pathsAndHandlers) {
const [pathSuffix, handler] = pathAndHandler;
this.router.post(
`${this.path}${pathSuffix}`,
AbstractController.catchWrap(handler as RequestHandler)
);
}
}

/**
* Wrapper for any asynchronous RequestHandler function. Pipes errors
* to downstream error handling middleware.
Expand Down
89 changes: 89 additions & 0 deletions src/controllers/contracts/ContractsInkController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ApiPromise } from '@polkadot/api';
import { ContractPromise } from '@polkadot/api-contract';
import { RequestHandler } from 'express';
import { BadRequest } from 'http-errors';

import { validateAddress } from '../../middleware';
import { ContractsInkService } from '../../services';
import {
IBodyContractMetadata,
IContractQueryParam,
IPostRequestHandler,
} from '../../types/requests';
import AbstractController from '../AbstractController';

export default class ContractsInkController extends AbstractController<ContractsInkService> {
constructor(api: ApiPromise) {
super(api, '/contracts/ink/:address', new ContractsInkService(api));
this.initRoutes();
}

protected initRoutes(): void {
this.router.use(this.path, validateAddress);
this.safeMountAsyncPostHandlers([
['/query', this.callContractQuery as RequestHandler],
]);
}

/**
* Send a message call to a contract. It defaults to get if nothing is inputted.
*
* @param _req
* @param res
*/
private callContractQuery: IPostRequestHandler<
IBodyContractMetadata,
IContractQueryParam
> = async (
{
params: { address },
body,
query: { method = 'get', gasLimit, storageDepositLimit, args },
},
res
): Promise<void> => {
const { api } = this;
const argsArray = Array.isArray(args) ? args : [];
const contract = new ContractPromise(api, body, address);
if (!contract.query[method]) {
throw new BadRequest(
`Invalid Method: Contract does not have the given ${method} message.`
);
}

const callMeta = contract.query[method].meta;
if (callMeta.isPayable || callMeta.isMutating) {
throw new BadRequest(
`Invalid Method: This endpoint does not handle mutating or payable calls.`
);
}

ContractsInkController.sanitizedSend(
res,
await this.service.fetchContractCall(
contract,
address,
method,
argsArray,
gasLimit,
storageDepositLimit
)
);
};
}
1 change: 1 addition & 0 deletions src/controllers/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ContractsInk } from './ContractsInkController';
2 changes: 2 additions & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AccountsVestingInfo,
} from './accounts';
import { Blocks, BlocksExtrinsics, BlocksTrace } from './blocks';
import { ContractsInk } from './contracts';
import { NodeNetwork, NodeTransactionPool, NodeVersion } from './node';
import {
PalletsAssets,
Expand Down Expand Up @@ -53,6 +54,7 @@ export const controllers = {
AccountsValidate,
AccountsVestingInfo,
AccountsStakingPayouts,
ContractsInk,
PalletsAssets,
PalletsStakingProgress,
PalletsStorage,
Expand Down
77 changes: 77 additions & 0 deletions src/services/contracts/ContractsInkService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ContractPromise } from '@polkadot/api-contract';

import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers';
import { polkadotRegistryV9190 } from '../../test-helpers/registries';
import { defaultMockApi } from '../test-helpers/mock';
import { ContractsInkService } from '.';

const contractInkService = new ContractsInkService(defaultMockApi);

const getFlipper = () =>
Promise.resolve().then(() => {
return {
debugMessage: polkadotRegistryV9190.createType('Text', ''),
gasConsumed: polkadotRegistryV9190.createType('u64', '7437907045'),
gasRequired: polkadotRegistryV9190.createType('u64', '74999922688'),
output: true,
result: polkadotRegistryV9190.createType('ContractExecResultResult', {
ok: {
flags: [],
data: '0x01',
},
}),
storageDeposit: polkadotRegistryV9190.createType('StorageDeposit', {
charge: '0',
}),
};
});

const mockContractPromise = {
query: {
get: getFlipper,
},
} as unknown as ContractPromise;

describe('ContractsInkService', () => {
it('fetchContractCall', async () => {
const address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const result = await contractInkService.fetchContractCall(
mockContractPromise,
address,
'get'
);
const expectedResponse = {
debugMessage: '',
gasConsumed: '7437907045',
gasRequired: '74999922688',
output: true,
result: {
ok: {
flags: [],
data: '0x01',
},
},
storageDeposit: {
charge: '0',
},
};

expect(sanitizeNumbers(result)).toStrictEqual(expectedResponse);
});
});
Loading

0 comments on commit f6499fa

Please sign in to comment.