diff --git a/jest.config.js b/jest.config.js index 4ac8eb1368..1fdc16e75b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,7 @@ const config = require('@polkadot/dev/config/jest'); module.exports = Object.assign({}, config, { moduleNameMapper: { + '@polkadot/api-contract(.*)$': '/packages/api-contract/src/$1', '@polkadot/api-derive(.*)$': '/packages/api-derive/src/$1', '@polkadot/api(.*)$': '/packages/api/src/$1', '@polkadot/rpc-(core|provider|rx)(.*)$': '/packages/rpc-$1/src/$2', @@ -13,6 +14,7 @@ module.exports = Object.assign({}, config, { modulePathIgnorePatterns: [ '/packages/api/build', '/packages/api-derive/build', + '/packages/api-contract/build', '/packages/rpc-core/build', '/packages/rpc-provider/build', '/packages/rpc-rx/build', diff --git a/packages/api-contract/LICENSE b/packages/api-contract/LICENSE new file mode 100644 index 0000000000..0d381b2e97 --- /dev/null +++ b/packages/api-contract/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/api-contract/README.md b/packages/api-contract/README.md new file mode 100644 index 0000000000..46f70ad392 --- /dev/null +++ b/packages/api-contract/README.md @@ -0,0 +1,13 @@ +# @polkadot/api-contract + +Interfaces to allow for the encoding and decoding of Substrate contract ABIs. + +```js +import { Abi } from '@polkadot/api-contract'; + +const abi = new Abi(<...JSON ABI...>); + +api.tx.contract + .call(, , , abi.messages.(<...params...>)) + .signAndSend(, (result: SubmittableResult) => { ... }); +``` diff --git a/packages/api-contract/package.json b/packages/api-contract/package.json new file mode 100644 index 0000000000..98b651ad84 --- /dev/null +++ b/packages/api-contract/package.json @@ -0,0 +1,32 @@ +{ + "name": "@polkadot/api-contract", + "version": "0.81.0-beta.17", + "description": "Interfaces for interacting with contracts and contract ABIs", + "main": "index.js", + "keywords": [ + "Polkadot", + "ABI" + ], + "author": "Jaco Greeff ", + "maintainers": [ + "Jaco Greeff " + ], + "contributors": [], + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/polkadot-js/api.git" + }, + "bugs": { + "url": "https://github.com/polkadot-js/api/issues" + }, + "homepage": "https://github.com/polkadot-js/api/tree/master/packages/api-contract#readme", + "dependencies": { + "@babel/runtime": "^7.4.5", + "@polkadot/types": "^0.81.0-beta.17" + } +} diff --git a/packages/api-contract/src/Abi.spec.ts b/packages/api-contract/src/Abi.spec.ts new file mode 100644 index 0000000000..c62a3e79b0 --- /dev/null +++ b/packages/api-contract/src/Abi.spec.ts @@ -0,0 +1,101 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ContractABIFn$Arg } from './types'; + +import typesAbi from '../test/abi/types.json'; +import erc20Abi from '../test/contracts/erc20.json'; + +import { Abi } from '.'; + +describe('Abi', () => { + describe('types encoding', () => { + let abi: Abi; + + beforeAll(() => { + abi = new Abi(typesAbi); + }); + + function check (method: string, args: Array, type: string | null): void { + const fn = abi.messages[method]; + + expect(fn.args).toEqual(args); + expect(fn.type).toEqual(type); + } + + it('has the primitive method with args & return', () => { + check( + 'primitive', + [ + { name: 'bool', type: 'bool' }, + { name: 'u32', type: 'u32' } + ], + 'bool' + ); + }); + + it('has the vector method with args & return', () => { + check( + 'vector', + [ + { name: 'vecU8', type: 'Vec' }, + { name: 'vecU32', type: 'Vec' } + ], + 'Vec' + ); + }); + + it('has the vector_fixed method with args & return', () => { + check( + 'vectorFixed', + [ + { name: 'vecU8Length32', type: '[u8;32]' } + ], + '[u32;8]' + ); + }); + + it('has the option method with args & return', () => { + check( + 'option', + [ + { name: 'optionU32', type: 'Option' } + ], + 'Option' + ); + }); + + it('has the result method with return (empty)', () => { + check( + 'result', + [], + '()' + ); + }); + + it('allows for nested args', () => { + check( + 'nested', + [ + { name: 'optionVec', type: 'Option>' } + ], + 'Vec<(u32,u64)>' + ); + }); + }); + + describe('erc20', () => { + let abi: Abi; + + beforeEach(() => { + abi = new Abi(erc20Abi); + }); + + it('has the attached methods', () => { + expect(Object.keys(abi.messages)).toEqual( + ['totalSupply', 'balanceOf', 'allowance', 'transfer', 'approve', 'transferFrom'] + ); + }); + }); +}); diff --git a/packages/api-contract/src/Abi.ts b/packages/api-contract/src/Abi.ts new file mode 100644 index 0000000000..c28fbb8f3c --- /dev/null +++ b/packages/api-contract/src/Abi.ts @@ -0,0 +1,29 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ContractABI, ContractABIFn } from './types'; + +import { stringCamelCase } from '@polkadot/util'; + +import { createMethod } from './method'; +import { validateAbi } from './validation'; + +export default class ContractAbi { + readonly abi: ContractABI; + readonly deploy: ContractABIFn; + readonly messages: { [index: string]: ContractABIFn } = {}; + + constructor (abi: ContractABI) { + validateAbi(abi); + + this.abi = abi; + this.deploy = createMethod('deploy', abi.deploy); + + abi.messages.forEach((method) => { + const name = stringCamelCase(method.name); + + this.messages[name] = createMethod(`messages.${name}`, method); + }); + } +} diff --git a/packages/api-contract/src/Contract.ts b/packages/api-contract/src/Contract.ts new file mode 100644 index 0000000000..eb87937a77 --- /dev/null +++ b/packages/api-contract/src/Contract.ts @@ -0,0 +1,24 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ContractABI } from './types'; + +import { AccountId } from '@polkadot/types'; + +import Abi from './Abi'; + +export default class Contract { + readonly abi: Abi; + readonly address: AccountId; + + // TODO This needs to be useful + // - pass API instance + // - expose calls here so we don't need to do the encoding manually + // + // Also, we need the same structure for managing code and deploying + constructor (address: string | AccountId, abi: ContractABI) { + this.abi = new Abi(abi); + this.address = new AccountId(address); + } +} diff --git a/packages/api-contract/src/index.ts b/packages/api-contract/src/index.ts new file mode 100644 index 0000000000..d658180876 --- /dev/null +++ b/packages/api-contract/src/index.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +export { default as Abi } from './Abi'; +export { default as Contract } from './Contract'; diff --git a/packages/api-contract/src/method.ts b/packages/api-contract/src/method.ts new file mode 100644 index 0000000000..9fccdab03d --- /dev/null +++ b/packages/api-contract/src/method.ts @@ -0,0 +1,69 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { CodecArg, Constructor } from '@polkadot/types/types'; +import { ContractABIFn, ContractABIFn$Arg, ContractABIMethod, ContractABIMethodBase, ContractABITypes } from './types'; + +import { Compact, createClass } from '@polkadot/types'; +import { assert, isString, isUndefined, stringCamelCase } from '@polkadot/util'; + +export function typeToString (type: ContractABITypes): string { + if (isString(type)) { + return type; + } else if (Array.isArray(type)) { + return `(${type.map((type) => typeToString(type)).join(',')})`; + } else if (type['Option']) { + return `Option<${typeToString(type['Option'].T)}>`; + } else if (type['Result']) { + return `()`; // Result is not supported, but only applicable for returns + } else if (type['Vec']) { + return `Vec<${typeToString(type['Vec'].T)}>`; + } else if (type['[T;n]']) { + return `[${typeToString(type['[T;n]'].T)};${type['[T;n]'].n}]`; + } + + throw new Error(`Unknown type specified ${JSON.stringify(type)}`); +} + +export function createArgClass (args: Array, baseDef: { [index: string]: string }): Constructor { + return createClass( + JSON.stringify( + args.reduce((base: { [index: string]: string }, { name, type }) => { + base[name] = type; + + return base; + }, baseDef) + ) + ); +} + +export function createMethod (name: string, method: Partial & ContractABIMethodBase): ContractABIFn { + const args: Array = method.args.map(({ name, type }) => ({ + name: stringCamelCase(name), + type: typeToString(type) + })); + const Clazz = createArgClass(args, isUndefined(method.selector) ? {} : { __selector: 'u32' }); + const baseStruct: { [index: string]: any } = { __selector: method.selector }; + const encoder = (...params: Array): Uint8Array => { + assert(params.length === args.length, `Expected ${args.length} arguments to contract ${name}, found ${params.length}`); + + const u8a = new Clazz( + args.reduce((mapped, { name }, index) => { + mapped[name] = params[index]; + + return mapped; + }, { ...baseStruct }) + ).toU8a(); + + return Compact.addLengthPrefix(u8a); + }; + + const fn = (encoder as ContractABIFn); + + fn.args = args; + fn.isConstant = !(method.mutates || false); + fn.type = method.return_type ? typeToString(method.return_type) : null; + + return fn; +} diff --git a/packages/api-contract/src/types.ts b/packages/api-contract/src/types.ts new file mode 100644 index 0000000000..43298bb917 --- /dev/null +++ b/packages/api-contract/src/types.ts @@ -0,0 +1,58 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { CodecArg } from '@polkadot/types/types'; + +export type ContractABITypes$Struct = { + 'Option'?: { + T: ContractABITypes + }, + 'Result'?: { + T: ContractABITypes, + E: ContractABITypes + }, + 'Vec'?: { + T: ContractABITypes + }, + '[T;n]'?: { + T: ContractABITypes, + n: number + } +}; + +export type ContractABITypes = string | ContractABITypes$Struct | Array; + +export type ContractABIArg = { + name: string, + type: ContractABITypes +}; + +export type ContractABIMethodBase = { + args: Array +}; + +export type ContractABIMethod = ContractABIMethodBase & { + mutates?: boolean, + name: string, + selector: number, + return_type: ContractABITypes | null +}; + +export type ContractABI = { + deploy: ContractABIMethodBase, + messages: Array, + name: string +}; + +export interface ContractABIFn$Arg { + name: string; + type: string; +} + +export interface ContractABIFn { + (...args: Array): Uint8Array; + args: Array; + isConstant: boolean; + type: string | null; +} diff --git a/packages/api-contract/src/validation.ts b/packages/api-contract/src/validation.ts new file mode 100644 index 0000000000..74fe50446a --- /dev/null +++ b/packages/api-contract/src/validation.ts @@ -0,0 +1,50 @@ +// Copyright 2017-2019 @polkadot/api-contract authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { ContractABI, ContractABIArg } from './types'; + +import { assert, isNumber, isNull, isString, isObject } from '@polkadot/util'; + +export function validateArgs (name: string, args: Array): void { + assert(Array.isArray(args), `Expected 'args' to exist on ${name}`); + + args.forEach((arg) => { + const unknownKeys = Object.keys(arg).filter((key) => !['name', 'type'].includes(key)); + + assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for ${name}`); + assert(isString(arg.name), `${name} args should have valid name `); + assert(isString(arg.type) || isObject(arg.type), `${name} args should have valid type`); + }); +} + +export function validateDeploy ({ deploy }: ContractABI): void { + const unknownKeys = Object.keys(deploy).filter((key) => !['args'].includes(key)); + + assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI deploy`); + + validateArgs('deploy', deploy.args); +} + +export function validateMethods ({ messages }: ContractABI): void { + messages.forEach((method) => { + const unknownKeys = Object.keys(method).filter((key) => !['args', 'mutates', 'name', 'selector', 'return_type'].includes(key)); + + assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for messages.${method.name}`); + assert(isString(method.name), `Expected name for messages.${method.name}`); + assert(isNumber(method.selector), `Expected selector for messages.${method.name}`); + assert(isNull(method.return_type) || isString(method.return_type) || isObject(method.return_type), `Expected return_type for messages.${method.name}`); + + validateArgs(`messages.${method.name}`, method.args); + }); +} + +export function validateAbi (abi: ContractABI): void { + const unknownKeys = Object.keys(abi).filter((key) => !['deploy', 'messages', 'name'].includes(key)); + + assert(unknownKeys.length === 0, `Unknown fields ${unknownKeys.join(', ')} found in ABI`); + assert(abi.deploy && abi.messages && isString(abi.name), 'ABI should have deploy, messages & name sections'); + + validateDeploy(abi); + validateMethods(abi); +} diff --git a/packages/types/test/abi/types.json b/packages/api-contract/test/abi/types.json similarity index 100% rename from packages/types/test/abi/types.json rename to packages/api-contract/test/abi/types.json diff --git a/packages/api/test/data/erc20.json b/packages/api-contract/test/contracts/erc20.json similarity index 100% rename from packages/api/test/data/erc20.json rename to packages/api-contract/test/contracts/erc20.json diff --git a/packages/api/test/data/erc20.wasm b/packages/api-contract/test/contracts/erc20.wasm similarity index 100% rename from packages/api/test/data/erc20.wasm rename to packages/api-contract/test/contracts/erc20.wasm diff --git a/packages/api/test/data/flipper-pruned.wasm b/packages/api-contract/test/contracts/flipper-pruned.wasm similarity index 100% rename from packages/api/test/data/flipper-pruned.wasm rename to packages/api-contract/test/contracts/flipper-pruned.wasm diff --git a/packages/api/test/data/flipper.json b/packages/api-contract/test/contracts/flipper.json similarity index 100% rename from packages/api/test/data/flipper.json rename to packages/api-contract/test/contracts/flipper.json diff --git a/packages/api/test/data/incrementer.json b/packages/api-contract/test/contracts/incrementer.json similarity index 100% rename from packages/api/test/data/incrementer.json rename to packages/api-contract/test/contracts/incrementer.json diff --git a/packages/api/test/data/shared_vec-pruned.wasm b/packages/api-contract/test/contracts/shared_vec-pruned.wasm similarity index 100% rename from packages/api/test/data/shared_vec-pruned.wasm rename to packages/api-contract/test/contracts/shared_vec-pruned.wasm diff --git a/packages/api/test/data/shared_vec.json b/packages/api-contract/test/contracts/shared_vec.json similarity index 100% rename from packages/api/test/data/shared_vec.json rename to packages/api-contract/test/contracts/shared_vec.json diff --git a/packages/api/test/e2e/promise-contract.spec.ts b/packages/api/test/e2e/promise-contract.spec.ts index 7832a7ae79..fc8f7f77ac 100644 --- a/packages/api/test/e2e/promise-contract.spec.ts +++ b/packages/api/test/e2e/promise-contract.spec.ts @@ -2,17 +2,20 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { KeyringPair } from '@polkadot/keyring/types'; + import fs from 'fs'; import path from 'path'; -import { ContractAbi, Address, Hash } from '@polkadot/types'; -import { KeyringPair } from '@polkadot/keyring/types'; +import { Abi } from '@polkadot/api-contract'; import testingPairs from '@polkadot/keyring/testingPairs'; +import { Address, Hash } from '@polkadot/types'; -import Api from './../../src/promise'; -import flipperAbi from '../data/flipper.json'; -import erc20Abi from '../data/erc20.json'; -import { SubmittableResult } from './../../src'; +import flipperAbi from '../../../api-contract/test/contracts/flipper.json'; + +import { ApiPromise, SubmittableResult } from '../../src'; + +const flipperCode = fs.readFileSync(path.join(__dirname, '../../../api-contract/test/contracts/flipper-pruned.wasm')).toString('hex'); describe.skip('Promise e2e contracts', () => { let address: Address; @@ -20,11 +23,11 @@ describe.skip('Promise e2e contracts', () => { let keyring: { [index: string]: KeyringPair }; - let api: Api; + let api: ApiPromise; beforeEach(async (done) => { if (!api) { - api = await Api.create(); + api = await ApiPromise.create(); keyring = testingPairs({ type: 'sr25519' }); } @@ -35,31 +38,29 @@ describe.skip('Promise e2e contracts', () => { describe('flipper', () => { const MAX_GAS = 500000; - let abi: ContractAbi; + let abi: Abi; beforeEach(() => { - abi = new ContractAbi(flipperAbi); + abi = new Abi(flipperAbi); }); it('allows putCode', (done) => { - const code = fs.readFileSync(path.join(__dirname, '../data/flipper-pruned.wasm')).toString('hex'); - return ( api.tx.contract - .putCode(MAX_GAS, `0x${code}`) - .signAndSend(keyring.eve, (result: SubmittableResult) => { - console.error('putCode', JSON.stringify(result)); + .putCode(MAX_GAS, `0x${flipperCode}`) + .signAndSend(keyring.eve, (result: SubmittableResult) => { + console.error('putCode', JSON.stringify(result)); - if (result.status.isFinalized) { - const record = result.findRecord('contract', 'CodeStored'); + if (result.status.isFinalized) { + const record = result.findRecord('contract', 'CodeStored'); - if (record) { - codeHash = record.event.data[0] as Hash; + if (record) { + codeHash = record.event.data[0] as Hash; - done(); + done(); + } } - } - }) + }) ); }); @@ -101,18 +102,4 @@ describe.skip('Promise e2e contracts', () => { ); }); }); - - describe('erc20', () => { - let abi: ContractAbi; - - beforeEach(() => { - abi = new ContractAbi(erc20Abi); - }); - - it('has the attached methods', () => { - expect(Object.keys(abi.messages)).toEqual( - ['totalSupply', 'balanceOf', 'allowance', 'transfer', 'approve', 'transferFrom'] - ); - }); - }); }); diff --git a/packages/types/src/ContractAbi.spec.ts b/packages/types/src/ContractAbi.spec.ts deleted file mode 100644 index 348c3945aa..0000000000 --- a/packages/types/src/ContractAbi.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import TypesAbi from '../test/abi/types.json'; - -import ContractAbi, { ContractABIFn$Arg } from './ContractAbi'; - -describe('ContractAbi', () => { - let abi: ContractAbi; - - beforeAll(() => { - abi = new ContractAbi(TypesAbi); - }); - - function check (method: string, args: Array, type: string | null): void { - const fn = abi.messages[method]; - - expect(fn.args).toEqual(args); - expect(fn.type).toEqual(type); - } - - it('has the primitive method with args & return', () => { - check( - 'primitive', - [ - { name: 'bool', type: 'bool' }, - { name: 'u32', type: 'u32' } - ], - 'bool' - ); - }); - - it('has the vector method with args & return', () => { - check( - 'vector', - [ - { name: 'vecU8', type: 'Vec' }, - { name: 'vecU32', type: 'Vec' } - ], - 'Vec' - ); - }); - - it('has the vector_fixed method with args & return', () => { - check( - 'vectorFixed', - [ - { name: 'vecU8Length32', type: '[u8;32]' } - ], - '[u32;8]' - ); - }); - - it('has the option method with args & return', () => { - check( - 'option', - [ - { name: 'optionU32', type: 'Option' } - ], - 'Option' - ); - }); - - it('has the result method with return (empty)', () => { - check( - 'result', - [], - '()' - ); - }); - - it('allows for nested args', () => { - check( - 'nested', - [ - { name: 'optionVec', type: 'Option>' } - ], - 'Vec<(u32,u64)>' - ); - }); -}); diff --git a/packages/types/src/ContractAbi.ts b/packages/types/src/ContractAbi.ts deleted file mode 100644 index 2aff586092..0000000000 --- a/packages/types/src/ContractAbi.ts +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2017-2019 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { CodecArg, Constructor } from './types'; - -import { assert, isNumber, isNull, isString, isUndefined, stringCamelCase, isObject } from '@polkadot/util'; - -import Compact from './codec/Compact'; -import { createClass } from './codec/createType'; - -export type ContractABITypes$Struct = { - 'Option'?: { - T: ContractABITypes - }, - 'Result'?: { - T: ContractABITypes, - E: ContractABITypes - }, - 'Vec'?: { - T: ContractABITypes - }, - '[T;n]'?: { - T: ContractABITypes, - n: number - } -}; - -export type ContractABITypes = string | ContractABITypes$Struct | Array; - -export type ContractABIArg = { - name: string, - type: ContractABITypes -}; - -export type ContractABIMethodBase = { - args: Array -}; - -export type ContractABIMethod = ContractABIMethodBase & { - mutates?: boolean, - name: string, - selector: number, - return_type: ContractABITypes | null -}; - -export type ContractABI = { - deploy: ContractABIMethodBase, - messages: Array, - name: string -}; - -export interface ContractABIFn$Arg { - name: string; - type: string; -} - -export interface ContractABIFn { - (...args: Array): Uint8Array; - args: Array; - isConstant: boolean; - type: string | null; -} - -export interface Contract { - abi: ContractABI; - deploy: ContractABIFn; - messages: { - [index: string]: ContractABIFn - }; -} - -export function validateArgs (name: string, args: Array): void { - assert(Array.isArray(args), `Expected 'args' to exist on ${name}`); - - args.forEach((arg) => { - const unknownKeys = Object.keys(arg).filter((key) => !['name', 'type'].includes(key)); - - assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for ${name}`); - assert(isString(arg.name), `${name} args should have valid name `); - assert(isString(arg.type) || isObject(arg.type), `${name} args should have valid type`); - }); -} - -export function validateDeploy ({ deploy }: ContractABI): void { - const unknownKeys = Object.keys(deploy).filter((key) => !['args'].includes(key)); - - assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI deploy`); - - validateArgs('deploy', deploy.args); -} - -export function validateMethods ({ messages }: ContractABI): void { - messages.forEach((method) => { - const unknownKeys = Object.keys(method).filter((key) => !['args', 'mutates', 'name', 'selector', 'return_type'].includes(key)); - - assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for messages.${method.name}`); - assert(isString(method.name), `Expected name for messages.${method.name}`); - assert(isNumber(method.selector), `Expected selector for messages.${method.name}`); - assert(isNull(method.return_type) || isString(method.return_type) || isObject(method.return_type), `Expected return_type for messages.${method.name}`); - - validateArgs(`messages.${method.name}`, method.args); - }); -} - -export function validateAbi (abi: ContractABI): void { - const unknownKeys = Object.keys(abi).filter((key) => !['deploy', 'messages', 'name'].includes(key)); - - assert(unknownKeys.length === 0, `Unknown fields ${unknownKeys.join(', ')} found in ABI`); - assert(abi.deploy && abi.messages && isString(abi.name), 'ABI should have deploy, messages & name sections'); - - validateDeploy(abi); - validateMethods(abi); -} - -export default class ContractAbi implements Contract { - abi: ContractABI; - deploy: ContractABIFn; - messages: { [index: string]: ContractABIFn } = {}; - - constructor (abi: ContractABI) { - validateAbi(abi); - - this.abi = abi; - this.deploy = this._createEncoded('deploy', abi.deploy); - - abi.messages.forEach((method) => { - const name = stringCamelCase(method.name); - - this.messages[name] = this._createEncoded(`messages.${name}`, method); - }); - } - - private _convertType (type: ContractABITypes): string { - if (isString(type)) { - return type; - } else if (Array.isArray(type)) { - return `(${type.map((type) => this._convertType(type)).join(',')})`; - } else if (type['Option']) { - return `Option<${this._convertType(type['Option'].T)}>`; - } else if (type['Result']) { - return `()`; // Result is not supported, but only applicable for returns - } else if (type['Vec']) { - return `Vec<${this._convertType(type['Vec'].T)}>`; - } else if (type['[T;n]']) { - return `[${this._convertType(type['[T;n]'].T)};${type['[T;n]'].n}]`; - } - - throw new Error(`Unknown type specified ${JSON.stringify(type)}`); - } - - private _createClazz (args: Array, baseDef: { [index: string]: string }): Constructor { - return createClass( - JSON.stringify( - args.reduce((base: { [index: string]: string }, { name, type }) => { - base[name] = type; - - return base; - }, baseDef) - ) - ); - } - - private _createEncoded (name: string, method: Partial & ContractABIMethodBase): ContractABIFn { - const args: Array = method.args.map(({ name, type }) => ({ - name: stringCamelCase(name), - type: this._convertType(type) - })); - const Clazz = this._createClazz(args, isUndefined(method.selector) ? {} : { __selector: 'u32' }); - const baseStruct: { [index: string]: any } = { __selector: method.selector }; - const encoder = (...params: Array): Uint8Array => { - assert(params.length === args.length, `Expected ${args.length} arguments to contract ${name}, found ${params.length}`); - - const u8a = new Clazz( - args.reduce((mapped, { name }, index) => { - mapped[name] = params[index]; - - return mapped; - }, { ...baseStruct }) - ).toU8a(); - - return Compact.addLengthPrefix(u8a); - }; - - const fn = (encoder as ContractABIFn); - - fn.args = args; - fn.isConstant = !(method.mutates || false); - fn.type = method.return_type ? this._convertType(method.return_type) : null; - - return fn; - } -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 4369cf4f41..b88317cd24 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -9,6 +9,5 @@ import { assertSingletonPackage } from '@polkadot/util'; assertSingletonPackage('@polkadot/types'); -export { default as ContractAbi } from './ContractAbi'; export * from './codec'; export * from './index.types'; diff --git a/tsconfig.json b/tsconfig.json index 92edb5ea3c..4caefbbd9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ { "@polkadot/api": ["packages/api/src"], "@polkadot/api/*": ["packages/api/src/*"], + "@polkadot/api-contract": ["packages/api-contract/src"], + "@polkadot/api-contract/*": ["packages/api-contract/src/*"], "@polkadot/api-derive": ["packages/api-derive/src"], "@polkadot/api-derive/*": ["packages/api-derive/src/*"], "@polkadot/rpc-core": ["packages/rpc-core/src"],