@ocap/asset 1.6.10

Sign up to get free protection for your applications and to get access to all the features.
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,29 @@
1
+ ![nft](https://www.arcblock.io/.netlify/functions/badge/?text=Asset+Factory)
2
+
3
+ [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
4
+ [![docs](https://img.shields.io/badge/powered%20by-arcblock-green.svg)](https://docs.arcblock.io)
5
+ [![Gitter](https://badges.gitter.im/ArcBlock/community.svg)](https://gitter.im/ArcBlock/community?utm_source=badge\&utm_medium=badge\&utm_campaign=pr-badge)
6
+
7
+ > Utility to create standard assets that can be rendered in [DID Wallet](https://www.abtwallet.io)
8
+
9
+
10
+ ## Table of Contents
11
+
12
+ * [Install](#install)
13
+ * [Usage](#usage)
14
+
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npm install @ocap/asset
20
+ // or
21
+ yarn add @ocap/asset
22
+ ```
23
+
24
+
25
+ ## Usage
26
+
27
+ ```js
28
+ // TODO
29
+ ```
package/lib/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ // Generate by [js2dts@0.3.3](https://github.com/whxaxes/js2dts#readme)
2
+
3
+ declare const _Lib: _Lib.T107;
4
+ declare namespace _Lib {
5
+ export interface T101 {
6
+ factory: any;
7
+ inputs: any;
8
+ owner: string;
9
+ issuer: any;
10
+ }
11
+ export interface T102 {
12
+ asset: any;
13
+ address: string;
14
+ }
15
+ export interface T103 {
16
+ address: any;
17
+ output: any;
18
+ data: any;
19
+ }
20
+ export interface T105 {
21
+ id: any;
22
+ pk: string;
23
+ name: any;
24
+ }
25
+ export interface T106 {
26
+ address: string;
27
+ issuer: _Lib.T105;
28
+ variables: any;
29
+ asset: any;
30
+ }
31
+ export interface T107 {
32
+ isValidFactory: (props: any) => boolean;
33
+ isValidHook: (hook: any, quota: any, throwOnError?: boolean) => boolean;
34
+ findPrerenderKeys: (obj: any, keyword: string) => string;
35
+ mintFromFactory: (T100: _Lib.T101) => _Lib.T102;
36
+ formatFactoryState: (state: any) => _Lib.T103;
37
+ preMintFromFactory: (T104: _Lib.T101) => _Lib.T106;
38
+ }
39
+ }
40
+ export = _Lib;
package/lib/index.js ADDED
@@ -0,0 +1,318 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ const get = require('lodash/get');
3
+ const set = require('lodash/set');
4
+ const uniq = require('lodash/uniq');
5
+ const uniqBy = require('lodash/uniqBy');
6
+ const flatten = require('flat');
7
+ const mustache = require('mustache');
8
+ const isEmpty = require('empty-value');
9
+ const isAbsoluteUrl = require('is-absolute-url');
10
+
11
+ const cloneDeep = require('lodash/cloneDeep');
12
+ const { toTypeInfo, fromPublicKeyHash } = require('@arcblock/did');
13
+ const { verify, stableStringify, proofTypes } = require('@arcblock/vc');
14
+ const { toAssetAddress } = require('@arcblock/did-util');
15
+ const { types } = require('@ocap/mcrypto');
16
+ const { BN, toBase58, toBase64 } = require('@ocap/util');
17
+ const { compile, validate, getQuota } = require('@ocap/contract');
18
+
19
+ const debug = require('debug')(require('../package.json').name);
20
+
21
+ const SUPPORTED_HOOK_NAMES = ['mint', 'postMint'];
22
+ const SUPPORTED_HOOK_TYPES = ['contract', 'url'];
23
+
24
+ const isValidHook = (hook, quota, throwOnError = false) => {
25
+ if (SUPPORTED_HOOK_TYPES.includes(hook.type) === false) {
26
+ return false;
27
+ }
28
+ if (SUPPORTED_HOOK_NAMES.includes(hook.name) === false) {
29
+ return false;
30
+ }
31
+
32
+ if (hook.type === 'url') {
33
+ return isAbsoluteUrl(hook.hook);
34
+ }
35
+
36
+ if (hook.type === 'contract') {
37
+ try {
38
+ const compiled = compile(hook.hook);
39
+ validate(compiled, quota);
40
+ return true;
41
+ } catch (err) {
42
+ if (process.env.NODE_ENV !== 'test') {
43
+ console.error('invalid contract hook', err.message);
44
+ }
45
+
46
+ if (throwOnError) {
47
+ throw new Error(`Factory hook ${hook.name} is invalid: ${err.message}`);
48
+ }
49
+
50
+ return false;
51
+ }
52
+ }
53
+
54
+ return false;
55
+ };
56
+
57
+ const isValidFactory = (props) => {
58
+ if (!props) {
59
+ throw new Error('Factory props should not be empty');
60
+ }
61
+
62
+ // required props
63
+ const missingProp = ['name', 'description', 'input', 'output', 'settlement'].find((x) => !props[x]);
64
+ if (missingProp) {
65
+ throw new Error(`Factory ${missingProp} prop should not be empty`);
66
+ }
67
+
68
+ if (['instant', 'periodic'].includes(props.settlement) === false) {
69
+ throw new Error('Factory settlement prop should only be instant or periodic');
70
+ }
71
+
72
+ // input.tokens and input.assets should not be empty
73
+ if (['value', 'tokens', 'assets'].every((x) => isEmpty(props.input[x]))) {
74
+ throw new Error('Factory input should contain at least one token or asset');
75
+ }
76
+
77
+ if (uniqBy(props.input.tokens, 'address').length !== props.input.tokens.length) {
78
+ throw new Error('Factory token input should not contains duplicate address');
79
+ }
80
+
81
+ if (uniq(props.input.assets).length !== props.input.assets.length) {
82
+ throw new Error('Factory asset input should not contains duplicate address');
83
+ }
84
+
85
+ for (const token of props.input.tokens) {
86
+ try {
87
+ // eslint-disable-next-line no-new
88
+ new BN(token.value);
89
+ } catch (err) {
90
+ throw new Error(`Factory token ${token.address} is invalid: token value is not valid big number`);
91
+ }
92
+ }
93
+
94
+ // validate output
95
+ if (!props.output || typeof props.output !== 'object') {
96
+ throw new Error('Factory output should be an object');
97
+ }
98
+ try {
99
+ // should be a valid mustache template when serialized as json
100
+ const template = JSON.stringify(props.output);
101
+ mustache.parse(template);
102
+ } catch (err) {
103
+ throw new Error('Factory output should be a valid mustache template when serialized as json');
104
+ }
105
+
106
+ // validate hooks
107
+ const quota = getQuota(props.input);
108
+ if (Array.isArray(props.hooks)) {
109
+ const invalidHook = props.hooks.find((x) => isValidHook(x, quota, true) === false);
110
+ if (invalidHook) {
111
+ throw new Error(`Factory hook ${invalidHook.name} is invalid`);
112
+ }
113
+ }
114
+
115
+ // ensure input and hook are zero-sum for factories that consumes token
116
+ if (props.settlement === 'instant') {
117
+ if (quota.value <= 0 && Object.keys(quota.tokens).every((x) => quota[x] <= 0)) {
118
+ return true;
119
+ }
120
+
121
+ if (isEmpty(props.hooks)) {
122
+ throw new Error('Factory hooks should not be empty for instant settlement');
123
+ }
124
+
125
+ const mintHook = props.hooks.find((x) => x.name === 'mint');
126
+ if (!mintHook) {
127
+ throw new Error('Factory hook mint should not be empty for instant settlement that consumes token');
128
+ }
129
+
130
+ try {
131
+ const compiled = compile(mintHook.hook);
132
+ validate(compiled, quota, true);
133
+ } catch (err) {
134
+ throw new Error(`Factory hook mint is invalid: ${err.message}`);
135
+ }
136
+ }
137
+
138
+ return true;
139
+ };
140
+
141
+ /**
142
+ * Find credentialSubject path in the object
143
+ * Because they need prerender
144
+ *
145
+ * @param {object} obj
146
+ * @param {string} keyword
147
+ * @return {string} list of keys
148
+ */
149
+ const findPrerenderKeys = (obj, keyword) => {
150
+ const flatObj = flatten(obj, { safe: true });
151
+ const keys = Object.keys(flatObj)
152
+ .map((x) => x.split('.'))
153
+ .filter((x) => x.includes(keyword))
154
+ .map((x) => x.slice(0, x.lastIndexOf(keyword) + 1))
155
+ .sort((a, b) => b.length - a.length)
156
+ .map((x) => x.join('.'));
157
+
158
+ return uniq(keys);
159
+ };
160
+
161
+ /**
162
+ * Mint from an asset factory, used on server side
163
+ *
164
+ * @param {object} params { factory, inputs, issuer }
165
+ * @param {object} params.factory factory object
166
+ * @param {object} params.inputs factory input variables
167
+ * @param {string} params.owner owner did for the new asset
168
+ * @param {object} params.issuer issuer object
169
+ */
170
+ const mintFromFactory = ({ factory, inputs, owner, issuer }) => {
171
+ const { output, address: factoryAddress, numMinted, data } = factory;
172
+ debug(
173
+ 'mintFromFactory.args',
174
+ JSON.stringify({ output, factoryAddress, numMinted, inputs, owner, issuer, data }, null, 2)
175
+ );
176
+ const asset = JSON.parse(
177
+ mustache.render(JSON.stringify(output), {
178
+ input: inputs,
179
+ data: data.value || data,
180
+ ctx: { factory: factoryAddress, id: numMinted + 1, owner, issuer },
181
+ })
182
+ );
183
+
184
+ debug('mintFromFactory.result', JSON.stringify(asset, null, 2));
185
+ const address = toAssetAddress(asset);
186
+ return { asset, address };
187
+ };
188
+
189
+ /**
190
+ * Simulate minting from an asset factory, used for client side
191
+ *
192
+ * @param {object} params { factory, inputs, issuer }
193
+ * @param {object} params.factory factory object
194
+ * @param {object} params.inputs factory input variables
195
+ * @param {string} params.owner owner did for the new asset
196
+ * @param {object} params.issuer factory issuer wallet and name
197
+ */
198
+ const preMintFromFactory = ({ factory, inputs, owner, issuer }) => {
199
+ if (Object.keys(inputs).some((x) => typeof inputs[x] !== 'string')) {
200
+ throw new Error('Failed to mint asset from factory: input values must be strings');
201
+ }
202
+
203
+ let asset = null;
204
+ const { output, numMinted, address: factoryAddress, data } = factory;
205
+ const { wallet, name } = issuer;
206
+
207
+ debug(
208
+ 'preMintFromFactory.args',
209
+ JSON.stringify({ output, factoryAddress, numMinted, inputs, owner, issuer, data }, null, 2)
210
+ );
211
+
212
+ const extra = {};
213
+ const issuerObject = { id: wallet.address, pk: toBase58(wallet.publicKey), name };
214
+
215
+ const render = (templateObject) =>
216
+ JSON.parse(
217
+ mustache.render(JSON.stringify(templateObject), {
218
+ input: { ...inputs, ...extra },
219
+ data: data.value || data,
220
+ ctx: { factory: factoryAddress, id: numMinted + 1, owner, issuer: issuerObject },
221
+ })
222
+ );
223
+
224
+ const template = cloneDeep(output);
225
+
226
+ // prerender credentialSubjects if they exists
227
+ // then populate their ids
228
+ const prerenderKeys = findPrerenderKeys(template, 'credentialSubject');
229
+ if (prerenderKeys.length) {
230
+ extra.issuanceDate = new Date().toISOString();
231
+
232
+ for (const key of prerenderKeys) {
233
+ const subjectTemplate = get(template, key);
234
+ const subjectObject = Array.isArray(subjectTemplate)
235
+ ? subjectTemplate.map((x) => render(x))
236
+ : render(subjectTemplate);
237
+ set(template, key, subjectObject);
238
+
239
+ // calculate credential id from credentialSubject
240
+ const vcRootPath = key.split('.').slice(0, -1).join('.');
241
+ const vcIdPath = vcRootPath.split('.').concat(['id']).join('.');
242
+ const typeInfo = toTypeInfo(issuerObject.id);
243
+ const vcType = { ...typeInfo, role: types.RoleType.ROLE_VC };
244
+ const vcId = fromPublicKeyHash(wallet.hash(stableStringify(subjectObject)), vcType);
245
+
246
+ extra.id = vcId;
247
+ extra.proofType = proofTypes[typeInfo.pk];
248
+
249
+ // technically we do not support nested vc when minting
250
+ // But it is possible to support multiple credentialSubjects when minting
251
+ set(template, vcIdPath, vcId);
252
+
253
+ // Generate proof signatures
254
+ if (!proofTypes[typeInfo.pk]) {
255
+ throw new Error('Unsupported signer type when create verifiable credential');
256
+ }
257
+
258
+ let vcObj = render(get(template, vcRootPath));
259
+ delete vcObj.proof;
260
+ const vcStr = stableStringify(vcObj);
261
+ const signature = toBase64(wallet.sign(vcStr));
262
+
263
+ vcObj.proof = {
264
+ type: proofTypes[typeInfo.pk],
265
+ created: extra.issuanceDate,
266
+ proofPurpose: 'assertionMethod',
267
+ jws: signature,
268
+ };
269
+
270
+ extra.signature = signature;
271
+
272
+ try {
273
+ // Simulate minting from start, so that we can ensure the validity of minted asset
274
+ asset = render(cloneDeep(output));
275
+ vcObj = get(asset, vcRootPath);
276
+
277
+ debug('preMintFromFactory.result', JSON.stringify(asset, null, 2));
278
+ verify({ vc: vcObj, trustedIssuers: [issuerObject.id], ownerDid: owner, ignoreExpired: true });
279
+ } catch (err) {
280
+ console.error(err);
281
+ throw new Error('Failed to mint asset from factory: invalid verifiable credential minted');
282
+ }
283
+ }
284
+ } else {
285
+ // populate other variables into the whole output
286
+ asset = render(template);
287
+ debug('preMintFromFactory.result', JSON.stringify(asset, null, 2));
288
+ }
289
+
290
+ // calculate address
291
+ const address = toAssetAddress(asset);
292
+
293
+ // return extra inputs
294
+ return { address, issuer: issuerObject, variables: { ...inputs, ...extra }, asset };
295
+ };
296
+
297
+ const formatFactoryState = (state) => {
298
+ const { address, output, data } = state;
299
+ const outputX = cloneDeep(output);
300
+
301
+ outputX.data.value = JSON.parse(outputX.data.value);
302
+ outputX.data.type = outputX.data.typeUrl;
303
+
304
+ return {
305
+ address,
306
+ output: outputX,
307
+ data: JSON.parse(data.value),
308
+ };
309
+ };
310
+
311
+ module.exports = {
312
+ isValidFactory,
313
+ isValidHook,
314
+ findPrerenderKeys,
315
+ mintFromFactory,
316
+ formatFactoryState,
317
+ preMintFromFactory,
318
+ };
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@ocap/asset",
3
+ "description": "Utility to work with asset and factory on ArcBlock blockchain",
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
+ "@arcblock/did-util": "1.6.10",
23
+ "@arcblock/vc": "1.6.10",
24
+ "@ocap/contract": "1.6.10",
25
+ "@ocap/mcrypto": "1.6.10",
26
+ "@ocap/util": "1.6.10",
27
+ "@ocap/wallet": "1.6.10",
28
+ "debug": "^4.3.3",
29
+ "empty-value": "^1.0.1",
30
+ "flat": "^5.0.2",
31
+ "is-absolute-url": "^3.0.3",
32
+ "json-stable-stringify": "^1.0.1",
33
+ "lodash": "^4.17.21",
34
+ "mustache": "^4.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "jest": "^27.3.1",
38
+ "remark-cli": "^10.0.1",
39
+ "remark-preset-github": "^4.0.1"
40
+ },
41
+ "remarkConfig": {
42
+ "plugins": [
43
+ "preset-github",
44
+ [
45
+ {
46
+ "repository": "ArcBlock/asset-chain"
47
+ }
48
+ ]
49
+ ]
50
+ },
51
+ "homepage": "https://github.com/ArcBlock/asset-chain/tree/master/core/asset",
52
+ "keywords": [
53
+ "blockchain",
54
+ "arcblock",
55
+ "sdk",
56
+ "nodejs"
57
+ ],
58
+ "license": "Apache-2.0",
59
+ "main": "./lib/index.js",
60
+ "files": [
61
+ "lib"
62
+ ],
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "https://github.com/ArcBlock/asset-chain/tree/master/core/asset"
66
+ },
67
+ "scripts": {
68
+ "lint": "eslint lib tests",
69
+ "lint:fix": "eslint --fix lib tests",
70
+ "docs": "yarn gen-dts && yarn gen-docs && yarn cleanup-docs && yarn format-docs",
71
+ "cleanup-docs": "node ../../scripts/cleanup-docs.js docs/README.md $npm_package_name",
72
+ "gen-dts": "j2d lib/index.js",
73
+ "gen-docs": "jsdoc2md lib/index.js > docs/README.md",
74
+ "format-docs": "remark . -o",
75
+ "test": "jest --forceExit --detectOpenHandles",
76
+ "coverage": "yarn test -- --coverage"
77
+ },
78
+ "gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
79
+ }