@ocap/contract 1.28.8 → 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 CHANGED
@@ -1,6 +1,5 @@
1
1
  ![nft](https://www.arcblock.io/.netlify/functions/badge/?text=OCAP+Contract)
2
2
 
3
- [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
4
3
  [![docs](https://img.shields.io/badge/powered%20by-arcblock-green.svg)](https://docs.arcblock.io)
5
4
  [![Gitter](https://badges.gitter.im/ArcBlock/community.svg)](https://gitter.im/ArcBlock/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
6
5
 
@@ -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;
@@ -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.28.8",
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.28.8",
22
- "@ocap/mcrypto": "1.28.8",
23
- "@ocap/util": "1.28.8",
24
- "debug": "^4.3.6",
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.21"
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.js",
64
- "gen-docs": "jsdoc2md lib/index.js > docs/README.md",
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 };