@blocklet/meta 1.8.13 → 1.8.16
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/lib/info.js +4 -0
- package/lib/parse-navigation.js +8 -1
- package/lib/payment/index.js +7 -0
- package/lib/{payment.js → payment/v1.js} +1 -1
- package/lib/payment/v2.js +619 -0
- package/lib/schema.js +35 -3
- package/lib/util-meta.js +173 -0
- package/lib/util.js +13 -0
- package/package.json +19 -12
package/lib/info.js
CHANGED
|
@@ -12,17 +12,20 @@ module.exports = (state, nodeSk, { returnWallet = true } = {}) => {
|
|
|
12
12
|
|
|
13
13
|
const customDescription = envs.find((x) => x.key === 'BLOCKLET_APP_DESCRIPTION');
|
|
14
14
|
const customPassportColor = envs.find((x) => x.key === 'BLOCKLET_PASSPORT_COLOR');
|
|
15
|
+
const customAppUrl = envs.find((x) => x.key === 'BLOCKLET_APP_URL');
|
|
15
16
|
|
|
16
17
|
const { did } = state.meta;
|
|
17
18
|
const name = getDisplayName(state);
|
|
18
19
|
const description = get(customDescription, 'value', state.meta.description);
|
|
19
20
|
const passportColor = get(customPassportColor, 'value', 'auto');
|
|
21
|
+
const appUrl = get(customAppUrl, 'value', '');
|
|
20
22
|
|
|
21
23
|
if (!returnWallet) {
|
|
22
24
|
return {
|
|
23
25
|
did,
|
|
24
26
|
name,
|
|
25
27
|
description,
|
|
28
|
+
appUrl,
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -54,6 +57,7 @@ module.exports = (state, nodeSk, { returnWallet = true } = {}) => {
|
|
|
54
57
|
name,
|
|
55
58
|
description,
|
|
56
59
|
passportColor,
|
|
60
|
+
appUrl,
|
|
57
61
|
wallet,
|
|
58
62
|
};
|
|
59
63
|
};
|
package/lib/parse-navigation.js
CHANGED
|
@@ -140,7 +140,7 @@ const doParseNavigation = (navigation, blocklet, prefix = '/', _level = 1) => {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
if (childNav.role) {
|
|
143
|
-
item.role = item.
|
|
143
|
+
item.role = item.role || childNav.role;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
if (childNav.section) {
|
|
@@ -151,6 +151,8 @@ const doParseNavigation = (navigation, blocklet, prefix = '/', _level = 1) => {
|
|
|
151
151
|
result.push(item);
|
|
152
152
|
} else {
|
|
153
153
|
// child declares multiple menus
|
|
154
|
+
const groupSection = childNavigation[0].section || [];
|
|
155
|
+
|
|
154
156
|
const list = doParseNavigation(
|
|
155
157
|
childNavigation,
|
|
156
158
|
child,
|
|
@@ -161,6 +163,11 @@ const doParseNavigation = (navigation, blocklet, prefix = '/', _level = 1) => {
|
|
|
161
163
|
if (_level === 1) { // eslint-disable-line
|
|
162
164
|
// primary menu
|
|
163
165
|
const item = cloneDeep(itemProto);
|
|
166
|
+
|
|
167
|
+
if (groupSection.length) {
|
|
168
|
+
item.section = item.section || groupSection;
|
|
169
|
+
}
|
|
170
|
+
|
|
164
171
|
item.items = list;
|
|
165
172
|
result.push({
|
|
166
173
|
...item,
|
|
@@ -3,7 +3,7 @@ const joinURL = require('url-join');
|
|
|
3
3
|
const { BN } = require('@ocap/util');
|
|
4
4
|
const { isValidFactory } = require('@ocap/asset');
|
|
5
5
|
const { toFactoryAddress } = require('@arcblock/did-util');
|
|
6
|
-
const { getBlockletPurchaseTemplate } = require('
|
|
6
|
+
const { getBlockletPurchaseTemplate } = require('../nft-templates');
|
|
7
7
|
|
|
8
8
|
const createShareContract = ({ tokens = [], shares = [] }) => {
|
|
9
9
|
const zeroBN = new BN(0);
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const debug = require('debug')('@blocklet/meta:payment');
|
|
4
|
+
const joinURL = require('url-join');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const stableStringify = require('json-stable-stringify');
|
|
7
|
+
const get = require('lodash/get');
|
|
8
|
+
const pick = require('lodash/pick');
|
|
9
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
10
|
+
const { BN, fromTokenToUnit, fromUnitToToken } = require('@ocap/util');
|
|
11
|
+
const { isValidFactory } = require('@ocap/asset');
|
|
12
|
+
const { fromPublicKey } = require('@ocap/wallet');
|
|
13
|
+
const { toFactoryAddress } = require('@arcblock/did-util');
|
|
14
|
+
const { toTypeInfo } = require('@arcblock/did');
|
|
15
|
+
const { BLOCKLET_STORE_META_PATH } = require('@abtnode/constant');
|
|
16
|
+
const { getBlockletPurchaseTemplate } = require('../nft-templates');
|
|
17
|
+
const { validateMeta } = require('../validate');
|
|
18
|
+
const { isComponentBlocklet, isFreeComponent, isFreeBlocklet } = require('../util');
|
|
19
|
+
const { getBlockletMetaFromUrls, getSourceUrlsFromConfig } = require('../util-meta');
|
|
20
|
+
|
|
21
|
+
const VERSION = '2.0.0';
|
|
22
|
+
|
|
23
|
+
const ZeroBN = new BN(0);
|
|
24
|
+
const defaultDecimals = 1e6; // we only support 6 decimals on share ratio
|
|
25
|
+
const defaultDecimalsBN = new BN(defaultDecimals);
|
|
26
|
+
|
|
27
|
+
const safeMul = (a, b) =>
|
|
28
|
+
Number(
|
|
29
|
+
fromUnitToToken(
|
|
30
|
+
fromTokenToUnit(a)
|
|
31
|
+
.mul(new BN(b * defaultDecimals))
|
|
32
|
+
.div(defaultDecimalsBN)
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const md5 = (str) => crypto.createHash('md5').update(str).digest('hex');
|
|
37
|
+
|
|
38
|
+
const getStoreInfo = async (url) => {
|
|
39
|
+
const storeMetaUrl = joinURL(new URL(url).origin, BLOCKLET_STORE_META_PATH);
|
|
40
|
+
const { data: info } = await axios.get(storeMetaUrl, { timeout: 8000 });
|
|
41
|
+
return info;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{
|
|
46
|
+
* meta: Object
|
|
47
|
+
* storeInfo: Object
|
|
48
|
+
* storeUrl: string
|
|
49
|
+
* children: Array<Component>
|
|
50
|
+
* }} Component
|
|
51
|
+
*
|
|
52
|
+
* @param {BlockletMeta} inputMeta
|
|
53
|
+
* @param {{
|
|
54
|
+
* ancestors: Array<{BlockletMeta}>
|
|
55
|
+
* bundles: {
|
|
56
|
+
* <bundleName>: <storeId>
|
|
57
|
+
* }
|
|
58
|
+
* }} context
|
|
59
|
+
*
|
|
60
|
+
* @returns {Array<Component>}
|
|
61
|
+
*/
|
|
62
|
+
const _getComponents = async (inputMeta, context = {}) => {
|
|
63
|
+
// FIXME 是否需要验证: 在同一个链上; 重复的 component
|
|
64
|
+
const { ancestors = [], bundles = {} } = context;
|
|
65
|
+
|
|
66
|
+
// check ancestor length
|
|
67
|
+
if (ancestors.length > 40) {
|
|
68
|
+
throw new Error('The depth of component should not exceed 40');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const configs = inputMeta.children || [];
|
|
72
|
+
|
|
73
|
+
if (!configs || !configs.length) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const children = [];
|
|
78
|
+
|
|
79
|
+
for (const config of configs) {
|
|
80
|
+
// get component meta
|
|
81
|
+
const urls = getSourceUrlsFromConfig(config);
|
|
82
|
+
let meta;
|
|
83
|
+
let url;
|
|
84
|
+
try {
|
|
85
|
+
const res = await getBlockletMetaFromUrls(urls, {
|
|
86
|
+
returnUrl: true,
|
|
87
|
+
validateFn: (m) => validateMeta(m),
|
|
88
|
+
ensureTarball: false,
|
|
89
|
+
});
|
|
90
|
+
meta = res.meta;
|
|
91
|
+
url = res.url;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(`Failed get component meta: ${config.title || config.name}: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// check is component
|
|
97
|
+
if (!isComponentBlocklet(meta)) {
|
|
98
|
+
throw new Error(`The blocklet cannot be a component: ${meta.title}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// check circular dependencies
|
|
102
|
+
if (ancestors.map((x) => x.meta?.did).indexOf(meta.did) > -1) {
|
|
103
|
+
throw new Error('Blocklet components have circular dependencies');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// generate child
|
|
107
|
+
const child = {
|
|
108
|
+
meta,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// child store info
|
|
112
|
+
if (config.source.store) {
|
|
113
|
+
const storeInfo = await getStoreInfo(url);
|
|
114
|
+
|
|
115
|
+
// check uniq bundle did in different stores
|
|
116
|
+
if (!bundles[child.meta.did]) {
|
|
117
|
+
bundles[child.meta.did] = storeInfo.id;
|
|
118
|
+
} else if (bundles[child.meta.did] !== storeInfo.id) {
|
|
119
|
+
throw new Error('Bundles with the same did cannot in different stores');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
child.storeInfo = storeInfo;
|
|
123
|
+
child.storeUrl = new URL(url).origin;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// child children
|
|
127
|
+
child.children = await _getComponents(meta, {
|
|
128
|
+
ancestors: [...ancestors, { meta }],
|
|
129
|
+
bundles,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
children.push(child);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return children;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @typedef {{
|
|
140
|
+
* id: string
|
|
141
|
+
* pk: string
|
|
142
|
+
* url: string
|
|
143
|
+
* components: Array<{did: string, version: string}>
|
|
144
|
+
* }} Store
|
|
145
|
+
* @param {Array<Component>} components
|
|
146
|
+
* @param {Array<Store>} _stores
|
|
147
|
+
* @returns {Array<Store>}
|
|
148
|
+
*/
|
|
149
|
+
const _getStores = (components, _stores = []) => {
|
|
150
|
+
for (const { meta, storeInfo, storeUrl, children } of components) {
|
|
151
|
+
if (storeInfo && (!isFreeBlocklet(meta) || !isFreeComponent(meta))) {
|
|
152
|
+
const store = _stores.find((x) => x.id === storeInfo.id);
|
|
153
|
+
if (!store) {
|
|
154
|
+
_stores.push({
|
|
155
|
+
id: storeInfo.id,
|
|
156
|
+
pk: storeInfo.pk,
|
|
157
|
+
url: storeUrl,
|
|
158
|
+
components: [{ did: meta.did, version: meta.version }],
|
|
159
|
+
});
|
|
160
|
+
} else if (!store.components.some((x) => x.did === meta.did && x.version === meta.version)) {
|
|
161
|
+
store.components.push({ did: meta.did, version: meta.version });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (children && children.length > 0) {
|
|
166
|
+
_getStores(children, _stores);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return _stores;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getComponents = async (inputMeta) => {
|
|
174
|
+
const components = await _getComponents(inputMeta);
|
|
175
|
+
const stores = await _getStores(components);
|
|
176
|
+
return { components, stores };
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const getPriceTokens = async (meta, ocapClient) => {
|
|
180
|
+
const priceTokens = cloneDeep(get(meta, 'payment.price', []));
|
|
181
|
+
for (const token of priceTokens) {
|
|
182
|
+
// eslint-disable-next-line no-await-in-loop
|
|
183
|
+
const { state } = await ocapClient.getTokenState({ address: token.address });
|
|
184
|
+
if (!state) {
|
|
185
|
+
throw new Error(`Token specified in blocklet meta was not found on chain: ${token.address}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
token.decimal = state.decimal;
|
|
189
|
+
}
|
|
190
|
+
return priceTokens;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getChildShare = (childMeta, parentPrice) => {
|
|
194
|
+
if (!childMeta?.payment?.componentPrice) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const priceList = childMeta.payment.componentPrice;
|
|
199
|
+
|
|
200
|
+
let price = 0;
|
|
201
|
+
|
|
202
|
+
for (const { type, value, parentPriceRange } of priceList) {
|
|
203
|
+
const isDefault = !parentPriceRange || !parentPriceRange.length;
|
|
204
|
+
const skip = isDefault && price !== 0;
|
|
205
|
+
const inRange =
|
|
206
|
+
isDefault || (parentPriceRange && parentPrice >= parentPriceRange[0] && parentPrice <= parentPriceRange[1]);
|
|
207
|
+
|
|
208
|
+
if (!skip && inRange) {
|
|
209
|
+
if (type === 'fixed') {
|
|
210
|
+
price = value;
|
|
211
|
+
} else if (type === 'percentage') {
|
|
212
|
+
price = safeMul(parentPrice, value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return price;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @returns {Array<{
|
|
222
|
+
* tokenAddress: string
|
|
223
|
+
* accountAddress: string
|
|
224
|
+
* amount: BN
|
|
225
|
+
* }>}
|
|
226
|
+
*/
|
|
227
|
+
const getTokenTransfers = ({ priceToken, shares = [], components = [] }) => {
|
|
228
|
+
// check share
|
|
229
|
+
const shareSum = shares.reduce((sum, x) => sum + x.value, 0);
|
|
230
|
+
if (shareSum > 1) {
|
|
231
|
+
throw new Error('payment.share invalid: share sum should not be greater than 1');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { value: price } = priceToken;
|
|
235
|
+
|
|
236
|
+
let parentShareBN = fromTokenToUnit(price, priceToken.decimal);
|
|
237
|
+
|
|
238
|
+
const contracts = [];
|
|
239
|
+
|
|
240
|
+
for (const child of components) {
|
|
241
|
+
if (!isFreeComponent(child.meta)) {
|
|
242
|
+
// // check same token
|
|
243
|
+
const [token] = child.meta.payment.price || [];
|
|
244
|
+
if (token && token.address !== priceToken.address) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`component price token is not same with app price token: ${child.meta.title || child.meta.name}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const childShare = getChildShare(child.meta, price);
|
|
251
|
+
|
|
252
|
+
parentShareBN = parentShareBN.sub(fromTokenToUnit(childShare, priceToken.decimal));
|
|
253
|
+
|
|
254
|
+
if (parentShareBN.lt(ZeroBN)) {
|
|
255
|
+
throw new Error('Price is not enough for component sharing');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const componentContracts = getTokenTransfers({
|
|
259
|
+
priceToken: { ...priceToken, value: childShare },
|
|
260
|
+
shares: child.meta.payment.share,
|
|
261
|
+
components: child.children || [],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
contracts.push(...componentContracts);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
shares.forEach(({ name, address: accountAddress, value: ratio }) => {
|
|
269
|
+
contracts.push({
|
|
270
|
+
tokenAddress: priceToken.address,
|
|
271
|
+
accountName: name,
|
|
272
|
+
accountAddress,
|
|
273
|
+
amount: parentShareBN.mul(new BN(ratio * defaultDecimals)).div(defaultDecimalsBN),
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const mergedContracts = [];
|
|
278
|
+
|
|
279
|
+
contracts.forEach((x) => {
|
|
280
|
+
const index = mergedContracts.findIndex(
|
|
281
|
+
(y) => y.tokenAddress === x.tokenAddress && y.accountAddress === x.accountAddress
|
|
282
|
+
);
|
|
283
|
+
if (index > -1) {
|
|
284
|
+
mergedContracts[index].amount = mergedContracts[index].amount.add(x.amount);
|
|
285
|
+
} else {
|
|
286
|
+
mergedContracts.push(x);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return mergedContracts;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const getContract = async ({ meta, priceTokens, components }) => {
|
|
294
|
+
const shares = meta.payment.share || [];
|
|
295
|
+
|
|
296
|
+
const [priceToken] = priceTokens;
|
|
297
|
+
|
|
298
|
+
const contracts = getTokenTransfers({ priceToken, shares, components });
|
|
299
|
+
|
|
300
|
+
const code = contracts
|
|
301
|
+
.map((x) => `transferToken('${x.tokenAddress}','${x.accountAddress}','${x.amount.toString()}')`)
|
|
302
|
+
.join(';\n');
|
|
303
|
+
|
|
304
|
+
const shareList = contracts.map((x) => ({
|
|
305
|
+
...x,
|
|
306
|
+
amount: fromUnitToToken(x.amount, priceToken.decimal),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
code,
|
|
311
|
+
shares: shareList,
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* we need to ensure that blocklet purchase factory does not change across changes
|
|
317
|
+
*
|
|
318
|
+
* @typedef {{
|
|
319
|
+
* data: {
|
|
320
|
+
* type: 'json'
|
|
321
|
+
* value: {
|
|
322
|
+
* did: string
|
|
323
|
+
* url: string
|
|
324
|
+
* name: string
|
|
325
|
+
* version: string
|
|
326
|
+
* payment: {
|
|
327
|
+
* version: string
|
|
328
|
+
* }
|
|
329
|
+
* stores: Array<{
|
|
330
|
+
* signer: string
|
|
331
|
+
* pk: string
|
|
332
|
+
* signature: string
|
|
333
|
+
* components: Array<{did: string, version: string}>
|
|
334
|
+
* paymentIntegrity: string
|
|
335
|
+
* }>
|
|
336
|
+
* }
|
|
337
|
+
* }
|
|
338
|
+
* }} Itx
|
|
339
|
+
* @returns {Itx}
|
|
340
|
+
*/
|
|
341
|
+
const _createNftFactoryItx = ({ meta, issuers, serviceUrl, storeSignatures, factoryInput, contract }) => {
|
|
342
|
+
const factoryOutput = getBlockletPurchaseTemplate(serviceUrl);
|
|
343
|
+
const itx = {
|
|
344
|
+
name: meta.title || meta.name,
|
|
345
|
+
description: `Purchase NFT factory for blocklet ${meta.name}`,
|
|
346
|
+
settlement: 'instant',
|
|
347
|
+
limit: 0,
|
|
348
|
+
trustedIssuers: issuers,
|
|
349
|
+
input: factoryInput,
|
|
350
|
+
output: {
|
|
351
|
+
issuer: '{{ctx.issuer.id}}',
|
|
352
|
+
parent: '{{ctx.factory}}',
|
|
353
|
+
moniker: 'BlockletPurchaseNFT',
|
|
354
|
+
readonly: true,
|
|
355
|
+
transferrable: false,
|
|
356
|
+
data: factoryOutput,
|
|
357
|
+
},
|
|
358
|
+
data: {
|
|
359
|
+
type: 'json',
|
|
360
|
+
value: {
|
|
361
|
+
did: meta.did,
|
|
362
|
+
url: joinURL(serviceUrl, `/blocklet/${meta.did}`),
|
|
363
|
+
name: meta.name,
|
|
364
|
+
version: meta.version,
|
|
365
|
+
payment: {
|
|
366
|
+
version: VERSION,
|
|
367
|
+
},
|
|
368
|
+
stores: storeSignatures.map((x) => pick(x, ['signer', 'pk', 'signature', 'components', 'paymentIntegrity'])),
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
hooks: [
|
|
372
|
+
{
|
|
373
|
+
name: 'mint',
|
|
374
|
+
type: 'contract',
|
|
375
|
+
hook: contract,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
itx.address = toFactoryAddress(itx);
|
|
381
|
+
|
|
382
|
+
isValidFactory(itx, true);
|
|
383
|
+
|
|
384
|
+
return itx;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const getFactoryInput = (inputTokens, { formatToken = true } = {}) => {
|
|
388
|
+
const tokens = cloneDeep(inputTokens);
|
|
389
|
+
tokens.forEach((token) => {
|
|
390
|
+
if (formatToken) {
|
|
391
|
+
token.value = fromTokenToUnit(token.value, token.decimal).toString();
|
|
392
|
+
}
|
|
393
|
+
delete token.decimal;
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
tokens,
|
|
397
|
+
assets: [],
|
|
398
|
+
variables: [],
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const getPaymentIntegrity = async ({ contract, factoryInput, storeComponents, meta, client, storeId }) => {
|
|
403
|
+
if (!contract && !factoryInput && !storeComponents) {
|
|
404
|
+
const priceTokens = await getPriceTokens(meta, client);
|
|
405
|
+
const { components, stores } = await getComponents(meta);
|
|
406
|
+
const store = stores.find((x) => x.id === storeId);
|
|
407
|
+
|
|
408
|
+
// eslint-disable-next-line no-param-reassign
|
|
409
|
+
contract = (await getContract({ meta, components, priceTokens })).code;
|
|
410
|
+
// eslint-disable-next-line no-param-reassign
|
|
411
|
+
factoryInput = await getFactoryInput(priceTokens);
|
|
412
|
+
// eslint-disable-next-line no-param-reassign
|
|
413
|
+
storeComponents = store?.components || [];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const paymentData = {
|
|
417
|
+
factoryInput,
|
|
418
|
+
contract,
|
|
419
|
+
components: storeComponents || [],
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const integrity = md5(stableStringify(paymentData));
|
|
423
|
+
|
|
424
|
+
return integrity;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const getStoreSignatures = async ({ meta, stores, factoryInput, contract }) => {
|
|
428
|
+
const storeSignatures = [];
|
|
429
|
+
for (const store of stores) {
|
|
430
|
+
const { id, url, pk, components: storeComponents } = store;
|
|
431
|
+
|
|
432
|
+
const paymentIntegrity = await getPaymentIntegrity({ factoryInput, contract, storeComponents });
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* protocol: /api/payment/signature
|
|
436
|
+
* method: POST
|
|
437
|
+
* body: { blockletMeta, paymentIntegrity, paymentVersion }
|
|
438
|
+
* return: { signer, pk, signature}
|
|
439
|
+
*/
|
|
440
|
+
const { data: res } = await axios.post(
|
|
441
|
+
`${url}/api/payment/signature`,
|
|
442
|
+
{
|
|
443
|
+
blockletMeta: meta,
|
|
444
|
+
paymentIntegrity,
|
|
445
|
+
paymentVersion: VERSION,
|
|
446
|
+
},
|
|
447
|
+
{ timeout: 20000 }
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (res.signer !== id) {
|
|
451
|
+
throw new Error('store signature: store id does not match');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (res.pk !== pk) {
|
|
455
|
+
throw new Error('store signature: store pk does not match');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// verify sig
|
|
459
|
+
const type = toTypeInfo(id);
|
|
460
|
+
const wallet = fromPublicKey(pk, type);
|
|
461
|
+
const verifyRes = wallet.verify(paymentIntegrity, res.signature);
|
|
462
|
+
if (verifyRes !== true) {
|
|
463
|
+
throw new Error('verify store signature failed');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
storeSignatures.push({
|
|
467
|
+
signer: res.signer,
|
|
468
|
+
pk: res.pk,
|
|
469
|
+
signature: res.signature,
|
|
470
|
+
components: storeComponents,
|
|
471
|
+
paymentIntegrity,
|
|
472
|
+
storeUrl: url,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
storeSignatures,
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Used by CLI and Store to independent compute factory itx
|
|
483
|
+
*
|
|
484
|
+
* @param {{
|
|
485
|
+
* blockletMeta: BlockletMeta,
|
|
486
|
+
* ocapClient: OcapClient,
|
|
487
|
+
* issuers: Array<string>,
|
|
488
|
+
* storeUrl: string,
|
|
489
|
+
* }}
|
|
490
|
+
* @returns {{
|
|
491
|
+
* itx: Itx
|
|
492
|
+
* store: Array<{id, url}>
|
|
493
|
+
* shares: Array<{
|
|
494
|
+
* accountName: string
|
|
495
|
+
* accountAddress: DID
|
|
496
|
+
* tokenAddress: DID
|
|
497
|
+
* amount: string|number,
|
|
498
|
+
* }>
|
|
499
|
+
* }}
|
|
500
|
+
*/
|
|
501
|
+
const createNftFactoryItx = async ({ blockletMeta, ocapClient, issuers, storeUrl }) => {
|
|
502
|
+
const priceTokens = await getPriceTokens(blockletMeta, ocapClient);
|
|
503
|
+
const { components, stores } = await getComponents(blockletMeta);
|
|
504
|
+
|
|
505
|
+
const factoryInput = getFactoryInput(priceTokens);
|
|
506
|
+
const { code: contract, shares } = await getContract({
|
|
507
|
+
meta: blockletMeta,
|
|
508
|
+
client: ocapClient,
|
|
509
|
+
priceTokens,
|
|
510
|
+
components,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const { storeSignatures } = await getStoreSignatures({
|
|
514
|
+
meta: blockletMeta,
|
|
515
|
+
client: ocapClient,
|
|
516
|
+
priceTokens,
|
|
517
|
+
components,
|
|
518
|
+
stores,
|
|
519
|
+
factoryInput,
|
|
520
|
+
contract,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
itx: _createNftFactoryItx({
|
|
525
|
+
meta: blockletMeta,
|
|
526
|
+
issuers,
|
|
527
|
+
serviceUrl: storeUrl,
|
|
528
|
+
priceTokens,
|
|
529
|
+
components,
|
|
530
|
+
stores,
|
|
531
|
+
storeSignatures,
|
|
532
|
+
factoryInput,
|
|
533
|
+
contract,
|
|
534
|
+
}),
|
|
535
|
+
stores: storeSignatures.map((x) => ({ id: x.signer, url: x.storeUrl })),
|
|
536
|
+
shares,
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Used by Store before generating payment signature
|
|
542
|
+
*
|
|
543
|
+
* @param {{
|
|
544
|
+
* integrity: string,
|
|
545
|
+
* blockletMeta: BlockletMeta,
|
|
546
|
+
* ocapClient: OcapClient,
|
|
547
|
+
* storeId: string
|
|
548
|
+
* }}
|
|
549
|
+
* @returns {string} integrity
|
|
550
|
+
*/
|
|
551
|
+
const verifyPaymentIntegrity = async ({ integrity: expected, blockletMeta, ocapClient, storeId }) => {
|
|
552
|
+
const actual = await getPaymentIntegrity({ meta: blockletMeta, client: ocapClient, storeId });
|
|
553
|
+
|
|
554
|
+
if (actual !== expected) {
|
|
555
|
+
throw new Error('verify payment integrity failed');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return expected;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Used by Store before generating downloadToken
|
|
563
|
+
*
|
|
564
|
+
* @param {{
|
|
565
|
+
* {FactoryState} factoryState
|
|
566
|
+
* {Wallet} signerWallet
|
|
567
|
+
* }}
|
|
568
|
+
*
|
|
569
|
+
* @returns {{
|
|
570
|
+
* components: Array<{did: string, version: string}>
|
|
571
|
+
* }}
|
|
572
|
+
*/
|
|
573
|
+
const verifyNftFactory = async ({ factoryState, signerWallet }) => {
|
|
574
|
+
const data = JSON.parse(factoryState?.data?.value);
|
|
575
|
+
const stores = data?.stores || [];
|
|
576
|
+
const store = stores.find((x) => x.signer === signerWallet.address);
|
|
577
|
+
|
|
578
|
+
if (!store) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Signer does not found in factory. factory: ${factoryState.address}, signer: ${signerWallet.address}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const c = factoryState.hooks.find((x) => x.type === 'contract');
|
|
585
|
+
const { components } = store;
|
|
586
|
+
|
|
587
|
+
// Token 的字段和 factory 中的字段不一致
|
|
588
|
+
const factoryInput = getFactoryInput(
|
|
589
|
+
factoryState.input.tokens.map((x) => pick(x, ['address', 'value'])),
|
|
590
|
+
{ formatToken: false }
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const integrity = await getPaymentIntegrity({
|
|
594
|
+
contract: c.hook,
|
|
595
|
+
factoryInput,
|
|
596
|
+
storeComponents: components,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (signerWallet.sign(integrity) !== store.signature) {
|
|
600
|
+
debug(store, factoryInput, integrity, components, c.hook);
|
|
601
|
+
throw new Error(`verify nft factory failed: ${factoryState.address}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { components };
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
createNftFactoryItx,
|
|
609
|
+
verifyPaymentIntegrity,
|
|
610
|
+
verifyNftFactory,
|
|
611
|
+
version: VERSION,
|
|
612
|
+
_test: {
|
|
613
|
+
getPriceTokens,
|
|
614
|
+
getFactoryInput,
|
|
615
|
+
getPaymentIntegrity,
|
|
616
|
+
getComponents,
|
|
617
|
+
getContract,
|
|
618
|
+
},
|
|
619
|
+
};
|
package/lib/schema.js
CHANGED
|
@@ -307,15 +307,16 @@ const createBlockletSchema = (
|
|
|
307
307
|
|
|
308
308
|
// Set the price and share of the blocklet
|
|
309
309
|
payment: Joi.object({
|
|
310
|
-
//
|
|
310
|
+
// Currently only supports 1 token
|
|
311
311
|
price: Joi.array()
|
|
312
|
-
.max(
|
|
312
|
+
.max(1)
|
|
313
313
|
.items(
|
|
314
314
|
Joi.object({
|
|
315
315
|
value: Joi.number().greater(0).required(),
|
|
316
316
|
address: Joi.DID().required(), // token address
|
|
317
317
|
})
|
|
318
|
-
)
|
|
318
|
+
)
|
|
319
|
+
.default([]),
|
|
319
320
|
// List of beneficiaries that share the token earns from blocklet purchase
|
|
320
321
|
// If left empty, blocklet publish workflow will enforce both the developer and the registry account
|
|
321
322
|
// If not, the blocklet publish workflow will enforce the registry account
|
|
@@ -331,6 +332,7 @@ const createBlockletSchema = (
|
|
|
331
332
|
value: Joi.number().greater(0).max(1).required(),
|
|
332
333
|
})
|
|
333
334
|
)
|
|
335
|
+
.default([])
|
|
334
336
|
.custom((value) => {
|
|
335
337
|
// If share is not empty, the total value should be 1
|
|
336
338
|
if (value.length === 0) {
|
|
@@ -342,6 +344,35 @@ const createBlockletSchema = (
|
|
|
342
344
|
}
|
|
343
345
|
return value;
|
|
344
346
|
}, 'invalid blocklet share'),
|
|
347
|
+
componentPrice: Joi.array()
|
|
348
|
+
.items(
|
|
349
|
+
Joi.object({
|
|
350
|
+
parentPriceRange: Joi.array()
|
|
351
|
+
.items(Joi.number())
|
|
352
|
+
// FIXME
|
|
353
|
+
// 1. 有重叠的区间时
|
|
354
|
+
// 2. 区间不连续时
|
|
355
|
+
// 3. 区间边界
|
|
356
|
+
.custom((value, helper) => {
|
|
357
|
+
if (value.length !== 2) {
|
|
358
|
+
return helper.message('length of range should be 2');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (value[0] < 0) {
|
|
362
|
+
return helper.message('the first value should not less than 0 in range');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (value[1] <= value[0]) {
|
|
366
|
+
return helper.message('the second value should greater than the first value in range');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return value;
|
|
370
|
+
}),
|
|
371
|
+
type: Joi.string().valid('fixed', 'percentage').required(),
|
|
372
|
+
value: Joi.number().greater(0).required(),
|
|
373
|
+
})
|
|
374
|
+
)
|
|
375
|
+
.single(),
|
|
345
376
|
}).default({ price: [], share: [] }),
|
|
346
377
|
|
|
347
378
|
keywords: Joi.alternatives()
|
|
@@ -468,6 +499,7 @@ const createBlockletSchema = (
|
|
|
468
499
|
// NOTE: following fields only exist in blocklet server and cannot be set manually
|
|
469
500
|
bundleName: Joi.string(),
|
|
470
501
|
bundleDid: Joi.DID().trim(),
|
|
502
|
+
storeId: Joi.string(),
|
|
471
503
|
}).options({ stripUnknown: true, noDefaults: false, ...schemaOptions });
|
|
472
504
|
};
|
|
473
505
|
|
package/lib/util-meta.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const any = require('promise.any');
|
|
4
|
+
const joinUrl = require('url-join');
|
|
5
|
+
|
|
6
|
+
const { BLOCKLET_STORE_API_BLOCKLET_PREFIX } = require('@abtnode/constant');
|
|
7
|
+
|
|
8
|
+
const toBlockletDid = require('./did');
|
|
9
|
+
const { validateMeta, fixAndValidateService } = require('./validate');
|
|
10
|
+
|
|
11
|
+
const validateUrl = async (url, expectedHttpResTypes = ['application/json', 'text/plain']) => {
|
|
12
|
+
const parsed = new URL(url);
|
|
13
|
+
const { protocol, pathname } = parsed;
|
|
14
|
+
|
|
15
|
+
// file
|
|
16
|
+
if (protocol.startsWith('file')) {
|
|
17
|
+
const decoded = decodeURIComponent(pathname);
|
|
18
|
+
if (!fs.existsSync(decoded)) {
|
|
19
|
+
throw new Error(`File does not exist: ${decoded}`);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// http(s)
|
|
25
|
+
if (protocol.startsWith('http')) {
|
|
26
|
+
let res;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
res = await axios({ url, method: 'HEAD', timeout: 1000 * 10 });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new Error(`Cannot get content-type from ${url}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
res.headers['content-type'] &&
|
|
36
|
+
expectedHttpResTypes.some((x) => res.headers['content-type'].includes(x)) === false
|
|
37
|
+
) {
|
|
38
|
+
throw new Error(`Unexpected content-type from ${url}: ${res.headers['content-type']}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error(`Invalid url protocol: ${protocol.replace(/:$/, '')}`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const validateBlockletMeta = (meta, opts = {}) => {
|
|
48
|
+
fixAndValidateService(meta);
|
|
49
|
+
return validateMeta(meta, opts);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getBlockletMetaByUrl = async (url) => {
|
|
53
|
+
const { protocol, pathname } = new URL(url);
|
|
54
|
+
|
|
55
|
+
if (protocol.startsWith('file')) {
|
|
56
|
+
const decoded = decodeURIComponent(pathname);
|
|
57
|
+
if (!fs.existsSync(decoded)) {
|
|
58
|
+
throw new Error(`File does not exist: ${decoded}`);
|
|
59
|
+
}
|
|
60
|
+
const d = await fs.promises.readFile(decoded);
|
|
61
|
+
const meta = JSON.parse(d);
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (protocol.startsWith('http')) {
|
|
66
|
+
const { data: meta } = await axios({ url, method: 'GET', timeout: 1000 * 20 });
|
|
67
|
+
if (Object.prototype.toString.call(meta) !== '[object Object]') {
|
|
68
|
+
throw new Error('Url is not valid');
|
|
69
|
+
}
|
|
70
|
+
return meta;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(`Invalid url protocol: ${protocol.replace(/:$/, '')}`);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getBlockletMetaFromUrl = async (
|
|
77
|
+
url,
|
|
78
|
+
{ validateFn = validateBlockletMeta, returnUrl = false, ensureTarball = true, logger } = {}
|
|
79
|
+
) => {
|
|
80
|
+
const meta = await getBlockletMetaByUrl(url);
|
|
81
|
+
delete meta.htmlAst;
|
|
82
|
+
|
|
83
|
+
const newMeta = validateFn(meta, { ensureDist: true });
|
|
84
|
+
|
|
85
|
+
if (ensureTarball) {
|
|
86
|
+
try {
|
|
87
|
+
const { href } = new URL(newMeta.dist.tarball, url);
|
|
88
|
+
const tarball = decodeURIComponent(href);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await validateUrl(tarball, ['application/octet-stream', 'application/x-gzip']);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!error.message.startsWith('Cannot get content-type')) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
newMeta.dist.tarball = tarball;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const msg = `Invalid blocklet meta: dist.tarball is not a valid url ${err.message}`;
|
|
101
|
+
|
|
102
|
+
if (logger) {
|
|
103
|
+
logger.error(msg);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(msg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (returnUrl) {
|
|
111
|
+
return { meta: newMeta, url };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return newMeta;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getBlockletMetaFromUrls = async (urls, { validateFn, returnUrl = false, ensureTarball = true, logger } = {}) => {
|
|
118
|
+
try {
|
|
119
|
+
const res = await any(
|
|
120
|
+
urls.map((url) => getBlockletMetaFromUrl(url, { validateFn, returnUrl, ensureTarball, logger }))
|
|
121
|
+
);
|
|
122
|
+
return res;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
let { message } = err;
|
|
125
|
+
if (Array.isArray(err.errors)) {
|
|
126
|
+
message = err.errors.map((x) => x.message).join(', ');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (logger) {
|
|
130
|
+
logger.error('failed get blocklet meta', { urls, message });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw new Error(message);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {*} config defined in childrenSchema in blocklet meta schema
|
|
139
|
+
*/
|
|
140
|
+
const getSourceUrlsFromConfig = (config) => {
|
|
141
|
+
if (config.source) {
|
|
142
|
+
if (config.source.url) {
|
|
143
|
+
return [config.source.url].flat();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { store, version, name } = config.source;
|
|
147
|
+
return [store]
|
|
148
|
+
.flat()
|
|
149
|
+
.map((x) =>
|
|
150
|
+
joinUrl(
|
|
151
|
+
x,
|
|
152
|
+
BLOCKLET_STORE_API_BLOCKLET_PREFIX,
|
|
153
|
+
toBlockletDid(name),
|
|
154
|
+
!version || version === 'latest' ? '' : version,
|
|
155
|
+
'blocklet.json'
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (config.resolved) {
|
|
161
|
+
return [config.resolved];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error('Invalid child config');
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
validateUrl,
|
|
169
|
+
getBlockletMetaByUrl,
|
|
170
|
+
getBlockletMetaFromUrl,
|
|
171
|
+
getBlockletMetaFromUrls,
|
|
172
|
+
getSourceUrlsFromConfig,
|
|
173
|
+
};
|
package/lib/util.js
CHANGED
|
@@ -259,6 +259,18 @@ const isFreeBlocklet = (meta) => {
|
|
|
259
259
|
return priceList.every((x) => x === 0);
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
+
const isFreeComponent = (meta) => {
|
|
263
|
+
if (!meta.payment) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!meta.payment.componentPrice) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return !meta.payment.componentPrice.length;
|
|
272
|
+
};
|
|
273
|
+
|
|
262
274
|
const isComponentBlocklet = (meta) => {
|
|
263
275
|
return get(meta, 'capabilities.component') !== false;
|
|
264
276
|
};
|
|
@@ -428,6 +440,7 @@ const urlFriendly = (name) => slugify(name.replace(/^[@./-]/, '').replace(/[@./_
|
|
|
428
440
|
|
|
429
441
|
module.exports = {
|
|
430
442
|
isFreeBlocklet,
|
|
443
|
+
isFreeComponent,
|
|
431
444
|
isComponentBlocklet,
|
|
432
445
|
forEachBlocklet,
|
|
433
446
|
forEachBlockletSync,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.8.
|
|
6
|
+
"version": "1.8.16",
|
|
7
7
|
"description": "Library to parse/validate/fix blocklet meta",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -18,17 +18,18 @@
|
|
|
18
18
|
"author": "wangshijun <wangshijun2020@gmail.com> (http://github.com/wangshijun)",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@abtnode/constant": "1.8.
|
|
22
|
-
"@abtnode/util": "1.8.
|
|
23
|
-
"@arcblock/did": "1.17.
|
|
24
|
-
"@arcblock/did-ext": "1.17.
|
|
25
|
-
"@arcblock/did-util": "1.17.
|
|
26
|
-
"@arcblock/jwt": "1.17.
|
|
27
|
-
"@ocap/asset": "1.17.
|
|
28
|
-
"@ocap/mcrypto": "1.17.
|
|
29
|
-
"@ocap/util": "1.17.
|
|
30
|
-
"@ocap/wallet": "1.17.
|
|
21
|
+
"@abtnode/constant": "1.8.16",
|
|
22
|
+
"@abtnode/util": "1.8.16",
|
|
23
|
+
"@arcblock/did": "1.17.17",
|
|
24
|
+
"@arcblock/did-ext": "1.17.17",
|
|
25
|
+
"@arcblock/did-util": "1.17.17",
|
|
26
|
+
"@arcblock/jwt": "1.17.17",
|
|
27
|
+
"@ocap/asset": "1.17.17",
|
|
28
|
+
"@ocap/mcrypto": "1.17.17",
|
|
29
|
+
"@ocap/util": "1.17.17",
|
|
30
|
+
"@ocap/wallet": "1.17.17",
|
|
31
31
|
"ajv": "^8.11.0",
|
|
32
|
+
"axios": "^0.27.2",
|
|
32
33
|
"cjk-length": "^1.0.0",
|
|
33
34
|
"debug": "^4.3.4",
|
|
34
35
|
"fs-extra": "^10.1.0",
|
|
@@ -39,12 +40,18 @@
|
|
|
39
40
|
"js-yaml": "^4.1.0",
|
|
40
41
|
"json-stable-stringify": "^1.0.1",
|
|
41
42
|
"lodash": "^4.17.21",
|
|
43
|
+
"promise.any": "^2.0.4",
|
|
42
44
|
"slugify": "^1.6.5",
|
|
43
45
|
"url-join": "^4.0.1",
|
|
44
46
|
"validate-npm-package-name": "^3.0.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
49
|
+
"body-parser": "^1.20.0",
|
|
50
|
+
"compression": "^1.7.4",
|
|
51
|
+
"detect-port": "^1.3.0",
|
|
52
|
+
"expand-tilde": "^2.0.2",
|
|
53
|
+
"express": "^4.18.1",
|
|
47
54
|
"jest": "^27.5.1"
|
|
48
55
|
},
|
|
49
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "2605cf6ebf2a0c3bb5524cb0f1385ad565fd85d6"
|
|
50
57
|
}
|