@ocap/asset 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 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
+ }