@ocap/resolver 1.6.3 → 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/lib/index.js CHANGED
@@ -1,15 +1,47 @@
1
1
  /* eslint-disable no-await-in-loop */
2
2
  /* eslint-disable newline-per-chained-call */
3
3
  const get = require('lodash/get');
4
+ const set = require('lodash/set');
5
+ const uniq = require('lodash/uniq');
6
+ const pick = require('lodash/pick');
7
+ const isEmpty = require('lodash/isEmpty');
8
+ const isEqual = require('lodash/isEqual');
9
+ const uniqBy = require('lodash/unionBy');
4
10
  const Config = require('@ocap/config');
5
11
  const states = require('@ocap/state');
12
+ const Joi = require('@arcblock/validator');
6
13
  const md5 = require('@ocap/util/lib/md5');
7
- const { fromPublicKey } = require('@arcblock/did');
14
+ const CustomError = require('@ocap/util/lib/error');
15
+ const { types } = require('@ocap/mcrypto');
16
+ const { fromTypeUrl } = require('@ocap/message');
17
+ const { toStakeAddress } = require('@arcblock/did-util');
18
+ const { fromPublicKey, toTypeInfo, isValid: isValidDid } = require('@arcblock/did');
8
19
  const { toBN, fromTokenToUnit } = require('@ocap/util');
20
+ const { DEFAULT_TOKEN_DECIMAL } = require('@ocap/util/lib/constant');
9
21
  const { createExecutor } = require('@ocap/tx-protocols');
10
- const { decodeItxData } = require('@ocap/tx-protocols/lib/util');
22
+ const { decodeAnySafe } = require('@ocap/tx-protocols/lib/util');
23
+ const {
24
+ createIndexedAccount,
25
+ createIndexedAsset,
26
+ createIndexedFactory,
27
+ createIndexedDelegation,
28
+ createIndexedToken,
29
+ createIndexedTransaction,
30
+ createIndexedStake,
31
+ createIndexedRollup,
32
+ createIndexedRollupBlock,
33
+ isDefaultTokenChanged,
34
+ } = require('@ocap/indexdb/lib/util');
35
+ const { formatTxType } = require('@ocap/util');
36
+
37
+ const { getTxReceipts, mergeTxReceipts, getTxSender, getTxReceiver } = require('@ocap/state/lib/states/tx');
11
38
 
12
39
  const debug = require('debug')(require('../package.json').name);
40
+ const hooks = require('./hooks');
41
+ const { getInstance: getTokenCacheInstance } = require('./token-cache');
42
+
43
+ const noop = (x) => x;
44
+ const CHAIN_ADDR = md5('OCAP_CHAIN_ADDR');
13
45
 
