Move contracts into api-contract

This commit is contained in:
Jaco Greeff
2019-06-14 09:05:21 +02:00
parent 9bf765be08
commit af0e8cf07a
24 changed files with 610 additions and 312 deletions
+101
View File
@@ -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<ContractABIFn$Arg>, 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<u8>' },
{ name: 'vecU32', type: 'Vec<u32>' }
],
'Vec<bool>'
);
});
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<u32>' }
],
'Option<bool>'
);
});
it('has the result method with return (empty)', () => {
check(
'result',
[],
'()'
);
});
it('allows for nested args', () => {
check(
'nested',
[
{ name: 'optionVec', type: 'Option<Vec<u32>>' }
],
'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']
);
});
});
});
+29
View File
@@ -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);
});
}
}
+24
View File
@@ -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);
}
}
+6
View File
@@ -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';
+69
View File
@@ -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<T>']) {
return `Option<${typeToString(type['Option<T>'].T)}>`;
} else if (type['Result<T,E>']) {
return `()`; // Result is not supported, but only applicable for returns
} else if (type['Vec<T>']) {
return `Vec<${typeToString(type['Vec<T>'].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<ContractABIFn$Arg>, 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<ContractABIMethod> & ContractABIMethodBase): ContractABIFn {
const args: Array<ContractABIFn$Arg> = 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<CodecArg>): 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;
}
+58
View File
@@ -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>'?: {
T: ContractABITypes
},
'Result<T,E>'?: {
T: ContractABITypes,
E: ContractABITypes
},
'Vec<T>'?: {
T: ContractABITypes
},
'[T;n]'?: {
T: ContractABITypes,
n: number
}
};
export type ContractABITypes = string | ContractABITypes$Struct | Array<string | ContractABITypes$Struct>;
export type ContractABIArg = {
name: string,
type: ContractABITypes
};
export type ContractABIMethodBase = {
args: Array<ContractABIArg>
};
export type ContractABIMethod = ContractABIMethodBase & {
mutates?: boolean,
name: string,
selector: number,
return_type: ContractABITypes | null
};
export type ContractABI = {
deploy: ContractABIMethodBase,
messages: Array<ContractABIMethod>,
name: string
};
export interface ContractABIFn$Arg {
name: string;
type: string;
}
export interface ContractABIFn {
(...args: Array<CodecArg>): Uint8Array;
args: Array<ContractABIFn$Arg>;
isConstant: boolean;
type: string | null;
}
+50
View File
@@ -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<ContractABIArg>): 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);
}