@ocap/contract 1.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +30 -0
- package/lib/index.d.ts +31 -0
- package/lib/index.js +249 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2018-2019 ArcBlock
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://github.com/prettier/prettier)
|
|
4
|
+
[](https://docs.arcblock.io)
|
|
5
|
+
[](https://gitter.im/ArcBlock/community?utm_source=badge\&utm_medium=badge\&utm_campaign=pr-badge)
|
|
6
|
+
|
|
7
|
+
> Utility to validate compile and execute ocap contract
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
* [Install](#install)
|
|
13
|
+
* [Usage](#usage)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @ocap/contract
|
|
20
|
+
// or
|
|
21
|
+
yarn add @ocap/contract
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const contract;
|
|
29
|
+
// TODO
|
|
30
|
+
```
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generate by [js2dts@0.3.3](https://github.com/whxaxes/js2dts#readme)
|
|
2
|
+
|
|
3
|
+
declare const _Lib: _Lib.T105;
|
|
4
|
+
declare namespace _Lib {
|
|
5
|
+
export interface T100 {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
export interface T101 {
|
|
9
|
+
value: any;
|
|
10
|
+
tokens: any;
|
|
11
|
+
}
|
|
12
|
+
export interface T102 {
|
|
13
|
+
type: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
export interface T103 {
|
|
17
|
+
args: _Lib.T102[];
|
|
18
|
+
}
|
|
19
|
+
export interface T104 {
|
|
20
|
+
transfer: _Lib.T103;
|
|
21
|
+
transferToken: _Lib.T103;
|
|
22
|
+
}
|
|
23
|
+
export interface T105 {
|
|
24
|
+
compile: (source: string, ...args: any[]) => any[];
|
|
25
|
+
validate: (compiled: any[], quota: any, ensureZeroSum?: boolean) => boolean;
|
|
26
|
+
merge: (compiled: any) => any[];
|
|
27
|
+
getQuota: (input?: _Lib.T100) => _Lib.T101;
|
|
28
|
+
SUPPORTED_CALLS: _Lib.T104;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export = _Lib;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax */
|
|
2
|
+
const get = require('lodash/get');
|
|
3
|
+
const set = require('lodash/set');
|
|
4
|
+
const esprima = require('esprima');
|
|
5
|
+
const { types } = require('@ocap/mcrypto');
|
|
6
|
+
const { BN } = require('@ocap/util');
|
|
7
|
+
const { isValid, toTypeInfo } = require('@arcblock/did');
|
|
8
|
+
|
|
9
|
+
const isCallExpression = (x) =>
|
|
10
|
+
x.type === 'ExpressionStatement' && x.expression && x.expression.type === 'CallExpression';
|
|
11
|
+
|
|
12
|
+
const ZERO = new BN('0');
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_CALLS = {
|
|
15
|
+
// used to transfer primary token to someone
|
|
16
|
+
transfer: {
|
|
17
|
+
args: [
|
|
18
|
+
{
|
|
19
|
+
type: 'Literal',
|
|
20
|
+
name: 'to',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'Literal',
|
|
24
|
+
name: 'amount',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
// used to transfer secondary token to someone
|
|
29
|
+
transferToken: {
|
|
30
|
+
args: [
|
|
31
|
+
{
|
|
32
|
+
type: 'Literal',
|
|
33
|
+
name: 'tokenAddress',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'Literal',
|
|
37
|
+
name: 'to',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'Literal',
|
|
41
|
+
name: 'amount',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Try to compile contract source code and return call set
|
|
49
|
+
*
|
|
50
|
+
* @param {string} source
|
|
51
|
+
* @return {array} list of parsed contract call and its args
|
|
52
|
+
* @throws {Error} for invalid contract or args
|
|
53
|
+
*/
|
|
54
|
+
const compile = (source) => {
|
|
55
|
+
if (typeof source !== 'string' || !source) {
|
|
56
|
+
throw new Error('contract source must be a non-empty string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const { body } = esprima.parseScript(source);
|
|
61
|
+
if (body.every((x) => isCallExpression(x)) === false) {
|
|
62
|
+
throw new Error('only function call expressions are allowed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = [];
|
|
66
|
+
|
|
67
|
+
const expressions = body.map((x) => x.expression);
|
|
68
|
+
const allowedCalls = Object.keys(SUPPORTED_CALLS);
|
|
69
|
+
for (const expression of expressions) {
|
|
70
|
+
const name = get(expression, 'callee.name');
|
|
71
|
+
if (allowedCalls.includes(name) === false) {
|
|
72
|
+
throw new Error(`unrecognized call ${name}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const expectedArgs = SUPPORTED_CALLS[name].args;
|
|
76
|
+
const actualArgs = expression.arguments;
|
|
77
|
+
const expectedArgCount = expectedArgs.length;
|
|
78
|
+
const actualArgCount = actualArgs.length;
|
|
79
|
+
if (actualArgCount !== expectedArgCount) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`invalid number of arguments for ${name}, expected ${expectedArgCount}, but found ${actualArgCount}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < expectedArgs.length; i++) {
|
|
86
|
+
const expectedArg = expectedArgs[i];
|
|
87
|
+
const actualArg = actualArgs[i];
|
|
88
|
+
if (expectedArg.type !== actualArg.type) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`invalid #${i + 1} argument for ${name}, expected ${expectedArg.type}, but found ${actualArg.type}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
result.push({
|
|
96
|
+
call: name,
|
|
97
|
+
args: expectedArgs.reduce((acc, x, i) => {
|
|
98
|
+
acc[x.name] = actualArgs[i].value;
|
|
99
|
+
return acc;
|
|
100
|
+
}, {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result.length === 0) {
|
|
105
|
+
throw new Error('compiled contract result is empty');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
throw new Error(`Invalid contract source: ${err.message}`);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const validateDid = (to, allowedRoleTypes) => {
|
|
115
|
+
if (isValid(to) === false) {
|
|
116
|
+
throw new Error('invalid did');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const type = toTypeInfo(to);
|
|
120
|
+
if (allowedRoleTypes.includes(type.role) === false) {
|
|
121
|
+
throw new Error('invalid did role type');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const validateAmount = (amount) => {
|
|
128
|
+
let bn = null;
|
|
129
|
+
try {
|
|
130
|
+
bn = new BN(amount);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new Error(`invalid amount: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (bn.toString() !== amount) {
|
|
136
|
+
throw new Error('invalid amount: not valid big number');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return bn;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate the compiled contract source
|
|
144
|
+
*
|
|
145
|
+
* @param {Array} compiled contract call list
|
|
146
|
+
* @param {object} quota { value, tokens: { [address]: value }}
|
|
147
|
+
* @return {boolean}
|
|
148
|
+
*/
|
|
149
|
+
const validate = (compiled, quota, ensureZeroSum = false) => {
|
|
150
|
+
if (!Array.isArray(compiled) || compiled.length === 0) {
|
|
151
|
+
throw new Error('no contract calls to validate');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// to track consumption
|
|
155
|
+
let remaining = new BN(quota ? quota.value : '0');
|
|
156
|
+
const tokenRemaining = Object.keys(quota && quota.tokens ? quota.tokens : {}).reduce((acc, x) => {
|
|
157
|
+
acc[x] = new BN(quota.tokens[x]);
|
|
158
|
+
return acc;
|
|
159
|
+
}, {});
|
|
160
|
+
|
|
161
|
+
for (const item of compiled) {
|
|
162
|
+
const { call, args } = item;
|
|
163
|
+
if (call === 'transfer') {
|
|
164
|
+
validateDid(args.to, [types.RoleType.ROLE_APPLICATION, types.RoleType.ROLE_ACCOUNT]);
|
|
165
|
+
const consumption = validateAmount(args.amount);
|
|
166
|
+
if (consumption.lte(ZERO)) {
|
|
167
|
+
throw new Error('transfer amount must be greater than 0');
|
|
168
|
+
}
|
|
169
|
+
if (quota) {
|
|
170
|
+
if (remaining.sub(consumption).lt(ZERO)) {
|
|
171
|
+
throw new Error('transfer exceeded quota');
|
|
172
|
+
}
|
|
173
|
+
remaining = remaining.sub(consumption);
|
|
174
|
+
}
|
|
175
|
+
} else if (call === 'transferToken') {
|
|
176
|
+
validateDid(args.tokenAddress, [types.RoleType.ROLE_TOKEN]);
|
|
177
|
+
validateDid(args.to, [types.RoleType.ROLE_APPLICATION, types.RoleType.ROLE_ACCOUNT]);
|
|
178
|
+
|
|
179
|
+
const token = args.tokenAddress;
|
|
180
|
+
const consumption = validateAmount(args.amount);
|
|
181
|
+
if (consumption.lte(ZERO)) {
|
|
182
|
+
throw new Error('token transfer amount must be greater than 0');
|
|
183
|
+
}
|
|
184
|
+
if (quota) {
|
|
185
|
+
if (!tokenRemaining[token] || tokenRemaining[token].sub(consumption).lt(ZERO)) {
|
|
186
|
+
throw new Error(`transferToken for ${args.tokenAddress} exceeded quota`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
tokenRemaining[token] = tokenRemaining[token].sub(consumption);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ensureZeroSum) {
|
|
195
|
+
if (remaining.eq(ZERO) === false) {
|
|
196
|
+
throw new Error('transfer violates zero sum');
|
|
197
|
+
}
|
|
198
|
+
const invalidToken = Object.keys(tokenRemaining).find((x) => tokenRemaining[x].eq(ZERO) === false);
|
|
199
|
+
if (invalidToken) {
|
|
200
|
+
throw new Error(`transferToken for ${invalidToken} violates zero sum`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// merge duplicate receiver transfers if they exist
|
|
208
|
+
const merge = (compiled) => {
|
|
209
|
+
const mergeAndSum = (items, uniqKey, sumBy) => {
|
|
210
|
+
const result = {};
|
|
211
|
+
const keys = Array.isArray(uniqKey) ? uniqKey : [uniqKey];
|
|
212
|
+
keys.sort();
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
const key = keys.map((x) => get(item, x)).join('-');
|
|
215
|
+
if (result[key] === undefined) {
|
|
216
|
+
result[key] = item;
|
|
217
|
+
} else {
|
|
218
|
+
const old = new BN(get(result[key], sumBy));
|
|
219
|
+
const delta = new BN(get(item, sumBy));
|
|
220
|
+
set(result[key], sumBy, old.add(delta).toString());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return Object.values(result);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
...mergeAndSum(
|
|
229
|
+
compiled.filter((x) => x.call === 'transfer'),
|
|
230
|
+
'args.to',
|
|
231
|
+
'args.amount'
|
|
232
|
+
),
|
|
233
|
+
...mergeAndSum(
|
|
234
|
+
compiled.filter((x) => x.call === 'transferToken'),
|
|
235
|
+
['args.tokenAddress', 'args.to'],
|
|
236
|
+
'args.amount'
|
|
237
|
+
),
|
|
238
|
+
];
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getQuota = (input = {}) => ({
|
|
242
|
+
value: input.value || '0',
|
|
243
|
+
tokens: (input.tokens || []).reduce((acc, x) => {
|
|
244
|
+
acc[x.address] = x.value;
|
|
245
|
+
return acc;
|
|
246
|
+
}, {}),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
module.exports = { compile, validate, merge, getQuota, SUPPORTED_CALLS };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ocap/contract",
|
|
3
|
+
"description": "Utility to compile/validate and run ocap contract",
|
|
4
|
+
"version": "1.6.10",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "wangshijun",
|
|
7
|
+
"email": "shijun@arcblock.io",
|
|
8
|
+
"url": "https://github.com/wangshijun"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/ArcBlock/asset-chain/issues",
|
|
12
|
+
"email": "shijun@arcblock.io"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"contributors": [
|
|
18
|
+
"wangshijun <shijun@arcblock.io> (https://github.com/wangshijun)"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@arcblock/did": "1.6.10",
|
|
22
|
+
"@ocap/mcrypto": "1.6.10",
|
|
23
|
+
"@ocap/util": "1.6.10",
|
|
24
|
+
"debug": "^4.3.3",
|
|
25
|
+
"esprima": "^4.0.1",
|
|
26
|
+
"lodash": "^4.17.21"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"jest": "^27.3.1",
|
|
30
|
+
"remark-cli": "^10.0.1",
|
|
31
|
+
"remark-preset-github": "^4.0.1"
|
|
32
|
+
},
|
|
33
|
+
"remarkConfig": {
|
|
34
|
+
"plugins": [
|
|
35
|
+
"preset-github",
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
"repository": "ArcBlock/asset-chain"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/ArcBlock/asset-chain/tree/master/core/contract",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"blockchain",
|
|
46
|
+
"arcblock",
|
|
47
|
+
"sdk",
|
|
48
|
+
"nodejs"
|
|
49
|
+
],
|
|
50
|
+
"license": "Apache-2.0",
|
|
51
|
+
"main": "./lib/index.js",
|
|
52
|
+
"files": [
|
|
53
|
+
"lib"
|
|
54
|
+
],
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/ArcBlock/asset-chain/tree/master/core/contract"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"lint": "eslint lib tests",
|
|
61
|
+
"lint:fix": "eslint --fix lib tests",
|
|
62
|
+
"docs": "yarn gen-dts && yarn gen-docs && yarn cleanup-docs && yarn format-docs",
|
|
63
|
+
"cleanup-docs": "node ../../scripts/cleanup-docs.js docs/README.md $npm_package_name",
|
|
64
|
+
"gen-dts": "j2d lib/index.js",
|
|
65
|
+
"gen-docs": "jsdoc2md lib/index.js > docs/README.md",
|
|
66
|
+
"format-docs": "remark . -o",
|
|
67
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
68
|
+
"coverage": "yarn test -- --coverage"
|
|
69
|
+
},
|
|
70
|
+
"gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
|
|
71
|
+
}
|