@ocap/contract 1.28.9 → 1.29.0
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/README.md +0 -1
- package/esm/index.d.mts +45 -0
- package/esm/index.mjs +170 -0
- package/lib/_virtual/rolldown_runtime.cjs +29 -0
- package/lib/index.cjs +178 -0
- package/lib/index.d.cts +45 -0
- package/package.json +36 -10
- package/lib/index.d.ts +0 -31
- package/lib/index.js +0 -260
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
[](https://github.com/prettier/prettier)
|
|
4
3
|
[](https://docs.arcblock.io)
|
|
5
4
|
[](https://gitter.im/ArcBlock/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
|
6
5
|
|
package/esm/index.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface CallArg {
|
|
3
|
+
type: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
interface SupportedCall {
|
|
7
|
+
args: CallArg[];
|
|
8
|
+
}
|
|
9
|
+
interface CompiledCall {
|
|
10
|
+
call: string;
|
|
11
|
+
args: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
interface Quota {
|
|
14
|
+
value: string;
|
|
15
|
+
tokens?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
interface QuotaInput {
|
|
18
|
+
value?: string;
|
|
19
|
+
tokens?: Array<{
|
|
20
|
+
address: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
declare const SUPPORTED_CALLS: Record<string, SupportedCall>;
|
|
25
|
+
/**
|
|
26
|
+
* Try to compile contract source code and return call set
|
|
27
|
+
*
|
|
28
|
+
* @param source
|
|
29
|
+
* @return list of parsed contract call and its args
|
|
30
|
+
* @throws Error for invalid contract or args
|
|
31
|
+
*/
|
|
32
|
+
declare const compile: (source: string) => CompiledCall[];
|
|
33
|
+
/**
|
|
34
|
+
* Validate the compiled contract source
|
|
35
|
+
*
|
|
36
|
+
* @param compiled contract call list
|
|
37
|
+
* @param quota { value, tokens: { [address]: value }}
|
|
38
|
+
* @param ensureZeroSum
|
|
39
|
+
* @return boolean
|
|
40
|
+
*/
|
|
41
|
+
declare const validate: (compiled: CompiledCall[], quota?: Quota, ensureZeroSum?: boolean) => boolean;
|
|
42
|
+
declare const merge: (compiled: CompiledCall[]) => CompiledCall[];
|
|
43
|
+
declare const getQuota: (input?: QuotaInput) => Quota;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { SUPPORTED_CALLS, compile, getQuota, merge, validate };
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { isValid, toTypeInfo } from "@arcblock/did";
|
|
2
|
+
import { types } from "@ocap/mcrypto";
|
|
3
|
+
import { BN } from "@ocap/util";
|
|
4
|
+
import * as esprima from "esprima";
|
|
5
|
+
import get from "lodash/get.js";
|
|
6
|
+
import set from "lodash/set.js";
|
|
7
|
+
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
const isCallExpression = (x) => x.type === "ExpressionStatement" && x.expression && x.expression.type === "CallExpression";
|
|
10
|
+
const ZERO = new BN("0");
|
|
11
|
+
const TOKEN_RECEIVER_ROLE_TYPES = [
|
|
12
|
+
types.RoleType.ROLE_APPLICATION,
|
|
13
|
+
types.RoleType.ROLE_ACCOUNT,
|
|
14
|
+
types.RoleType.ROLE_BLOCKLET
|
|
15
|
+
];
|
|
16
|
+
const SUPPORTED_CALLS = {
|
|
17
|
+
transfer: { args: [{
|
|
18
|
+
type: "Literal",
|
|
19
|
+
name: "to"
|
|
20
|
+
}, {
|
|
21
|
+
type: "Literal",
|
|
22
|
+
name: "amount"
|
|
23
|
+
}] },
|
|
24
|
+
transferToken: { args: [
|
|
25
|
+
{
|
|
26
|
+
type: "Literal",
|
|
27
|
+
name: "tokenAddress"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "Literal",
|
|
31
|
+
name: "to"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "Literal",
|
|
35
|
+
name: "amount"
|
|
36
|
+
}
|
|
37
|
+
] }
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Try to compile contract source code and return call set
|
|
41
|
+
*
|
|
42
|
+
* @param source
|
|
43
|
+
* @return list of parsed contract call and its args
|
|
44
|
+
* @throws Error for invalid contract or args
|
|
45
|
+
*/
|
|
46
|
+
const compile = (source) => {
|
|
47
|
+
if (typeof source !== "string" || !source) throw new Error("contract source must be a non-empty string");
|
|
48
|
+
try {
|
|
49
|
+
const { body } = esprima.parseScript(source);
|
|
50
|
+
if (body.every((x) => isCallExpression(x)) === false) throw new Error("only function call expressions are allowed");
|
|
51
|
+
const result = [];
|
|
52
|
+
const expressions = body.map((x) => x.expression);
|
|
53
|
+
const allowedCalls = Object.keys(SUPPORTED_CALLS);
|
|
54
|
+
for (const expression of expressions) {
|
|
55
|
+
const name = get(expression, "callee.name");
|
|
56
|
+
if (allowedCalls.includes(name) === false) throw new Error(`unrecognized call ${name}`);
|
|
57
|
+
const expectedArgs = SUPPORTED_CALLS[name].args;
|
|
58
|
+
const actualArgs = expression.arguments;
|
|
59
|
+
const expectedArgCount = expectedArgs.length;
|
|
60
|
+
const actualArgCount = actualArgs.length;
|
|
61
|
+
if (actualArgCount !== expectedArgCount) throw new Error(`invalid number of arguments for ${name}, expected ${expectedArgCount}, but found ${actualArgCount}`);
|
|
62
|
+
for (let i = 0; i < expectedArgs.length; i++) {
|
|
63
|
+
const expectedArg = expectedArgs[i];
|
|
64
|
+
const actualArg = actualArgs[i];
|
|
65
|
+
if (expectedArg.type !== actualArg.type) throw new Error(`invalid #${i + 1} argument for ${name}, expected ${expectedArg.type}, but found ${actualArg.type}`);
|
|
66
|
+
}
|
|
67
|
+
result.push({
|
|
68
|
+
call: name,
|
|
69
|
+
args: expectedArgs.reduce((acc, x, i) => {
|
|
70
|
+
acc[x.name] = actualArgs[i].value;
|
|
71
|
+
return acc;
|
|
72
|
+
}, {})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (result.length === 0) throw new Error("compiled contract result is empty");
|
|
76
|
+
return result;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error(`Invalid contract source: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const validateDid = (to, allowedRoleTypes) => {
|
|
82
|
+
if (isValid(to) === false) throw new Error("invalid did");
|
|
83
|
+
const type = toTypeInfo(to);
|
|
84
|
+
if (typeof type.role !== "number" || allowedRoleTypes.includes(type.role) === false) throw new Error("invalid did role type");
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
const validateAmount = (amount) => {
|
|
88
|
+
let bn = null;
|
|
89
|
+
try {
|
|
90
|
+
bn = new BN(amount);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(`invalid amount: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (bn.toString() !== amount) throw new Error("invalid amount: not valid big number");
|
|
95
|
+
return bn;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Validate the compiled contract source
|
|
99
|
+
*
|
|
100
|
+
* @param compiled contract call list
|
|
101
|
+
* @param quota { value, tokens: { [address]: value }}
|
|
102
|
+
* @param ensureZeroSum
|
|
103
|
+
* @return boolean
|
|
104
|
+
*/
|
|
105
|
+
const validate = (compiled, quota, ensureZeroSum = false) => {
|
|
106
|
+
if (!Array.isArray(compiled) || compiled.length === 0) throw new Error("no contract calls to validate");
|
|
107
|
+
if (compiled.length > 30) throw new Error("too much contract calls found, max is 15");
|
|
108
|
+
let remaining = new BN(quota ? quota.value : "0");
|
|
109
|
+
const tokens = quota?.tokens ?? {};
|
|
110
|
+
const tokenRemaining = Object.keys(tokens).reduce((acc, x) => {
|
|
111
|
+
acc[x] = new BN(tokens[x]);
|
|
112
|
+
return acc;
|
|
113
|
+
}, {});
|
|
114
|
+
for (const item of compiled) {
|
|
115
|
+
const { call, args } = item;
|
|
116
|
+
if (call === "transfer") {
|
|
117
|
+
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
118
|
+
const consumption = validateAmount(args.amount);
|
|
119
|
+
if (consumption.lte(ZERO)) throw new Error("transfer amount must be greater than 0");
|
|
120
|
+
if (quota) {
|
|
121
|
+
if (remaining.sub(consumption).isNeg()) throw new Error("transfer exceeded quota");
|
|
122
|
+
remaining = remaining.sub(consumption);
|
|
123
|
+
}
|
|
124
|
+
} else if (call === "transferToken") {
|
|
125
|
+
validateDid(args.tokenAddress, [types.RoleType.ROLE_TOKEN]);
|
|
126
|
+
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
127
|
+
const token = args.tokenAddress;
|
|
128
|
+
const consumption = validateAmount(args.amount);
|
|
129
|
+
if (consumption.lte(ZERO)) throw new Error("token transfer amount must be greater than 0");
|
|
130
|
+
if (quota) {
|
|
131
|
+
if (!tokenRemaining[token] || tokenRemaining[token].sub(consumption).isNeg()) throw new Error(`transferToken for ${args.tokenAddress} exceeded quota`);
|
|
132
|
+
tokenRemaining[token] = tokenRemaining[token].sub(consumption);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (ensureZeroSum) {
|
|
137
|
+
if (remaining.isZero() === false) throw new Error("transfer violates zero sum");
|
|
138
|
+
const invalidToken = Object.keys(tokenRemaining).find((x) => tokenRemaining[x].isZero() === false);
|
|
139
|
+
if (invalidToken) throw new Error(`transferToken for ${invalidToken} violates zero sum`);
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
const merge = (compiled) => {
|
|
144
|
+
const mergeAndSum = (items, uniqKey, sumBy) => {
|
|
145
|
+
const result = {};
|
|
146
|
+
const keys = Array.isArray(uniqKey) ? uniqKey : [uniqKey];
|
|
147
|
+
keys.sort();
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
const key = keys.map((x) => get(item, x)).join("-");
|
|
150
|
+
if (result[key] === void 0) result[key] = item;
|
|
151
|
+
else {
|
|
152
|
+
const old = new BN(get(result[key], sumBy));
|
|
153
|
+
const delta = new BN(get(item, sumBy));
|
|
154
|
+
set(result[key], sumBy, old.add(delta).toString());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return Object.values(result);
|
|
158
|
+
};
|
|
159
|
+
return [...mergeAndSum(compiled.filter((x) => x.call === "transfer"), "args.to", "args.amount"), ...mergeAndSum(compiled.filter((x) => x.call === "transferToken"), ["args.tokenAddress", "args.to"], "args.amount")];
|
|
160
|
+
};
|
|
161
|
+
const getQuota = (input = {}) => ({
|
|
162
|
+
value: input.value || "0",
|
|
163
|
+
tokens: (input.tokens || []).reduce((acc, x) => {
|
|
164
|
+
acc[x.address] = x.value;
|
|
165
|
+
return acc;
|
|
166
|
+
}, {})
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
export { SUPPORTED_CALLS, compile, getQuota, merge, validate };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
|
|
29
|
+
exports.__toESM = __toESM;
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let _arcblock_did = require("@arcblock/did");
|
|
3
|
+
let _ocap_mcrypto = require("@ocap/mcrypto");
|
|
4
|
+
let _ocap_util = require("@ocap/util");
|
|
5
|
+
let esprima = require("esprima");
|
|
6
|
+
esprima = require_rolldown_runtime.__toESM(esprima);
|
|
7
|
+
let lodash_get = require("lodash/get");
|
|
8
|
+
lodash_get = require_rolldown_runtime.__toESM(lodash_get);
|
|
9
|
+
let lodash_set = require("lodash/set");
|
|
10
|
+
lodash_set = require_rolldown_runtime.__toESM(lodash_set);
|
|
11
|
+
|
|
12
|
+
//#region src/index.ts
|
|
13
|
+
const isCallExpression = (x) => x.type === "ExpressionStatement" && x.expression && x.expression.type === "CallExpression";
|
|
14
|
+
const ZERO = new _ocap_util.BN("0");
|
|
15
|
+
const TOKEN_RECEIVER_ROLE_TYPES = [
|
|
16
|
+
_ocap_mcrypto.types.RoleType.ROLE_APPLICATION,
|
|
17
|
+
_ocap_mcrypto.types.RoleType.ROLE_ACCOUNT,
|
|
18
|
+
_ocap_mcrypto.types.RoleType.ROLE_BLOCKLET
|
|
19
|
+
];
|
|
20
|
+
const SUPPORTED_CALLS = {
|
|
21
|
+
transfer: { args: [{
|
|
22
|
+
type: "Literal",
|
|
23
|
+
name: "to"
|
|
24
|
+
}, {
|
|
25
|
+
type: "Literal",
|
|
26
|
+
name: "amount"
|
|
27
|
+
}] },
|
|
28
|
+
transferToken: { args: [
|
|
29
|
+
{
|
|
30
|
+
type: "Literal",
|
|
31
|
+
name: "tokenAddress"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "Literal",
|
|
35
|
+
name: "to"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "Literal",
|
|
39
|
+
name: "amount"
|
|
40
|
+
}
|
|
41
|
+
] }
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Try to compile contract source code and return call set
|
|
45
|
+
*
|
|
46
|
+
* @param source
|
|
47
|
+
* @return list of parsed contract call and its args
|
|
48
|
+
* @throws Error for invalid contract or args
|
|
49
|
+
*/
|
|
50
|
+
const compile = (source) => {
|
|
51
|
+
if (typeof source !== "string" || !source) throw new Error("contract source must be a non-empty string");
|
|
52
|
+
try {
|
|
53
|
+
const { body } = esprima.parseScript(source);
|
|
54
|
+
if (body.every((x) => isCallExpression(x)) === false) throw new Error("only function call expressions are allowed");
|
|
55
|
+
const result = [];
|
|
56
|
+
const expressions = body.map((x) => x.expression);
|
|
57
|
+
const allowedCalls = Object.keys(SUPPORTED_CALLS);
|
|
58
|
+
for (const expression of expressions) {
|
|
59
|
+
const name = (0, lodash_get.default)(expression, "callee.name");
|
|
60
|
+
if (allowedCalls.includes(name) === false) throw new Error(`unrecognized call ${name}`);
|
|
61
|
+
const expectedArgs = SUPPORTED_CALLS[name].args;
|
|
62
|
+
const actualArgs = expression.arguments;
|
|
63
|
+
const expectedArgCount = expectedArgs.length;
|
|
64
|
+
const actualArgCount = actualArgs.length;
|
|
65
|
+
if (actualArgCount !== expectedArgCount) throw new Error(`invalid number of arguments for ${name}, expected ${expectedArgCount}, but found ${actualArgCount}`);
|
|
66
|
+
for (let i = 0; i < expectedArgs.length; i++) {
|
|
67
|
+
const expectedArg = expectedArgs[i];
|
|
68
|
+
const actualArg = actualArgs[i];
|
|
69
|
+
if (expectedArg.type !== actualArg.type) throw new Error(`invalid #${i + 1} argument for ${name}, expected ${expectedArg.type}, but found ${actualArg.type}`);
|
|
70
|
+
}
|
|
71
|
+
result.push({
|
|
72
|
+
call: name,
|
|
73
|
+
args: expectedArgs.reduce((acc, x, i) => {
|
|
74
|
+
acc[x.name] = actualArgs[i].value;
|
|
75
|
+
return acc;
|
|
76
|
+
}, {})
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (result.length === 0) throw new Error("compiled contract result is empty");
|
|
80
|
+
return result;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new Error(`Invalid contract source: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const validateDid = (to, allowedRoleTypes) => {
|
|
86
|
+
if ((0, _arcblock_did.isValid)(to) === false) throw new Error("invalid did");
|
|
87
|
+
const type = (0, _arcblock_did.toTypeInfo)(to);
|
|
88
|
+
if (typeof type.role !== "number" || allowedRoleTypes.includes(type.role) === false) throw new Error("invalid did role type");
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
const validateAmount = (amount) => {
|
|
92
|
+
let bn = null;
|
|
93
|
+
try {
|
|
94
|
+
bn = new _ocap_util.BN(amount);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw new Error(`invalid amount: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
if (bn.toString() !== amount) throw new Error("invalid amount: not valid big number");
|
|
99
|
+
return bn;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Validate the compiled contract source
|
|
103
|
+
*
|
|
104
|
+
* @param compiled contract call list
|
|
105
|
+
* @param quota { value, tokens: { [address]: value }}
|
|
106
|
+
* @param ensureZeroSum
|
|
107
|
+
* @return boolean
|
|
108
|
+
*/
|
|
109
|
+
const validate = (compiled, quota, ensureZeroSum = false) => {
|
|
110
|
+
if (!Array.isArray(compiled) || compiled.length === 0) throw new Error("no contract calls to validate");
|
|
111
|
+
if (compiled.length > 30) throw new Error("too much contract calls found, max is 15");
|
|
112
|
+
let remaining = new _ocap_util.BN(quota ? quota.value : "0");
|
|
113
|
+
const tokens = quota?.tokens ?? {};
|
|
114
|
+
const tokenRemaining = Object.keys(tokens).reduce((acc, x) => {
|
|
115
|
+
acc[x] = new _ocap_util.BN(tokens[x]);
|
|
116
|
+
return acc;
|
|
117
|
+
}, {});
|
|
118
|
+
for (const item of compiled) {
|
|
119
|
+
const { call, args } = item;
|
|
120
|
+
if (call === "transfer") {
|
|
121
|
+
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
122
|
+
const consumption = validateAmount(args.amount);
|
|
123
|
+
if (consumption.lte(ZERO)) throw new Error("transfer amount must be greater than 0");
|
|
124
|
+
if (quota) {
|
|
125
|
+
if (remaining.sub(consumption).isNeg()) throw new Error("transfer exceeded quota");
|
|
126
|
+
remaining = remaining.sub(consumption);
|
|
127
|
+
}
|
|
128
|
+
} else if (call === "transferToken") {
|
|
129
|
+
validateDid(args.tokenAddress, [_ocap_mcrypto.types.RoleType.ROLE_TOKEN]);
|
|
130
|
+
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
131
|
+
const token = args.tokenAddress;
|
|
132
|
+
const consumption = validateAmount(args.amount);
|
|
133
|
+
if (consumption.lte(ZERO)) throw new Error("token transfer amount must be greater than 0");
|
|
134
|
+
if (quota) {
|
|
135
|
+
if (!tokenRemaining[token] || tokenRemaining[token].sub(consumption).isNeg()) throw new Error(`transferToken for ${args.tokenAddress} exceeded quota`);
|
|
136
|
+
tokenRemaining[token] = tokenRemaining[token].sub(consumption);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (ensureZeroSum) {
|
|
141
|
+
if (remaining.isZero() === false) throw new Error("transfer violates zero sum");
|
|
142
|
+
const invalidToken = Object.keys(tokenRemaining).find((x) => tokenRemaining[x].isZero() === false);
|
|
143
|
+
if (invalidToken) throw new Error(`transferToken for ${invalidToken} violates zero sum`);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
const merge = (compiled) => {
|
|
148
|
+
const mergeAndSum = (items, uniqKey, sumBy) => {
|
|
149
|
+
const result = {};
|
|
150
|
+
const keys = Array.isArray(uniqKey) ? uniqKey : [uniqKey];
|
|
151
|
+
keys.sort();
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
const key = keys.map((x) => (0, lodash_get.default)(item, x)).join("-");
|
|
154
|
+
if (result[key] === void 0) result[key] = item;
|
|
155
|
+
else {
|
|
156
|
+
const old = new _ocap_util.BN((0, lodash_get.default)(result[key], sumBy));
|
|
157
|
+
const delta = new _ocap_util.BN((0, lodash_get.default)(item, sumBy));
|
|
158
|
+
(0, lodash_set.default)(result[key], sumBy, old.add(delta).toString());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Object.values(result);
|
|
162
|
+
};
|
|
163
|
+
return [...mergeAndSum(compiled.filter((x) => x.call === "transfer"), "args.to", "args.amount"), ...mergeAndSum(compiled.filter((x) => x.call === "transferToken"), ["args.tokenAddress", "args.to"], "args.amount")];
|
|
164
|
+
};
|
|
165
|
+
const getQuota = (input = {}) => ({
|
|
166
|
+
value: input.value || "0",
|
|
167
|
+
tokens: (input.tokens || []).reduce((acc, x) => {
|
|
168
|
+
acc[x.address] = x.value;
|
|
169
|
+
return acc;
|
|
170
|
+
}, {})
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
//#endregion
|
|
174
|
+
exports.SUPPORTED_CALLS = SUPPORTED_CALLS;
|
|
175
|
+
exports.compile = compile;
|
|
176
|
+
exports.getQuota = getQuota;
|
|
177
|
+
exports.merge = merge;
|
|
178
|
+
exports.validate = validate;
|
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface CallArg {
|
|
3
|
+
type: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
interface SupportedCall {
|
|
7
|
+
args: CallArg[];
|
|
8
|
+
}
|
|
9
|
+
interface CompiledCall {
|
|
10
|
+
call: string;
|
|
11
|
+
args: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
interface Quota {
|
|
14
|
+
value: string;
|
|
15
|
+
tokens?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
interface QuotaInput {
|
|
18
|
+
value?: string;
|
|
19
|
+
tokens?: Array<{
|
|
20
|
+
address: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
declare const SUPPORTED_CALLS: Record<string, SupportedCall>;
|
|
25
|
+
/**
|
|
26
|
+
* Try to compile contract source code and return call set
|
|
27
|
+
*
|
|
28
|
+
* @param source
|
|
29
|
+
* @return list of parsed contract call and its args
|
|
30
|
+
* @throws Error for invalid contract or args
|
|
31
|
+
*/
|
|
32
|
+
declare const compile: (source: string) => CompiledCall[];
|
|
33
|
+
/**
|
|
34
|
+
* Validate the compiled contract source
|
|
35
|
+
*
|
|
36
|
+
* @param compiled contract call list
|
|
37
|
+
* @param quota { value, tokens: { [address]: value }}
|
|
38
|
+
* @param ensureZeroSum
|
|
39
|
+
* @return boolean
|
|
40
|
+
*/
|
|
41
|
+
declare const validate: (compiled: CompiledCall[], quota?: Quota, ensureZeroSum?: boolean) => boolean;
|
|
42
|
+
declare const merge: (compiled: CompiledCall[]) => CompiledCall[];
|
|
43
|
+
declare const getQuota: (input?: QuotaInput) => Quota;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { SUPPORTED_CALLS, compile, getQuota, merge, validate };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocap/contract",
|
|
3
3
|
"description": "Utility to compile/validate and run ocap contract",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.29.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "wangshijun",
|
|
7
7
|
"email": "shijun@arcblock.io",
|
|
@@ -17,15 +17,39 @@
|
|
|
17
17
|
"contributors": [
|
|
18
18
|
"wangshijun <shijun@arcblock.io> (https://github.com/wangshijun)"
|
|
19
19
|
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./lib/index.cjs",
|
|
22
|
+
"module": "./esm/index.mjs",
|
|
23
|
+
"types": "./esm/index.d.mts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./esm/index.d.mts",
|
|
27
|
+
"import": "./esm/index.mjs",
|
|
28
|
+
"default": "./lib/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./lib/*.js": {
|
|
31
|
+
"types": "./esm/*.d.mts",
|
|
32
|
+
"import": "./esm/*.mjs",
|
|
33
|
+
"default": "./lib/*.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./lib/*": {
|
|
36
|
+
"types": "./esm/*.d.mts",
|
|
37
|
+
"import": "./esm/*.mjs",
|
|
38
|
+
"default": "./lib/*.cjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
20
41
|
"dependencies": {
|
|
21
|
-
"@arcblock/did": "1.
|
|
22
|
-
"@ocap/mcrypto": "1.
|
|
23
|
-
"@ocap/util": "1.
|
|
24
|
-
"debug": "^4.3
|
|
42
|
+
"@arcblock/did": "1.29.0",
|
|
43
|
+
"@ocap/mcrypto": "1.29.0",
|
|
44
|
+
"@ocap/util": "1.29.0",
|
|
45
|
+
"debug": "^4.4.3",
|
|
25
46
|
"esprima": "^4.0.1",
|
|
26
|
-
"lodash": "^4.17.
|
|
47
|
+
"lodash": "^4.17.23"
|
|
27
48
|
},
|
|
28
49
|
"devDependencies": {
|
|
50
|
+
"@types/esprima": "^4.0.6",
|
|
51
|
+
"@types/estree": "^1.0.6",
|
|
52
|
+
"@types/lodash": "^4.17.16",
|
|
29
53
|
"remark-cli": "^10.0.1",
|
|
30
54
|
"remark-preset-github": "^4.0.4"
|
|
31
55
|
},
|
|
@@ -47,21 +71,23 @@
|
|
|
47
71
|
"nodejs"
|
|
48
72
|
],
|
|
49
73
|
"license": "Apache-2.0",
|
|
50
|
-
"main": "./lib/index.js",
|
|
51
74
|
"files": [
|
|
52
|
-
"lib"
|
|
75
|
+
"lib",
|
|
76
|
+
"esm"
|
|
53
77
|
],
|
|
54
78
|
"repository": {
|
|
55
79
|
"type": "git",
|
|
56
80
|
"url": "https://github.com/ArcBlock/blockchain/tree/master/core/contract"
|
|
57
81
|
},
|
|
58
82
|
"scripts": {
|
|
83
|
+
"build": "tsdown",
|
|
84
|
+
"prebuild": "rm -rf lib esm",
|
|
59
85
|
"lint": "biome check",
|
|
60
86
|
"lint:fix": "biome check --write",
|
|
61
87
|
"docs": "bun run gen-dts && bun run gen-docs && bun run cleanup-docs && bun run format-docs",
|
|
62
88
|
"cleanup-docs": "node ../../scripts/cleanup-docs.js docs/README.md $npm_package_name",
|
|
63
|
-
"gen-dts": "j2d lib/index.
|
|
64
|
-
"gen-docs": "jsdoc2md lib/index.
|
|
89
|
+
"gen-dts": "j2d lib/index.cjs",
|
|
90
|
+
"gen-docs": "jsdoc2md lib/index.cjs > docs/README.md",
|
|
65
91
|
"format-docs": "remark . -o",
|
|
66
92
|
"test": "bun test",
|
|
67
93
|
"coverage": "npm run test -- --coverage"
|
package/lib/index.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
const get = require('lodash/get');
|
|
2
|
-
const set = require('lodash/set');
|
|
3
|
-
const esprima = require('esprima');
|
|
4
|
-
const { types } = require('@ocap/mcrypto');
|
|
5
|
-
const { BN } = require('@ocap/util');
|
|
6
|
-
const { isValid, toTypeInfo } = require('@arcblock/did');
|
|
7
|
-
|
|
8
|
-
const isCallExpression = (x) =>
|
|
9
|
-
x.type === 'ExpressionStatement' && x.expression && x.expression.type === 'CallExpression';
|
|
10
|
-
|
|
11
|
-
const ZERO = new BN('0');
|
|
12
|
-
|
|
13
|
-
const TOKEN_RECEIVER_ROLE_TYPES = [
|
|
14
|
-
types.RoleType.ROLE_APPLICATION,
|
|
15
|
-
types.RoleType.ROLE_ACCOUNT,
|
|
16
|
-
types.RoleType.ROLE_BLOCKLET,
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const SUPPORTED_CALLS = {
|
|
20
|
-
// used to transfer primary token to someone
|
|
21
|
-
transfer: {
|
|
22
|
-
args: [
|
|
23
|
-
{
|
|
24
|
-
type: 'Literal',
|
|
25
|
-
name: 'to',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
type: 'Literal',
|
|
29
|
-
name: 'amount',
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
// used to transfer secondary token to someone
|
|
34
|
-
transferToken: {
|
|
35
|
-
args: [
|
|
36
|
-
{
|
|
37
|
-
type: 'Literal',
|
|
38
|
-
name: 'tokenAddress',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: 'Literal',
|
|
42
|
-
name: 'to',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: 'Literal',
|
|
46
|
-
name: 'amount',
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Try to compile contract source code and return call set
|
|
54
|
-
*
|
|
55
|
-
* @param {string} source
|
|
56
|
-
* @return {array} list of parsed contract call and its args
|
|
57
|
-
* @throws {Error} for invalid contract or args
|
|
58
|
-
*/
|
|
59
|
-
const compile = (source) => {
|
|
60
|
-
if (typeof source !== 'string' || !source) {
|
|
61
|
-
throw new Error('contract source must be a non-empty string');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const { body } = esprima.parseScript(source);
|
|
66
|
-
if (body.every((x) => isCallExpression(x)) === false) {
|
|
67
|
-
throw new Error('only function call expressions are allowed');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = [];
|
|
71
|
-
|
|
72
|
-
const expressions = body.map((x) => x.expression);
|
|
73
|
-
const allowedCalls = Object.keys(SUPPORTED_CALLS);
|
|
74
|
-
for (const expression of expressions) {
|
|
75
|
-
const name = get(expression, 'callee.name');
|
|
76
|
-
if (allowedCalls.includes(name) === false) {
|
|
77
|
-
throw new Error(`unrecognized call ${name}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const expectedArgs = SUPPORTED_CALLS[name].args;
|
|
81
|
-
const actualArgs = expression.arguments;
|
|
82
|
-
const expectedArgCount = expectedArgs.length;
|
|
83
|
-
const actualArgCount = actualArgs.length;
|
|
84
|
-
if (actualArgCount !== expectedArgCount) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`invalid number of arguments for ${name}, expected ${expectedArgCount}, but found ${actualArgCount}`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < expectedArgs.length; i++) {
|
|
91
|
-
const expectedArg = expectedArgs[i];
|
|
92
|
-
const actualArg = actualArgs[i];
|
|
93
|
-
if (expectedArg.type !== actualArg.type) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`invalid #${i + 1} argument for ${name}, expected ${expectedArg.type}, but found ${actualArg.type}`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
result.push({
|
|
101
|
-
call: name,
|
|
102
|
-
args: expectedArgs.reduce((acc, x, i) => {
|
|
103
|
-
acc[x.name] = actualArgs[i].value;
|
|
104
|
-
return acc;
|
|
105
|
-
}, {}),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (result.length === 0) {
|
|
110
|
-
throw new Error('compiled contract result is empty');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return result;
|
|
114
|
-
} catch (err) {
|
|
115
|
-
throw new Error(`Invalid contract source: ${err.message}`);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const validateDid = (to, allowedRoleTypes) => {
|
|
120
|
-
if (isValid(to) === false) {
|
|
121
|
-
throw new Error('invalid did');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const type = toTypeInfo(to);
|
|
125
|
-
if (allowedRoleTypes.includes(type.role) === false) {
|
|
126
|
-
throw new Error('invalid did role type');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return true;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const validateAmount = (amount) => {
|
|
133
|
-
let bn = null;
|
|
134
|
-
try {
|
|
135
|
-
bn = new BN(amount);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
throw new Error(`invalid amount: ${err.message}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (bn.toString() !== amount) {
|
|
141
|
-
throw new Error('invalid amount: not valid big number');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return bn;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Validate the compiled contract source
|
|
149
|
-
*
|
|
150
|
-
* @param {Array} compiled contract call list
|
|
151
|
-
* @param {object} quota { value, tokens: { [address]: value }}
|
|
152
|
-
* @return {boolean}
|
|
153
|
-
*/
|
|
154
|
-
const validate = (compiled, quota, ensureZeroSum = false) => {
|
|
155
|
-
if (!Array.isArray(compiled) || compiled.length === 0) {
|
|
156
|
-
throw new Error('no contract calls to validate');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Since QLDB's max batch size is 40, each contract call will update 1 account
|
|
160
|
-
// We need to set a smaller hard limit to avoid QLDB errors
|
|
161
|
-
if (compiled.length > 30) {
|
|
162
|
-
throw new Error('too much contract calls found, max is 15');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// to track consumption
|
|
166
|
-
let remaining = new BN(quota ? quota.value : '0');
|
|
167
|
-
const tokenRemaining = Object.keys(quota?.tokens ? quota.tokens : {}).reduce((acc, x) => {
|
|
168
|
-
acc[x] = new BN(quota.tokens[x]);
|
|
169
|
-
return acc;
|
|
170
|
-
}, {});
|
|
171
|
-
|
|
172
|
-
for (const item of compiled) {
|
|
173
|
-
const { call, args } = item;
|
|
174
|
-
if (call === 'transfer') {
|
|
175
|
-
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
176
|
-
const consumption = validateAmount(args.amount);
|
|
177
|
-
if (consumption.lte(ZERO)) {
|
|
178
|
-
throw new Error('transfer amount must be greater than 0');
|
|
179
|
-
}
|
|
180
|
-
if (quota) {
|
|
181
|
-
if (remaining.sub(consumption).isNeg()) {
|
|
182
|
-
throw new Error('transfer exceeded quota');
|
|
183
|
-
}
|
|
184
|
-
remaining = remaining.sub(consumption);
|
|
185
|
-
}
|
|
186
|
-
} else if (call === 'transferToken') {
|
|
187
|
-
validateDid(args.tokenAddress, [types.RoleType.ROLE_TOKEN]);
|
|
188
|
-
validateDid(args.to, TOKEN_RECEIVER_ROLE_TYPES);
|
|
189
|
-
|
|
190
|
-
const token = args.tokenAddress;
|
|
191
|
-
const consumption = validateAmount(args.amount);
|
|
192
|
-
if (consumption.lte(ZERO)) {
|
|
193
|
-
throw new Error('token transfer amount must be greater than 0');
|
|
194
|
-
}
|
|
195
|
-
if (quota) {
|
|
196
|
-
if (!tokenRemaining[token] || tokenRemaining[token].sub(consumption).isNeg()) {
|
|
197
|
-
throw new Error(`transferToken for ${args.tokenAddress} exceeded quota`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
tokenRemaining[token] = tokenRemaining[token].sub(consumption);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (ensureZeroSum) {
|
|
206
|
-
if (remaining.isZero() === false) {
|
|
207
|
-
throw new Error('transfer violates zero sum');
|
|
208
|
-
}
|
|
209
|
-
const invalidToken = Object.keys(tokenRemaining).find((x) => tokenRemaining[x].isZero() === false);
|
|
210
|
-
if (invalidToken) {
|
|
211
|
-
throw new Error(`transferToken for ${invalidToken} violates zero sum`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return true;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// merge duplicate receiver transfers if they exist
|
|
219
|
-
const merge = (compiled) => {
|
|
220
|
-
const mergeAndSum = (items, uniqKey, sumBy) => {
|
|
221
|
-
const result = {};
|
|
222
|
-
const keys = Array.isArray(uniqKey) ? uniqKey : [uniqKey];
|
|
223
|
-
keys.sort();
|
|
224
|
-
for (const item of items) {
|
|
225
|
-
const key = keys.map((x) => get(item, x)).join('-');
|
|
226
|
-
if (result[key] === undefined) {
|
|
227
|
-
result[key] = item;
|
|
228
|
-
} else {
|
|
229
|
-
const old = new BN(get(result[key], sumBy));
|
|
230
|
-
const delta = new BN(get(item, sumBy));
|
|
231
|
-
set(result[key], sumBy, old.add(delta).toString());
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return Object.values(result);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return [
|
|
239
|
-
...mergeAndSum(
|
|
240
|
-
compiled.filter((x) => x.call === 'transfer'),
|
|
241
|
-
'args.to',
|
|
242
|
-
'args.amount'
|
|
243
|
-
),
|
|
244
|
-
...mergeAndSum(
|
|
245
|
-
compiled.filter((x) => x.call === 'transferToken'),
|
|
246
|
-
['args.tokenAddress', 'args.to'],
|
|
247
|
-
'args.amount'
|
|
248
|
-
),
|
|
249
|
-
];
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const getQuota = (input = {}) => ({
|
|
253
|
-
value: input.value || '0',
|
|
254
|
-
tokens: (input.tokens || []).reduce((acc, x) => {
|
|
255
|
-
acc[x.address] = x.value;
|
|
256
|
-
return acc;
|
|
257
|
-
}, {}),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
module.exports = { compile, validate, merge, getQuota, SUPPORTED_CALLS };
|