14
46
  const formatData = (data) => {
15
47
  if (!data) {
@@ -27,18 +59,48 @@ const formatData = (data) => {
27
59
  data.type_url = data.typeUrl;
28
60
  return data;
29
61
  } catch (err) {
30
- // do nothing
62
+ // Some legacy data contains escaped back slashes
63
+ try {
64
+ JSON.parse(data.value.replace(/\\"/g, '"'));
65
+ data.type_url = data.typeUrl;
66
+ data.value = data.value.replace(/\\"/g, '"');
67
+ return data;
68
+ } catch (e) {
69
+ // do nothing
70
+ }
31
71
  }
32
72
  }
33
73
 
34
- const decoded = data.typeUrl ? decodeItxData(data) : data;
35
- if (['json', 'vc'].includes(decoded.type)) {
74
+ const decoded = data.typeUrl ? decodeAnySafe(data) : data;
75
+ if (['json', 'vc', 'AssetFactory'].includes(decoded.type)) {
36
76
  return { typeUrl: decoded.type, type_url: decoded.type, value: JSON.stringify(decoded.value) };
37
77
  }
38
78
 
39
79
  return { typeUrl: decoded.type, type_url: decoded.type, value: decoded.value };
40
80
  };
41
81
 
82
+ const formatDelegationOps = (state) => {
83
+ if (state && state.ops && typeof state.ops === 'object') {
84
+ state.ops = Object.keys(state.ops).map((x) => ({ key: x, value: state.ops[x] }));
85
+ }
86
+ return state;
87
+ };
88
+
89
+ const extractTokenMeta = (address, tokenStates) => {
90
+ const tokenState = tokenStates.filter(Boolean).find((t) => t.address === address);
91
+ if (!tokenState) {
92
+ return {};
93
+ }
94
+
95
+ return {
96
+ address,
97
+ decimal: typeof tokenState.decimal === 'undefined' ? DEFAULT_TOKEN_DECIMAL : tokenState.decimal,
98
+ unit: tokenState.unit,
99
+ symbol: tokenState.symbol,
100
+ foreignToken: tokenState.foreignToken,
101
+ };
102
+ };
103
+
42
104
  module.exports = class OCAPResolver {
43
105
  /**
44
106
  * Creates an instance of OCAPResolver.
@@ -59,41 +121,71 @@ module.exports = class OCAPResolver {
59
121
 
60
122
  this.statedb = statedb;
61
123
  this.indexdb = indexdb;
124
+ this.filter = filter;
125
+
62
126
  this.config = Object.freeze(Config.validate(config));
127
+ this.chainAddr = md5(config.chainId);
128
+ this.tokenItx = Config.genTokenItx(this.config);
129
+ this.consensus = `${statedb.name} v${statedb.version}`;
63
130
 
64
- if (validateTokenConfig) {
65
- this.validateTokenConfig();
131
+ if (indexdb) {
132
+ this.tokenCache = getTokenCacheInstance(indexdb.token);
133
+ }
134
+
135
+ if (this.tokenItx) {
136
+ this.config.token.address = this.tokenItx.address;
137
+ if (this.tokenCache) {
138
+ this.tokenCache.set(this.tokenItx.address, this.tokenItx);
139
+ }
66
140
  }
67
141
 
68
- this.filter = filter;
69
142
  this.executor = createExecutor({
70
143
  filter,
71
144
  runAsLambda: typeof statedb.runAsLambda === 'function' ? statedb.runAsLambda.bind(statedb) : null,
72
145
  });
146
+ this.formatTx = this._formatTx.bind(this);
147
+
148
+ if (validateTokenConfig) {
149
+ this.validateTokenConfig();
150
+ }
73
151
 
74
152
  this.connectIndexDB();
75
- this.runAsLambda((txn) => this.initializeStateDB(txn));
153
+ this.runAsLambda((txn) => this.initializeStateDB(txn)).catch((err) => {
154
+ console.error('Failed to initialize statedb:', err.message);
155
+ process.exit(1);
156
+ });
76
157
  }
77
158
 
78
159
  async sendTx({ tx: txBase64 }) {
79
160
  debug('sendTx', txBase64);
80
161
 
162
+ if (process.env.CHAIN_MODE === 'readonly') {
163
+ throw new CustomError('FORBIDDEN', 'This chain node is running in readonly mode');
164
+ }
165
+
81
166
  // 0. create new context
82
167
  const context = { txBase64, statedb: this.statedb, config: this.config };
83
168
 
84
- // 1. execute the transaction
169
+ // 1. execute and persist the transaction
85
170
  const result = await this.executor.execute(context);
86
171
 
87
172
  // 2. Return the hash
88
173
  return result.txHash;
89
174
  }
90
175
 
91
- async getTx({ hash }) {
92
- const result = await this.runAsLambda((txn) => this.statedb.tx.get(hash, { txn }));
93
- if (result) {
94
- result.tx.itxJson.data = formatData(result.tx.itxJson.data);
176
+ getTx({ hash }, ctx) {
177
+ if (Joi.patterns.txHash.test(hash)) {
178
+ return this._getState({
179
+ table: 'tx',
180
+ id: hash.toUpperCase(),
181
+ dataKey: 'tx.itxJson.data',
182
+ onRead: (tx) => this.formatTx(tx, ctx),
183
+ expandContext: false,
184
+ ctx,
185
+ });
95
186
  }
96
- return result;
187
+
188
+ return null;
97
189
  }
98
190
 
99
191
  getBlock() {
@@ -110,15 +202,15 @@ module.exports = class OCAPResolver {
110
202
 
111
203
  async getChainInfo() {
112
204
  return Promise.resolve({
113
- id: md5(this.config.chainId),
205
+ id: this.chainAddr,
114
206
  totalTxs: await this.indexdb.tx.count(),
115
207
  blockHash: md5(''),
116
208
  blockHeight: 0,
117
209
  blockTime: '',
118
210
  version: this.config.version,
119
- address: md5(this.config.chainId),
211
+ address: this.chainAddr,
120
212
  appHash: md5(new Date().toISOString()),
121
- consensusVersion: this.config.version,
213
+ consensusVersion: this.consensus,
122
214
  forgeAppsVersion: [],
123
215
  moniker: this.config.chainId,
124
216
  network: this.config.chainId,
@@ -130,9 +222,9 @@ module.exports = class OCAPResolver {
130
222
 
131
223
  getNodeInfo() {
132
224
  return Promise.resolve({
133
- address: md5(this.config.chainId),
225
+ address: this.chainAddr,
134
226
  appHash: md5(new Date().toISOString()),
135
- consensusVersion: `${this.statedb.name} v${this.statedb.version}`,
227
+ consensusVersion: this.consensus,
136
228
  forgeAppsVersion: [],
137
229
  geoInfo: {
138
230
  city: 'Unknown',
@@ -140,7 +232,7 @@ module.exports = class OCAPResolver {
140
232
  latitude: 0,
141
233
  longitude: 0,
142
234
  },
143
- id: md5(this.config.chainId),
235
+ id: this.chainAddr,
144
236
  ip: '',
145
237
  moniker: this.config.chainId,
146
238
  network: this.config.chainId,
@@ -165,7 +257,7 @@ module.exports = class OCAPResolver {
165
257
  blockHeight: 0,
166
258
  validators: [
167
259
  {
168
- address: fromPublicKey(md5(this.config.chainId)),
260
+ address: fromPublicKey(this.chainAddr),
169
261
  name: '',
170
262
  proposerPriority: '0',
171
263
  votingPower: '10',
@@ -178,25 +270,196 @@ module.exports = class OCAPResolver {
178
270
  return JSON.stringify(this.config);
179
271
  }
180
272
 
181
- async getAccountState({ address }) {
182
- const account = await this.runAsLambda((txn) => this.statedb.account.get(address, { txn }));
183
- if (account) {
184
- account.data = formatData(account.data);
273
+ getAccountState({ address }, ctx) {
274
+ return this._getState({
275
+ table: 'account',
276
+ id: address,
277
+ dataKey: 'data',
278
+ onRead: async (state) => {
279
+ if (state) {
280
+ if (state.tokens) {
281
+ state.tokens = await this.formatTokenMap(state.tokens);
282
+ }
283
+ }
284
+
285
+ return state;
286
+ },
287
+ ctx,
288
+ });
289
+ }
290
+
291
+ getStakeState({ address }, ctx) {
292
+ return this._getState({
293
+ table: 'stake',
294
+ id: address,
295
+ dataKey: 'data',
296
+ onRead: async (state) => {
297
+ if (state) {
298
+ if (state.tokens) {
299
+ state.tokens = await this.formatTokenMap(state.tokens);
300
+ }
301
+ if (state.revokedTokens) {
302
+ state.revokedTokens = await this.formatTokenMap(state.revokedTokens);
303
+ }
304
+ }
305
+
306
+ return state;
307
+ },
308
+ ctx,
309
+ });
310
+ }
311
+
312
+ async getRollupState({ address }, ctx) {
313
+ const rollup = await this._getState({
314
+ table: 'rollup',
315
+ id: address,
316
+ dataKey: 'data',
317
+ onRead: async (state) => {
318
+ if (state) {
319
+ const tokens = await this.formatTokenMap({ [state.tokenAddress]: '0' });
320
+ state.tokenInfo = tokens.find((x) => x.address === state.tokenAddress);
321
+ state.foreignToken = state.tokenInfo.foreignToken;
322
+ }
323
+
324
+ return state;
325
+ },
326
+ ctx,
327
+ });
328
+
329
+ if (!rollup) {
330
+ return null;
331
+ }
332
+
333
+ const indexed = await this.indexdb.rollup.get(address);
334
+ if (indexed) {
335
+ rollup.totalDepositAmount = indexed.totalDepositAmount || '0';
336
+ rollup.totalWithdrawAmount = indexed.totalWithdrawAmount || '0';
185
337
  }
186
- return account;
338
+
339
+ return rollup;
187
340
  }
188
341
 
189
- async getAssetState({ address }) {
190
- const asset = await this.runAsLambda((txn) => this.statedb.asset.get(address, { txn }));
191
- if (asset) {
192
- asset.data = formatData(asset.data);
342
+ async getRollupBlock({ hash, height, rollupAddress }, ctx) {
343
+ if (hash) {
344
+ return this._getState({ table: 'rollupBlock', id: hash, dataKey: 'data', ctx });
345
+ }
346
+
347
+ const blockHeight = Number(height || 0);
348
+ if (blockHeight > 0 && rollupAddress) {
349
+ const { blocks } = await this._doPaginatedSearch(
350
+ 'listRollupBlocks',
351
+ { height: blockHeight, rollupAddress },
352
+ 'blocks',
353
+ 'data',
354
+ ctx
355
+ );
356
+
357
+ if (blocks.length) {
358
+ return this._getState({ table: 'rollupBlock', id: blocks[0].hash, dataKey: 'data', ctx });
359
+ }
360
+
361
+ return null;
193
362
  }
194
- return asset;
363
+
364
+ throw new Error('Can not get rollup block without hash or height + rollup');
365
+ }
366
+
367
+ getAssetState({ address }, ctx) {
368
+ return this._getState({
369
+ table: 'asset',
370
+ id: address,
371
+ dataKey: 'data',
372
+ onRead: (state) => {
373
+ if (state && Array.isArray(state.tagsList)) {
374
+ state.tags = state.tagsList;
375
+ }
376
+
377
+ return state;
378
+ },
379
+ ctx,
380
+ });
381
+ }
382
+
383
+ getEvidenceState({ hash }, ctx) {
384
+ return this._getState({ table: 'evidence', id: hash, dataKey: null, ctx });
385
+ }
386
+
387
+ getFactoryState({ address }, ctx) {
388
+ return this._getState({
389
+ table: 'factory',
390
+ id: address,
391
+ dataKey: 'data',
392
+ onRead: async (state) => {
393
+ if (state) {
394
+ state.tokens = await this.formatTokenMap(state.tokens);
395
+ if (Array.isArray(state.input.tokens) && state.input.tokens.length > 0) {
396
+ state.input.tokens = await this.formatTokenArray(state.input.tokens || []);
397
+ }
398
+
399
+ state.output.data = formatData(state.output.data);
400
+ }
401
+
402
+ return state;
403
+ },
404
+ ctx,
405
+ });
406
+ }
407
+
408
+ async getTokenState({ address }, ctx) {
409
+ const state = await this._getState({ table: 'token', id: address, dataKey: 'data', ctx });
410
+ if (state) {
411
+ if (typeof state.decimal === 'undefined') {
412
+ state.decimal = DEFAULT_TOKEN_DECIMAL;
413
+ }
414
+ }
415
+
416
+ return state;
417
+ }
418
+
419
+ getDelegateState({ address }, ctx) {
420
+ return this._getState({ table: 'delegation', id: address, dataKey: 'data', onRead: formatDelegationOps, ctx });
421
+ }
422
+
423
+ async getAccountTokens({ address, token }, ctx) {
424
+ if (!address) {
425
+ return [];
426
+ }
427
+
428
+ let state = null;
429
+
430
+ const type = toTypeInfo(address);
431
+ if (type.role === types.RoleType.ROLE_ASSET) {
432
+ state = await this._getState({ table: 'factory', id: address, dataKey: 'data', expandContext: false, ctx });
433
+ } else {
434
+ state = await this._getState({ table: 'account', id: address, dataKey: 'data', expandContext: false, ctx });
435
+ }
436
+
437
+ if (!state || !state.tokens) {
438
+ return [];
439
+ }
440
+
441
+ const allTokens = Object.keys(state.tokens);
442
+ const wantedTokens = token ? allTokens.filter((x) => x === token) : allTokens;
443
+
444
+ const tokens = await Promise.all(
445
+ wantedTokens.map((x) => this._getState({ table: 'token', id: x, dataKey: 'data', expandContext: false, ctx }))
446
+ );
447
+ const tokensInfo = tokens.reduce((acc, x) => {
448
+ acc[x.address] = { symbol: x.symbol, decimal: x.decimal || DEFAULT_TOKEN_DECIMAL, unit: x.util }; // 兼容没有 decimal 的 token
449
+ return acc;
450
+ }, {});
451
+
452
+ return wantedTokens.map((x) => ({
453
+ address: x,
454
+ ...tokensInfo[x],
455
+ balance: state.tokens[x],
456
+ }));
195
457
  }
196
458
 
197
459
  getForgeState() {
460
+ const { accounts, token, transaction } = this.config;
198
461
  return Promise.resolve({
199
- accountConfig: [],
462
+ accountConfig: accounts,
200
463
  address: 'forge_state',
201
464
  consensus: {
202
465
  maxBytes: '150000',
@@ -207,52 +470,18 @@ module.exports = class OCAPResolver {
207
470
  validatorChanged: false,
208
471
  },
209
472
  data: null,
210
- gas: [],
211
- protocols: [],
212
- stakeSummary: [],
213
473
  tasks: [],
214
- token: this.config.token,
215
- tokenSwapConfig: {
216
- commissionHolderAddress: '',
217
- commissionRate: 0,
218
- maxCommission: null,
219
- minCommission: null,
220
- revokeCommissionRate: 0,
221
- },
474
+ token,
222
475
  txConfig: {
223
- minimumStake: '10000000000000000',
224
- declare: {
225
- cost: 0,
226
- hierarchy: 5,
227
- restricted: false,
228
- },
229
- stake: {
230
- timeoutGeneral: 86400,
231
- timeoutStakeForNode: 604800,
232
- },
233
- ...this.config.transaction,
476
+ ...transaction,
477
+ txFee: Object.keys(transaction.txFee)
478
+ .filter((x) => x !== 'default')
479
+ .map((x) => ({ typeUrl: x, fee: fromTokenToUnit(transaction.txFee[x], token.decimal).toString(10) })),
234
480
  },
235
481
  upgradeInfo: null,
236
482
  });
237
483
  }
238
484
 
239
- getSwapState() {
240
- return null;
241
- }
242
-
243
- async getDelegateState({ address }) {
244
- const state = await this.runAsLambda((txn) => this.statedb.delegation.get(address, { txn }));
245
- if (state && state.ops) {
246
- state.data = formatData(state.data);
247
- state.ops = Object.keys(state.ops).reduce((acc, key) => {
248
- acc.push({ key, value: state.ops[key] });
249
- return acc;
250
- }, []);
251
- }
252
-
253
- return state;
254
- }
255
-
256
485
  async getForgeStats() {
257
486
  const [accountCount, assetCount, txCount] = await Promise.all([
258
487
  this.indexdb.account.count(),
@@ -284,63 +513,119 @@ module.exports = class OCAPResolver {
284
513
  };
285
514
  }
286
515
 
287
- async listTransactions(args) {
288
- const txs = await this.indexdb.listTransactions(args);
289
- txs.forEach((x) => {
290
- x.tx.itxJson.data = formatData(x.tx.itxJson.data);
291
- });
292
- return txs;
516
+ listTransactions(args, ctx) {
517
+ return this._doPaginatedSearch('listTransactions', args, 'transactions', 'tx.itxJson.data', ctx);
293
518
  }
294
519
 
295
- async listAssets(args) {
296
- const res = await this.indexdb.listAssets(args);
297
- const { assets = [], ...rest } = res;
298
- assets.forEach((x) => {
299
- x.data = formatData(x.data);
300
- });
301
-
302
- return { assets, ...rest };
520
+ listAssets(args, ctx) {
521
+ return this._doPaginatedSearch('listAssets', args, 'assets', 'data', ctx);
303
522
  }
304
523
 
305
- listStakes() {
306
- return [];
524
+ listAssetTransactions(args = {}, ctx) {
525
+ if (!args.address) {
526
+ return { transactions: [] };
527
+ }
528
+
529
+ return this._doPaginatedSearch(
530
+ 'listTransactions',
531
+ { ...args, assetFilter: { assets: [args.address] } },
532
+ 'transactions',
533
+ 'tx.itxJson.data',
534
+ ctx
535
+ );
307
536
  }
308
537
 
309
- async listAssetTransactions(args) {
310
- const txs = await this.indexdb.listAssetTransactions(args);
311
- txs.forEach((x) => {
312
- x.tx.itxJson.data = formatData(x.tx.itxJson.data);
538
+ async listFactories(args, ctx) {
539
+ const result = await this._doPaginatedSearch('listFactories', args, 'factories', 'data', ctx);
540
+ result.factories = result.factories.map((x) => {
541
+ x.output.data = formatData(x.output.data);
542
+ return x;
313
543
  });
314
- return txs;
315
- }
316
544
 
317
- listBlocks() {
318
- return [];
545
+ return result;
319
546
  }
320
547
 
321
548
  listTopAccounts(args) {
322
- return this.indexdb.listTopAccounts(args);
549
+ if (!args.tokenAddress) {
550
+ args.tokenAddress = this.tokenItx.address;
551
+ }
552
+
553
+ return this._doPaginatedSearch('listTopAccounts', args, 'accounts');
323
554
  }
324
555
 
325
- getHealthStatus() {
326
- // TODO: implement this
327
- return null;
556
+ listTokens(args, ctx) {
557
+ return this._doPaginatedSearch('listTokens', args, 'tokens', 'data', ctx);
328
558
  }
329
559
 
330
- listSwap() {
331
- return [];
560
+ listStakes(args, ctx) {
561
+ return this._doPaginatedSearch('listStakes', args, 'stakes', 'data', ctx);
332
562
  }
333
563
 
334
- getSwapStatistics() {
335
- return null;
564
+ listRollups(args, ctx) {
565
+ return this._doPaginatedSearch('listRollups', args, 'rollups', 'data', ctx);
336
566
  }
337
567
 
338
- subscribe() {
339
- throw new Error('subscribe must be implemented in sub adapter');
568
+ listRollupBlocks(args, ctx) {
569
+ return this._doPaginatedSearch('listRollupBlocks', args, 'blocks', 'data', ctx);
340
570
  }
341
571
 
342
- unsubscribe() {
343
- throw new Error('unsubscribe must be implemented in sub adapter');
572
+ async listRollupValidators(args, ctx) {
573
+ if (!args.rollupAddress) {
574
+ return { validators: [] };
575
+ }
576
+
577
+ const rollup = await this.runAsLambda((txn) => this.statedb.rollup.get(args.rollupAddress, { txn }));
578
+ if (!rollup) {
579
+ return { validators: [] };
580
+ }
581
+
582
+ const result = await this._doPaginatedSearch('listRollupValidators', args, 'validators', null, ctx);
583
+ const stakes = (
584
+ await Promise.all(
585
+ result.validators.map((x) => {
586
+ const stakeAddress = toStakeAddress(x.address, args.rollupAddress);
587
+ return this.runAsLambda((txn) => this.statedb.stake.get(stakeAddress, { txn }));
588
+ })
589
+ )
590
+ ).filter(Boolean);
591
+ result.validators.forEach((x) => {
592
+ const stakeAddress = toStakeAddress(x.address, args.rollupAddress);
593
+ const stake = stakes.find((s) => s.address === stakeAddress);
594
+ x.availableStake = stake ? stake.tokens[rollup.tokenAddress] : '0';
595
+ });
596
+
597
+ return result;
598
+ }
599
+
600
+ async search(args) {
601
+ if (!args.keyword) {
602
+ return { results: [] };
603
+ }
604
+
605
+ const doSearch = async (type, keyword) => {
606
+ const result = await this.indexdb[type].get(keyword);
607
+ return result ? { type, id: keyword } : null;
608
+ };
609
+
610
+ if (Joi.patterns.txHash.test(args.keyword)) {
611
+ const results = await Promise.all([
612
+ doSearch('tx', args.keyword.toUpperCase()),
613
+ doSearch('rollupBlock', args.keyword),
614
+ ]);
615
+ return { results: results.filter(Boolean) };
616
+ }
617
+
618
+ const entitiesByDid = ['account', 'asset', 'delegation', 'factory', 'token', 'stake', 'rollup'];
619
+ if (isValidDid(args.keyword)) {
620
+ const results = await Promise.all(entitiesByDid.map((type) => doSearch(type, args.keyword)));
621
+ return { results: results.filter(Boolean) };
622
+ }
623
+
624
+ return { results: [] };
625
+ }
626
+
627
+ listBlocks() {
628
+ return { blocks: [] };
344
629
  }
345
630
 
346
631
  validateTokenConfig() {
@@ -352,96 +637,353 @@ module.exports = class OCAPResolver {
352
637
  }
353
638
  }
354
639
 
640
+ runAsLambda(fn) {
641
+ if (typeof this.statedb.runAsLambda === 'function') {
642
+ return this.statedb.runAsLambda((txn) => fn(txn));
643
+ }
644
+
645
+ return fn();
646
+ }
647
+
648
+ getChain() {
649
+ return this.runAsLambda((txn) => this.statedb.chain.get(CHAIN_ADDR, { txn }));
650
+ }
651
+
652
+ getToken(id) {
653
+ return this.runAsLambda((txn) => this.statedb.token.get(id, { txn }));
654
+ }
655
+
355
656
  async initializeStateDB(txn) {
356
657
  const { accounts, token } = this.config;
357
- const { account: accountStateDB } = this.statedb;
358
- const { account: accountState } = states;
658
+ const { account: accountDB, chain: chainDB, token: tokenDB } = this.statedb;
659
+ const { account: accountState, chain: chainState, token: tokenState } = states;
660
+
661
+ const ctx = { txn };
662
+ const context = { txTime: new Date().toISOString(), txHash: '' };
663
+
664
+ // Auto persist config to chain state
665
+ // Will throw error if immutable chain config are updated
666
+ const info = await this.getChain();
667
+ if (!info) {
668
+ const state = chainState.create({ ...this.config, address: CHAIN_ADDR }, context);
669
+ const result = await chainDB.create(CHAIN_ADDR, state, ctx);
670
+ debug('create chain state', result);
671
+ } else if (isEqual(pick(info, Object.keys(this.config)), this.config) === false) {
672
+ const state = chainState.update(info, this.config, context);
673
+ const result = await chainDB.update(CHAIN_ADDR, state, ctx);
674
+ debug('update chain state', result);
675
+ }
676
+
677
+ // Auto persist token state, just once
678
+ // Since the token info should not be changed after restart
679
+ if (this.tokenItx) {
680
+ const existToken = await this.getToken(this.tokenItx.address);
681
+ if (!existToken) {
682
+ const state = tokenState.create(this.tokenItx, context);
683
+ const result = await tokenDB.create(this.tokenItx.address, state, ctx);
684
+ tokenDB.emit('create', result, ctx);
685
+ debug('create token state', result);
686
+ }
687
+
688
+ // Auto populate token holder accounts if not exist
689
+ for (let i = 0; i < accounts.length; i++) {
690
+ const { address, balance } = accounts[i];
691
+ try {
692
+ const existAccount = await accountDB.get(address, ctx);
693
+ if (!existAccount) {
694
+ const balanceStr = fromTokenToUnit(balance, token.decimal).toString(10);
695
+ const state = accountState.create(
696
+ { address, tokens: { [this.tokenItx.address]: balanceStr }, moniker: 'token-holder' },
697
+ context
698
+ );
699
+ const result = await accountDB.create(address, state, ctx);
700
+ accountDB.emit('create', result, ctx);
701
+ }
702
+ } catch (err) {
703
+ console.error('Failed to initialize initial token holders', err);
704
+ }
705
+ }
706
+ }
707
+ }
359
708
 
360
- const emit = this.executor.emit.bind(this.executor);
709
+ connectIndexDB() {
710
+ const getToken = this.getToken.bind(this);
711
+
712
+ this.statedb.tx.on('create', async (x, ctx) => {
713
+ try {
714
+ const tx = await createIndexedTransaction(x, ctx);
715
+ await this.indexdb.tx.insert(tx);
716
+ if (typeof hooks.onCreateTx === 'function') {
717
+ await hooks.onCreateTx(tx, ctx, this.indexdb);
718
+ }
719
+ } catch (error) {
720
+ console.error('create tx index failed', { account: x, error });
721
+ }
722
+ });
361
723
 
362
- // Auto populate token holder accounts if not exist
363
- for (let i = 0; i < accounts.length; i++) {
364
- const { address, balance } = accounts[i];
724
+ this.statedb.account.on('create', async (x) => {
365
725
  try {
366
- const exist = await accountStateDB.get(address);
367
- if (!exist) {
368
- const balanceStr = fromTokenToUnit(balance, token.decimal).toString(10);
369
- const context = { txTime: new Date().toISOString(), txHash: '', emit };
370
- const state = accountState.create({ address, balance: balanceStr, moniker: 'token-holder' }, context);
371
- await accountStateDB.create(address, state, { txn });
726
+ await this.indexdb.account.insert(await createIndexedAccount(x, getToken));
727
+ } catch (error) {
728
+ console.error('create account index failed', { account: x, error });
729
+ }
730
+ });
731
+ this.statedb.account.on('update', async (x) => {
732
+ try {
733
+ await this.indexdb.account.update(x.address, await createIndexedAccount(x, getToken));
734
+ } catch (error) {
735
+ console.error('update account index failed', { account: x, error });
736
+ }
737
+ });
738
+
739
+ const mapping = {
740
+ asset: [createIndexedAsset, 'address'],
741
+ delegation: [createIndexedDelegation, 'address'],
742
+ token: [createIndexedToken, 'address'],
743
+ factory: [createIndexedFactory, 'address'],
744
+ stake: [createIndexedStake, 'address'],
745
+ rollup: [createIndexedRollup, 'address', hooks.onCreateRollup],
746
+ rollupBlock: [createIndexedRollupBlock, 'hash', hooks.onCreateRollupBlock],
747
+ };
748
+ Object.keys(mapping).forEach((table) => {
749
+ const [fn, key, onCreate, onUpdate] = mapping[table];
750
+
751
+ this.statedb[table].on('create', async (x, _ctx) => {
752
+ try {
753
+ const ctx = this.enrichIndexContext(_ctx);
754
+ const doc = await fn(x, ctx);
755
+ await this.indexdb[table].insert(doc);
756
+ if (typeof onCreate === 'function') {
757
+ await onCreate(doc, ctx, this.indexdb);
758
+ }
759
+ } catch (error) {
760
+ console.error(`create ${table} index failed`, { [table]: x, error });
761
+ }
762
+ });
763
+
764
+ this.statedb[table].on('update', async (x, _ctx) => {
765
+ try {
766
+ const ctx = this.enrichIndexContext(_ctx);
767
+ const doc = await fn(x, ctx);
768
+ await this.indexdb[table].update(doc[key], doc);
769
+ if (typeof onUpdate === 'function') {
770
+ await onUpdate(doc, ctx, this.indexdb);
771
+ }
772
+ } catch (error) {
773
+ console.error(`update ${table} index failed`, { [table]: x, error });
774
+ }
775
+ });
776
+ });
777
+ }
778
+
779
+ async _getState({ table, id, dataKey, onRead = noop, expandContext = true, ctx }) {
780
+ if (!id) {
781
+ throw new CustomError('INVALID_REQUEST', `Missing required parameter to read ${table} state`);
782
+ }
783
+
784
+ const state = await this.runAsLambda((txn) => this.statedb[table].get(id, { txn }));
785
+ if (state) {
786
+ if (dataKey) {
787
+ set(state, dataKey, formatData(get(state, dataKey)));
788
+ }
789
+
790
+ // expand context transactions
791
+ if (expandContext && state.context) {
792
+ const { genesisTx, renaissanceTx } = state.context;
793
+ const txs = (
794
+ await Promise.all(
795
+ uniq([genesisTx, renaissanceTx])
796
+ .filter(Boolean)
797
+ .map((x) => this.getTx({ hash: x }, ctx))
798
+ )
799
+ ).filter(Boolean);
800
+ state.context.genesisTx = txs.find((x) => x.hash === genesisTx);
801
+ state.context.renaissanceTx = txs.find((x) => x.hash === renaissanceTx);
802
+
803
+ if (!state.context.genesisTx) {
804
+ state.context.genesisTx = { hash: genesisTx };
805
+ }
806
+ if (!state.context.renaissanceTx) {
807
+ state.context.renaissanceTx = { hash: renaissanceTx };
372
808
  }
373
- } catch (err) {
374
- // Do nothing
375
809
  }
376
810
  }
811
+
812
+ return onRead(state);
377
813
  }
378
814
 
379
- runAsLambda(fn) {
380
- if (typeof this.statedb.runAsLambda === 'function') {
381
- return new Promise((resolve, reject) => this.statedb.runAsLambda((txn) => fn(txn).then(resolve).catch(reject)));
815
+ async _doPaginatedSearch(fn, args, listKey, dataKey, ctx) {
816
+ const result = await this.indexdb[fn](args);
817
+ const { [listKey]: items = [], ...rest } = result;
818
+ let data = items;
819
+
820
+ if (dataKey) {
821
+ data = await Promise.all(
822
+ items.map(async (x) => {
823
+ let tx = set(x, dataKey, formatData(get(x, dataKey)));
824
+ if (listKey === 'transactions') {
825
+ tx = await this.formatTx(tx, ctx);
826
+ }
827
+
828
+ return tx;
829
+ })
830
+ );
382
831
  }
383
832
 
384
- return fn();
833
+ const final = { [listKey]: data, ...rest };
834
+ if (!final.paging) {
835
+ final.paging = { cursor: '0', next: false, total: 0 };
836
+ }
837
+
838
+ return final;
385
839
  }
386
840
 
387
- connectIndexDB() {
388
- const { account, asset, delegation, tx } = this.indexdb;
389
- this.executor.on('tx.create', (x, ctx) => tx.insert(x, ctx));
390
- this.executor.on('account.create', (x, ctx) => account.insert(this.createIndexedAccount(x), ctx));
391
- this.executor.on('account.update', (x, ctx) => account.update(x.address, this.createIndexedAccount(x), ctx));
392
- this.executor.on('asset.create', (x, ctx) => asset.insert(this.createIndexedAsset(x), ctx));
393
- this.executor.on('asset.update', (x, ctx) => asset.update(x.address, this.createIndexedAsset(x), ctx));
394
- this.executor.on('delegation.create', (x, ctx) => delegation.insert(this.createIndexedDelegation(x, ctx), ctx));
395
- this.executor.on('delegation.update', (x, ctx) => delegation.update(x.address, x, ctx));
841
+ async _formatTx(tx, ctx) {
842
+ if (!tx) {
843
+ return tx;
844
+ }
845
+
846
+ if (!tx.sender && !tx.receiver) {
847
+ const typeUrl = fromTypeUrl(tx.tx.itx.typeUrl);
848
+ tx.sender = getTxSender({ tx: tx.tx, itx: tx.tx.itxJson, typeUrl });
849
+ tx.receiver = getTxReceiver({ tx: tx.tx, itx: tx.tx.itxJson, typeUrl });
850
+ }
851
+
852
+ if (!tx.receipts) {
853
+ tx.receipts = getTxReceipts(tx, { config: this.config });
854
+ }
855
+
856
+ // https://github.com/blocklet/abt-wallet/issues/681
857
+ if (get(tx, 'tx.itxJson.type_url') === 'fg:t:deposit_token') {
858
+ tx.receipts = getTxReceipts(tx, { config: this.config });
859
+ }
860
+
861
+ tx.receipts = mergeTxReceipts(tx.receipts);
862
+ tx.tokenSymbols = await this.getTxTokenSymbols(tx);
863
+
864
+ this.fixReceiptTokens(tx, ctx);
865
+ this.fixTokenSymbols(tx, ctx);
866
+
867
+ return tx;
396
868
  }
397
869
 
398
- createIndexedAccount(x) {
399
- return {
400
- address: x.address,
401
- balance: x.balance,
402
- genesisTime: x.context.genesisTime,
403
- migratedFrom: x.migratedFrom[0] || '',
404
- migratedTo: x.migratedTo[0] || '',
405
- moniker: x.moniker,
406
- nonce: x.nonce,
407
- numAssets: x.numAssets,
408
- numTxs: x.numTxs,
409
- recentNumTxs: [],
410
- renaissanceTime: x.context.renaissanceTime,
411
- totalReceivedStakes: '0',
412
- totalStakes: '0',
413
- totalUnstakes: '0',
414
- };
870
+ async formatTokenArray(tokens) {
871
+ const uniqTokens = uniqBy(tokens, 'address');
872
+ const tokenStates = await Promise.all(uniqTokens.map((token) => this.tokenCache.get(token.address)));
873
+
874
+ return uniqTokens.map((token) => ({ ...token, ...extractTokenMeta(token.address, tokenStates) }));
415
875
  }
416
876
 
417
- createIndexedAsset(x) {
418
- return {
419
- address: x.address,
420
- consumedTime: x.consumedTime,
421
- data: x.data,
422
- genesisTime: x.context.genesisTime,
423
- issuer: x.issuer,
424
- moniker: x.moniker,
425
- owner: x.owner,
426
- parent: x.parent,
427
- readonly: x.readonly,
428
- renaissanceTime: x.context.renaissanceTime,
429
- transferrable: x.transferrable,
430
- ttl: x.ttl,
431
- };
877
+ async formatTokenMap(tokens) {
878
+ if (isEmpty(tokens)) {
879
+ return [];
880
+ }
881
+
882
+ const tasks = Object.keys(tokens).map((address) => this.tokenCache.get(address));
883
+ const tokenStates = await Promise.all(tasks);
884
+
885
+ return Object.keys(tokens).map((address) => ({
886
+ value: tokens[address],
887
+ ...extractTokenMeta(address, tokenStates),
888
+ }));
432
889
  }
433
890
 
434
- createIndexedDelegation(x, ctx) {
435
- return {
436
- address: x.address,
437
- data: x.data,
438
- genesisTime: x.context.genesisTime,
439
- renaissanceTime: x.context.renaissanceTime,
440
- ops: x.ops,
441
- from: get(ctx, 'senderState.address', ''),
442
- to: get(ctx, 'receiverState.address', ''),
443
- };
891
+ async getTxTokenSymbols(tx) {
892
+ if (!tx) {
893
+ return [];
894
+ }
895
+
896
+ const rollupTxs = [
897
+ 'DepositTokenV2Tx',
898
+ 'WithdrawTokenV2Tx',
899
+ 'JoinRollupTx',
900
+ 'LeaveRollupTx',
901
+ 'CreateRollupBlockTx',
902
+ 'ClaimBlockRewardTx',
903
+ ];
904
+ const typeUrl = formatTxType(tx.tx.itxJson._type);
905
+ const tokens = [];
906
+
907
+ if (isDefaultTokenChanged(tx, this.config.token)) {
908
+ tokens.push(this.tokenItx);
909
+ }
910
+
911
+ if (typeUrl === 'AcquireAssetV2Tx') {
912
+ const factory = await this.indexdb.factory.get(tx.tx.itxJson.factory);
913
+ tokens.push(...factory.input.tokens);
914
+ } else if (typeUrl === 'ExchangeV2Tx') {
915
+ tokens.push(...tx.tx.itxJson.sender.tokens);
916
+ tokens.push(...tx.tx.itxJson.receiver.tokens);
917
+ } else if (typeUrl === 'TransferV2Tx') {
918
+ tokens.push(...tx.tx.itxJson.tokens);
919
+ } else if (['TransferV3Tx', 'AcquireAssetV3Tx', 'StakeTx'].includes(typeUrl)) {
920
+ tokens.push(
921
+ ...uniqBy(
922
+ tx.tx.itxJson.inputs.reduce((acc, x) => acc.concat(x.tokens), []),
923
+ 'address'
924
+ ).filter((x) => x.address)
925
+ );
926
+ } else if (typeUrl === 'RevokeStakeTx') {
927
+ tokens.push(
928
+ ...uniqBy(
929
+ tx.tx.itxJson.outputs.reduce((acc, x) => acc.concat(x.tokens), []),
930
+ 'address'
931
+ ).filter((x) => x.address)
932
+ );
933
+ } else if (typeUrl === 'ClaimStakeTx') {
934
+ const revokeTx = await this.indexdb.tx.get(tx.tx.itxJson.evidence.hash);
935
+ tokens.push(
936
+ ...uniqBy(
937
+ revokeTx.tx.itxJson.outputs.reduce((acc, x) => acc.concat(x.tokens), []),
938
+ 'address'
939
+ ).filter((x) => x.address)
940
+ );
941
+ } else if (rollupTxs.includes(typeUrl)) {
942
+ const rollup = await this.indexdb.rollup.get(tx.tx.itxJson.rollup);
943
+ tokens.push({ address: rollup.tokenAddress, value: '0' });
944
+ } else if (typeUrl === 'CreateTokenTx') {
945
+ return [extractTokenMeta(tx.tx.itxJson.address, [tx.tx.itxJson])];
946
+ }
947
+
948
+ if (tokens.length > 0) {
949
+ return this.formatTokenArray(tokens);
950
+ }
951
+
952
+ return [];
953
+ }
954
+
955
+ fixReceiptTokens(tx) {
956
+ // Auto populate initial token address in tx receipts
957
+ tx.receipts.forEach((receipt) =>
958
+ receipt.changes.forEach((x) => {
959
+ if (x.target === '') {
960
+ x.target = this.tokenItx.address;
961
+ }
962
+ })
963
+ );
964
+
965
+ return tx;
966
+ }
967
+
968
+ fixTokenSymbols(tx) {
969
+ const index = tx.tokenSymbols.findIndex((x) => x.address === this.tokenItx.address);
970
+ if (index === -1) {
971
+ tx.tokenSymbols.push(extractTokenMeta(this.tokenItx.address, [this.tokenItx]));
972
+ }
973
+
974
+ return tx;
975
+ }
976
+
977
+ enrichIndexContext(ctx) {
978
+ if (Array.isArray(ctx.tokenStates)) {
979
+ ctx.tokenStates.push(this.tokenItx);
980
+ } else if (ctx.tokenState) {
981
+ ctx.tokenStates = [ctx.tokenState];
982
+ }
983
+
984
+ return ctx;
444
985
  }
445
986
  };
446
987
 
447
988
  module.exports.formatData = formatData;
989
+ module.exports.formatDelegationOps = formatDelegationOps;