@ocap/resolver 1.19.1 → 1.19.3
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/hooks.js +8 -1
- package/lib/index.js +159 -0
- package/lib/token-distribution.js +334 -0
- package/package.json +12 -12
package/lib/hooks.js
CHANGED
|
@@ -295,7 +295,14 @@ const onAccountMigrate = async (tx, resolver) => {
|
|
|
295
295
|
}
|
|
296
296
|
};
|
|
297
297
|
|
|
298
|
-
const onCreateTx = (tx, ctx, resolver) => {
|
|
298
|
+
const onCreateTx = async (tx, ctx, resolver) => {
|
|
299
|
+
// Always update distribution even if tx failed cause there might be had gas
|
|
300
|
+
try {
|
|
301
|
+
await resolver.tokenDistribution.updateByTx(tx, ctx);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
resolver.logger.error('Failed to update token distribution', e);
|
|
304
|
+
}
|
|
305
|
+
|
|
299
306
|
if (tx.code !== 'OK') {
|
|
300
307
|
return;
|
|
301
308
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-loop-func */
|
|
1
2
|
/* eslint-disable no-await-in-loop */
|
|
2
3
|
/* eslint-disable newline-per-chained-call */
|
|
3
4
|
const get = require('lodash/get');
|
|
@@ -20,6 +21,7 @@ const { BN, toBN, fromTokenToUnit, toAddress } = require('@ocap/util');
|
|
|
20
21
|
const { DEFAULT_TOKEN_DECIMAL } = require('@ocap/util/lib/constant');
|
|
21
22
|
const { createExecutor } = require('@ocap/tx-protocols');
|
|
22
23
|
const { decodeAnySafe } = require('@ocap/tx-protocols/lib/util');
|
|
24
|
+
const { Joi } = require('@arcblock/validator');
|
|
23
25
|
|
|
24
26
|
const {
|
|
25
27
|
createIndexedAccount,
|
|
@@ -48,6 +50,7 @@ const hooks = require('./hooks');
|
|
|
48
50
|
const tokenFlow = require('./token-flow');
|
|
49
51
|
const { getInstance: getTokenCacheInstance } = require('./token-cache');
|
|
50
52
|
const { MigrationChainManager } = require('./migration-chain');
|
|
53
|
+
const { TokenDistributionManager } = require('./token-distribution');
|
|
51
54
|
|
|
52
55
|
const noop = (x) => x;
|
|
53
56
|
const CHAIN_ADDR = md5('OCAP_CHAIN_ADDR');
|
|
@@ -183,6 +186,7 @@ module.exports = class OCAPResolver {
|
|
|
183
186
|
|
|
184
187
|
if (indexdb) {
|
|
185
188
|
this.tokenCache = getTokenCacheInstance(indexdb.token);
|
|
189
|
+
this.tokenDistribution = new TokenDistributionManager(this);
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
if (this.tokenItx) {
|
|
@@ -842,6 +846,9 @@ module.exports = class OCAPResolver {
|
|
|
842
846
|
}
|
|
843
847
|
} catch (error) {
|
|
844
848
|
console.error('create tx index failed', { account: x, error });
|
|
849
|
+
} finally {
|
|
850
|
+
// eslint-disable-next-line no-console
|
|
851
|
+
console.log(x);
|
|
845
852
|
}
|
|
846
853
|
});
|
|
847
854
|
|
|
@@ -1242,6 +1249,158 @@ module.exports = class OCAPResolver {
|
|
|
1242
1249
|
|
|
1243
1250
|
return updatedTxs.filter(Boolean);
|
|
1244
1251
|
}
|
|
1252
|
+
|
|
1253
|
+
async updateTokenDistribution({ tokenAddress = this.config.token.address, force = false } = {}) {
|
|
1254
|
+
const paramsSchema = Joi.object({
|
|
1255
|
+
tokenAddress: Joi.DID().prefix().role('ROLE_TOKEN').required(),
|
|
1256
|
+
force: Joi.boolean().required(),
|
|
1257
|
+
});
|
|
1258
|
+
const validation = paramsSchema.validate({ tokenAddress, force });
|
|
1259
|
+
if (validation.error) {
|
|
1260
|
+
throw new CustomError('INVALID_PARAMS', validation.error.message);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const data = await this.tokenDistribution.updateByToken(tokenAddress, force);
|
|
1264
|
+
return data;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async getTokenDistribution({ tokenAddress = this.config.token.address }) {
|
|
1268
|
+
const data = await this.tokenDistribution.getDistribution(tokenAddress);
|
|
1269
|
+
return data;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Fetch data in chunks to handle large datasets
|
|
1274
|
+
* @param {Function} fn Function to fetch data
|
|
1275
|
+
* @param {Object} param
|
|
1276
|
+
* @param {number} [param.concurrency=3] Number of concurrent requests
|
|
1277
|
+
* @param {number} [param.chunkSize=2000] Maximum number of items to return in each next() call
|
|
1278
|
+
* @param {number} [param.pageSize=100] Number of items per page
|
|
1279
|
+
* @param {Function} param.getTime Function to get timestamp from item
|
|
1280
|
+
* @param {string} param.dataKey Key to access data in response
|
|
1281
|
+
* @returns {Promise<Array>} Array of fetched items
|
|
1282
|
+
*/
|
|
1283
|
+
listChunks(fn, { concurrency = 3, chunkSize = 2000, pageSize = 100, getTime, dataKey }) {
|
|
1284
|
+
let totalPage;
|
|
1285
|
+
let curPage;
|
|
1286
|
+
let done = false;
|
|
1287
|
+
let time;
|
|
1288
|
+
|
|
1289
|
+
const fetchFirstPage = async () => {
|
|
1290
|
+
const { paging, page, [dataKey]: list } = await fn({ size: pageSize, cursor: '0' });
|
|
1291
|
+
const total = paging?.total || page?.total;
|
|
1292
|
+
curPage = 1;
|
|
1293
|
+
totalPage = Math.ceil(total / pageSize);
|
|
1294
|
+
return list;
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
const next = async () => {
|
|
1298
|
+
if (done) {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
let results = [];
|
|
1303
|
+
|
|
1304
|
+
// first page
|
|
1305
|
+
if (!totalPage) {
|
|
1306
|
+
const data = await fetchFirstPage();
|
|
1307
|
+
// only 1 page
|
|
1308
|
+
if (data.length < pageSize) {
|
|
1309
|
+
done = true;
|
|
1310
|
+
return data;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
time = getTime(data[data.length - 1]);
|
|
1314
|
+
results = results.concat(data);
|
|
1315
|
+
|
|
1316
|
+
// limit
|
|
1317
|
+
if (data.length >= chunkSize) {
|
|
1318
|
+
return results;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// next pages
|
|
1323
|
+
for (; curPage < totalPage; curPage += concurrency) {
|
|
1324
|
+
const batchResults = await Promise.all(
|
|
1325
|
+
new Array(concurrency).fill(true).map(async (_, i) => {
|
|
1326
|
+
const { [dataKey]: list } = await fn({
|
|
1327
|
+
size: pageSize,
|
|
1328
|
+
cursor: i * pageSize,
|
|
1329
|
+
time,
|
|
1330
|
+
});
|
|
1331
|
+
return list;
|
|
1332
|
+
})
|
|
1333
|
+
);
|
|
1334
|
+
const flatResults = batchResults.flat();
|
|
1335
|
+
|
|
1336
|
+
// finish
|
|
1337
|
+
if (!flatResults.length) {
|
|
1338
|
+
done = true;
|
|
1339
|
+
return [];
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
results = results.concat(flatResults);
|
|
1343
|
+
time = results.length ? getTime(results[results.length - 1]) : null;
|
|
1344
|
+
|
|
1345
|
+
// limit
|
|
1346
|
+
if (results.length >= chunkSize) {
|
|
1347
|
+
return results;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
done = true;
|
|
1352
|
+
return results;
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
return {
|
|
1356
|
+
next,
|
|
1357
|
+
done,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
listTransactionsChunks(args = {}, { chunkSize = 2000, pageSize = 100 } = {}) {
|
|
1362
|
+
return this.listChunks(
|
|
1363
|
+
({ size, time, cursor }) =>
|
|
1364
|
+
this.listTransactions({
|
|
1365
|
+
...args,
|
|
1366
|
+
paging: {
|
|
1367
|
+
order: { field: 'time', type: 'asc' },
|
|
1368
|
+
...(args.paging || {}),
|
|
1369
|
+
size,
|
|
1370
|
+
cursor,
|
|
1371
|
+
},
|
|
1372
|
+
timeFilter: Object.assign(args.timeFilter || {}, time ? { startDateTime: time } : {}),
|
|
1373
|
+
}),
|
|
1374
|
+
{
|
|
1375
|
+
dataKey: 'transactions',
|
|
1376
|
+
getTime: (tx) => tx?.time,
|
|
1377
|
+
chunkSize,
|
|
1378
|
+
pageSize,
|
|
1379
|
+
}
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
listStakeChunks(args = {}, { chunkSize = 2000, pageSize = 100 } = {}) {
|
|
1384
|
+
return this.listChunks(
|
|
1385
|
+
({ size, time, cursor }) =>
|
|
1386
|
+
this.listStakes({
|
|
1387
|
+
...args,
|
|
1388
|
+
paging: {
|
|
1389
|
+
order: { field: 'time', type: 'asc' },
|
|
1390
|
+
...(args.paging || {}),
|
|
1391
|
+
size,
|
|
1392
|
+
cursor,
|
|
1393
|
+
},
|
|
1394
|
+
timeFilter: Object.assign(args.timeFilter || {}, time ? { startDateTime: time } : {}),
|
|
1395
|
+
}),
|
|
1396
|
+
{
|
|
1397
|
+
dataKey: 'stakes',
|
|
1398
|
+
getTime: (state) => state?.time,
|
|
1399
|
+
chunkSize,
|
|
1400
|
+
pageSize,
|
|
1401
|
+
}
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1245
1404
|
};
|
|
1246
1405
|
|
|
1247
1406
|
module.exports.formatData = formatData;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/* eslint-disable no-loop-func */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
3
|
+
/* eslint-disable consistent-return */
|
|
4
|
+
const { BN, isSameDid, fromTokenToUnit } = require('@ocap/util');
|
|
5
|
+
const { toTypeInfo } = require('@arcblock/did');
|
|
6
|
+
const {
|
|
7
|
+
types: { RoleType },
|
|
8
|
+
} = require('@ocap/mcrypto');
|
|
9
|
+
const { createIndexedTokenDistribution } = require('@ocap/indexdb/lib/util');
|
|
10
|
+
const { eachReceipts } = require('@ocap/state/lib/states/tx');
|
|
11
|
+
const { isGasStakeAddress } = require('@ocap/tx-protocols/lib/util');
|
|
12
|
+
|
|
13
|
+
const ZERO = new BN(0);
|
|
14
|
+
|
|
15
|
+
class TokenDistributionManager {
|
|
16
|
+
constructor(resolver) {
|
|
17
|
+
this.resolver = resolver;
|
|
18
|
+
this.indexdb = resolver.indexdb;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
formatDistribution(distribution) {
|
|
22
|
+
const { tokenAddress, account, gas, fee, slashedVault, stake, revokedStake, gasStake, other, txTime } =
|
|
23
|
+
distribution;
|
|
24
|
+
return {
|
|
25
|
+
tokenAddress,
|
|
26
|
+
account: new BN(account || 0),
|
|
27
|
+
gas: new BN(gas || 0),
|
|
28
|
+
fee: new BN(fee || 0),
|
|
29
|
+
slashedVault: new BN(slashedVault || 0),
|
|
30
|
+
stake: new BN(stake || 0),
|
|
31
|
+
revokedStake: new BN(revokedStake || 0),
|
|
32
|
+
gasStake: new BN(gasStake || 0),
|
|
33
|
+
other: new BN(other || 0),
|
|
34
|
+
txTime: txTime || new Date(0).toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getDistribution(tokenAddress) {
|
|
39
|
+
const data = await this.indexdb.tokenDistribution.get(tokenAddress);
|
|
40
|
+
return data && createIndexedTokenDistribution(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async saveDistribution(distribution) {
|
|
44
|
+
const data = createIndexedTokenDistribution(distribution);
|
|
45
|
+
const indexdbDistribution = await this.getDistribution(data.tokenAddress);
|
|
46
|
+
|
|
47
|
+
if (!indexdbDistribution) {
|
|
48
|
+
await this.indexdb.tokenDistribution.insert(data);
|
|
49
|
+
} else {
|
|
50
|
+
// ensure txTime is latest
|
|
51
|
+
const latestTime = Math.max(new Date(indexdbDistribution.txTime).getTime(), new Date(data.txTime).getTime());
|
|
52
|
+
data.txTime = new Date(latestTime).toISOString();
|
|
53
|
+
await this.indexdb.tokenDistribution.update(data.tokenAddress, data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate token distribution based on all transaction data.
|
|
61
|
+
* This method is usually used to update historical token distribution data
|
|
62
|
+
*
|
|
63
|
+
* @param {string} tokenAddress Token address
|
|
64
|
+
* @param {boolean} force If force is false, only calculate distributions for new transactions after txTime in indexdb. default is false
|
|
65
|
+
* @returns {Promise<Object>}
|
|
66
|
+
*/
|
|
67
|
+
async updateByToken(tokenAddress, force) {
|
|
68
|
+
const { logger, config } = this.resolver;
|
|
69
|
+
|
|
70
|
+
const distribution = force
|
|
71
|
+
? this.formatDistribution({ tokenAddress })
|
|
72
|
+
: this.formatDistribution((await this.getDistribution(tokenAddress)) || { tokenAddress });
|
|
73
|
+
const isDefaultToken = tokenAddress === config.token.address;
|
|
74
|
+
|
|
75
|
+
logger?.info(`Update distribution by token (${tokenAddress})`, {
|
|
76
|
+
distribution: createIndexedTokenDistribution(distribution),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (force && isDefaultToken) {
|
|
80
|
+
this.handleModerator(distribution);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { next } = await this.resolver.listTransactionsChunks({
|
|
84
|
+
timeFilter: { startDateTime: distribution.txTime },
|
|
85
|
+
// Default token is used for gas payment and may appear in any transaction, so we cannot filter by tokenFilter
|
|
86
|
+
tokenFilter: isDefaultToken ? {} : { tokenFilter: { tokens: [tokenAddress] } },
|
|
87
|
+
});
|
|
88
|
+
let nextData = await next();
|
|
89
|
+
|
|
90
|
+
// Process transactions in chunks and update indexdb
|
|
91
|
+
while (nextData.length) {
|
|
92
|
+
logger?.info('Updating token distribution in chunks', {
|
|
93
|
+
chunkSize: nextData.length,
|
|
94
|
+
startTime: nextData[0].time,
|
|
95
|
+
startHash: nextData[0].hash,
|
|
96
|
+
endTime: nextData[nextData.length - 1].time,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const handlePromises = nextData.map((tx) => this.handleTx(tx, distribution));
|
|
100
|
+
await Promise.all(handlePromises);
|
|
101
|
+
|
|
102
|
+
// update indexdb
|
|
103
|
+
await this.saveDistribution(distribution);
|
|
104
|
+
nextData = await next();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// We cannot distinguish between revokedStake and stake from tx receipts here,
|
|
108
|
+
// so we need to read all stake transactions and recalculate token distribution based on their revokeTokens and tokens
|
|
109
|
+
await this.splitStake(distribution, force);
|
|
110
|
+
await this.saveDistribution(distribution);
|
|
111
|
+
|
|
112
|
+
const result = createIndexedTokenDistribution(distribution);
|
|
113
|
+
|
|
114
|
+
logger.info(`Token distribution update completed (${tokenAddress})`, { distribution: result });
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Split out revokedStake / gasStake from stake
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} distribution
|
|
123
|
+
* @param {boolean} force If force is false, only process transactions after txTime in indexdb. Default is false
|
|
124
|
+
* @returns {Promise<Object>}
|
|
125
|
+
*/
|
|
126
|
+
async splitStake(distribution, force) {
|
|
127
|
+
const { tokenAddress } = distribution;
|
|
128
|
+
|
|
129
|
+
const { next } = await this.resolver.listStakeChunks({
|
|
130
|
+
timeFilter: !force ? { startDateTime: distribution.txTime } : {},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let nextData = await next();
|
|
134
|
+
|
|
135
|
+
// Process transactions in chunks and update indexdb
|
|
136
|
+
while (nextData.length) {
|
|
137
|
+
nextData.forEach((stakeState) => {
|
|
138
|
+
const isGasStake = this.isGasStake(stakeState);
|
|
139
|
+
const token = stakeState.tokens.find((x) => x.address === tokenAddress);
|
|
140
|
+
const revokedToken = stakeState.revokedTokens.find((x) => x.address === tokenAddress);
|
|
141
|
+
const revokedBalance = new BN(revokedToken?.balance || 0);
|
|
142
|
+
const balance = new BN(token?.balance || 0);
|
|
143
|
+
|
|
144
|
+
// stake -->> revokedStake
|
|
145
|
+
distribution.revokedStake = distribution.revokedStake.add(revokedBalance);
|
|
146
|
+
distribution.stake = distribution.stake.sub(revokedBalance);
|
|
147
|
+
|
|
148
|
+
// stake -->> gasStake
|
|
149
|
+
if (isGasStake) {
|
|
150
|
+
distribution.gasStake = distribution.gasStake.add(balance);
|
|
151
|
+
distribution.stake = distribution.stake.sub(balance);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// continue
|
|
156
|
+
nextData = await next();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return distribution;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Update token distribution by a single transaction.
|
|
164
|
+
* This method is usually used when a tx is completed.
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} tx The transaction object
|
|
167
|
+
* @param {Object} context The transaction context
|
|
168
|
+
* @returns {Promise<Object>} The updated token distributions
|
|
169
|
+
*/
|
|
170
|
+
async updateByTx(tx, context) {
|
|
171
|
+
const formattedTx = await this.resolver.formatTx(tx);
|
|
172
|
+
const tokens = formattedTx.tokenSymbols.map(({ address }) => address);
|
|
173
|
+
|
|
174
|
+
// Parse all tokens in this transaction
|
|
175
|
+
const results = await Promise.all(
|
|
176
|
+
tokens.map(async (tokenAddress) => {
|
|
177
|
+
const distribution = this.formatDistribution((await this.getDistribution(tokenAddress)) || { tokenAddress });
|
|
178
|
+
await this.handleTx(tx, distribution, { context, isHandleStake: true });
|
|
179
|
+
const data = await this.saveDistribution(distribution);
|
|
180
|
+
return data;
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse token distribution for a single transaction
|
|
189
|
+
* @param {Object} tx
|
|
190
|
+
* @param {String} distribution
|
|
191
|
+
* @param {Object} param
|
|
192
|
+
* @param {Object} param.context
|
|
193
|
+
* @param {Boolean} param.isHandleStake Whether to handle revoked / gas stake tokens
|
|
194
|
+
* @returns {Promise<Object>} token distribution
|
|
195
|
+
*/
|
|
196
|
+
async handleTx(tx, distribution, { context, isHandleStake = false } = {}) {
|
|
197
|
+
if (isHandleStake && !context) {
|
|
198
|
+
throw new Error('FORBIDDEN', 'context is missing, handle revoke stake is not supported without context');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const type = tx.tx?.itxJson?._type;
|
|
202
|
+
const receipts = tx.receipts || [];
|
|
203
|
+
const { tokenAddress } = distribution;
|
|
204
|
+
|
|
205
|
+
// Iterate through each transaction receipt and distribute tokens based on account type
|
|
206
|
+
const handlePromises = eachReceipts(receipts, async (address, change) => {
|
|
207
|
+
if (!isSameDid(change.target, tokenAddress)) return;
|
|
208
|
+
// In migrate tx, tokens are frozen for the old account and minted for the new account
|
|
209
|
+
// It behaves like minting new tokens in receipts
|
|
210
|
+
// So we should skip here
|
|
211
|
+
if (change.action === 'migrate') return;
|
|
212
|
+
|
|
213
|
+
const roleType = toTypeInfo(address).role;
|
|
214
|
+
const value = new BN(change.value);
|
|
215
|
+
|
|
216
|
+
// Stake
|
|
217
|
+
if (roleType === RoleType.ROLE_STAKE) {
|
|
218
|
+
if (isHandleStake) {
|
|
219
|
+
const stakeState = await this.getStakeState(address, context);
|
|
220
|
+
const isGasStake = this.isGasStake(stakeState);
|
|
221
|
+
// stake revokedTokens -->> account tokens
|
|
222
|
+
if (type === 'ClaimStakeTx') {
|
|
223
|
+
distribution.revokedStake = distribution.revokedStake.add(value);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// stake tokens + stake revokedTokens -->> account tokens
|
|
227
|
+
if (type === 'SlashStakeTx') {
|
|
228
|
+
const beforeState = context.stateSnapshot[address];
|
|
229
|
+
const afterState = context.stakeState;
|
|
230
|
+
if (!beforeState || !afterState) {
|
|
231
|
+
throw new Error('INVALID_TOKEN_DISTRIBUTION', 'stake state is missing on slash stake tx');
|
|
232
|
+
}
|
|
233
|
+
const revokeTokenDiff = new BN(beforeState.revokedTokens[tokenAddress]).sub(
|
|
234
|
+
new BN(afterState.revokedTokens[tokenAddress])
|
|
235
|
+
);
|
|
236
|
+
const tokenDiff = new BN(beforeState.tokens[tokenAddress]).sub(new BN(afterState.tokens[tokenAddress]));
|
|
237
|
+
|
|
238
|
+
distribution.revokedStake = distribution.revokedStake.sub(revokeTokenDiff);
|
|
239
|
+
if (isGasStake) {
|
|
240
|
+
distribution.gasStake = distribution.gasStake.sub(tokenDiff);
|
|
241
|
+
} else {
|
|
242
|
+
distribution.stake = distribution.stake.sub(tokenDiff);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// gasStake tokens -->> account tokens
|
|
247
|
+
if (isGasStake) {
|
|
248
|
+
distribution.gasStake = distribution.gasStake.add(value);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
distribution.stake = distribution.stake.add(value);
|
|
253
|
+
}
|
|
254
|
+
// Gas
|
|
255
|
+
else if (change.action === 'gas' && value.gt(ZERO)) {
|
|
256
|
+
distribution.gas = distribution.gas.add(value);
|
|
257
|
+
}
|
|
258
|
+
// Fee
|
|
259
|
+
else if (change.action === 'fee' && value.gt(ZERO)) {
|
|
260
|
+
distribution.fee = distribution.fee.add(value);
|
|
261
|
+
}
|
|
262
|
+
// SlashVault
|
|
263
|
+
else if (
|
|
264
|
+
type === 'SlashStakeTx' &&
|
|
265
|
+
value.gt(ZERO) &&
|
|
266
|
+
isSameDid(address, this.resolver.config.vaults.slashedStake)
|
|
267
|
+
) {
|
|
268
|
+
distribution.slashedVault = distribution.slashedVault.add(value);
|
|
269
|
+
}
|
|
270
|
+
// Others: Account
|
|
271
|
+
else {
|
|
272
|
+
distribution.account = distribution.account.add(value);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await Promise.all(handlePromises);
|
|
277
|
+
|
|
278
|
+
if (isHandleStake && type === 'RevokeStakeTx') {
|
|
279
|
+
// RevokedStake has no receipts
|
|
280
|
+
// stake tokens -->> stake revokedTokens
|
|
281
|
+
const { address } = tx.tx.itxJson;
|
|
282
|
+
const stakeState = await this.getStakeState(address, context);
|
|
283
|
+
const isGasStake = this.isGasStake(stakeState);
|
|
284
|
+
|
|
285
|
+
tx.tx.itxJson.outputs.forEach((x) => {
|
|
286
|
+
x.tokens
|
|
287
|
+
.filter((change) => isSameDid(change.address, tokenAddress))
|
|
288
|
+
.forEach((change) => {
|
|
289
|
+
const value = new BN(change.value);
|
|
290
|
+
distribution.revokedStake = distribution.revokedStake.add(value);
|
|
291
|
+
if (isGasStake) {
|
|
292
|
+
distribution.gasStake = distribution.gasStake.sub(value);
|
|
293
|
+
} else {
|
|
294
|
+
distribution.stake = distribution.stake.sub(value);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
distribution.txTime = tx.time;
|
|
301
|
+
|
|
302
|
+
return distribution;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
handleModerator(distribution) {
|
|
306
|
+
const { config } = this.resolver;
|
|
307
|
+
|
|
308
|
+
if (distribution.tokenAddress !== config.token.address) {
|
|
309
|
+
return distribution;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const value = config.accounts
|
|
313
|
+
.filter((account) => account.balance > 0)
|
|
314
|
+
.map((account) => new BN(account.balance))
|
|
315
|
+
.reduce((cur, balance) => cur.add(balance), ZERO);
|
|
316
|
+
|
|
317
|
+
distribution.account = distribution.account.add(fromTokenToUnit(value.toString()));
|
|
318
|
+
return distribution;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async getStakeState(address, ctx) {
|
|
322
|
+
const stakeState =
|
|
323
|
+
ctx?.stateSnapshot?.[address] ||
|
|
324
|
+
(await this.resolver.runAsLambda((txn) => this.resolver.statedb.stake.get(address, { txn })));
|
|
325
|
+
|
|
326
|
+
return stakeState;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
isGasStake(stakeState) {
|
|
330
|
+
return isGasStakeAddress(stakeState.sender, stakeState.address) && stakeState.message === 'stake-for-gas';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { TokenDistributionManager };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.19.
|
|
6
|
+
"version": "1.19.3",
|
|
7
7
|
"description": "GraphQL resolver built upon ocap statedb and GQL layer",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"jest": "^29.7.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@arcblock/did": "1.19.
|
|
26
|
-
"@arcblock/did-util": "1.19.
|
|
27
|
-
"@arcblock/validator": "1.19.
|
|
28
|
-
"@ocap/config": "1.19.
|
|
29
|
-
"@ocap/indexdb": "1.19.
|
|
30
|
-
"@ocap/mcrypto": "1.19.
|
|
31
|
-
"@ocap/message": "1.19.
|
|
32
|
-
"@ocap/state": "1.19.
|
|
33
|
-
"@ocap/tx-protocols": "1.19.
|
|
34
|
-
"@ocap/util": "1.19.
|
|
25
|
+
"@arcblock/did": "1.19.3",
|
|
26
|
+
"@arcblock/did-util": "1.19.3",
|
|
27
|
+
"@arcblock/validator": "1.19.3",
|
|
28
|
+
"@ocap/config": "1.19.3",
|
|
29
|
+
"@ocap/indexdb": "1.19.3",
|
|
30
|
+
"@ocap/mcrypto": "1.19.3",
|
|
31
|
+
"@ocap/message": "1.19.3",
|
|
32
|
+
"@ocap/state": "1.19.3",
|
|
33
|
+
"@ocap/tx-protocols": "1.19.3",
|
|
34
|
+
"@ocap/util": "1.19.3",
|
|
35
35
|
"debug": "^4.3.6",
|
|
36
36
|
"lodash": "^4.17.21"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "756076dad0df7468beecc95c8effd55f8c4c4f49"
|
|
39
39
|
}
|