@ocap/resolver 1.28.8 → 1.29.0

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.
Files changed (43) hide show
  1. package/esm/api.d.mts +24 -0
  2. package/esm/api.mjs +53 -0
  3. package/esm/hooks.d.mts +153 -0
  4. package/esm/hooks.mjs +267 -0
  5. package/esm/index.d.mts +201 -0
  6. package/esm/index.mjs +1327 -0
  7. package/esm/migration-chain.d.mts +52 -0
  8. package/esm/migration-chain.mjs +97 -0
  9. package/esm/package.mjs +5 -0
  10. package/esm/token-cache.d.mts +20 -0
  11. package/esm/token-cache.mjs +26 -0
  12. package/esm/token-distribution.d.mts +166 -0
  13. package/esm/token-distribution.mjs +241 -0
  14. package/esm/token-flow.d.mts +139 -0
  15. package/esm/token-flow.mjs +330 -0
  16. package/esm/types.d.mts +115 -0
  17. package/esm/types.mjs +1 -0
  18. package/lib/_virtual/rolldown_runtime.cjs +29 -0
  19. package/lib/api.cjs +54 -0
  20. package/lib/api.d.cts +24 -0
  21. package/lib/hooks.cjs +274 -0
  22. package/lib/hooks.d.cts +153 -0
  23. package/lib/index.cjs +1343 -0
  24. package/lib/index.d.cts +201 -0
  25. package/lib/migration-chain.cjs +99 -0
  26. package/lib/migration-chain.d.cts +52 -0
  27. package/lib/package.cjs +11 -0
  28. package/lib/token-cache.cjs +27 -0
  29. package/lib/token-cache.d.cts +20 -0
  30. package/lib/token-distribution.cjs +243 -0
  31. package/lib/token-distribution.d.cts +166 -0
  32. package/lib/token-flow.cjs +336 -0
  33. package/lib/token-flow.d.cts +139 -0
  34. package/lib/types.cjs +0 -0
  35. package/lib/types.d.cts +115 -0
  36. package/package.json +49 -21
  37. package/lib/api.js +0 -71
  38. package/lib/hooks.js +0 -339
  39. package/lib/index.js +0 -1486
  40. package/lib/migration-chain.js +0 -144
  41. package/lib/token-cache.js +0 -40
  42. package/lib/token-distribution.js +0 -358
  43. package/lib/token-flow.js +0 -445
package/lib/token-flow.js DELETED
@@ -1,445 +0,0 @@
1
- const { BN, fromTokenToUnit, isSameDid } = require('@ocap/util');
2
- const { schemas, Joi } = require('@arcblock/validator');
3
- const { CustomError } = require('@ocap/util/lib/error');
4
- const uniq = require('lodash/uniq');
5
- const { FORGE_TOKEN_HOLDER } = require('@ocap/state/lib/states/tx');
6
-
7
- const ZERO = new BN(0);
8
- const paramsSchema = Joi.object({
9
- accountAddress: schemas.tokenHolder.required(),
10
- tokenAddress: Joi.DID().prefix().role('ROLE_TOKEN').required(),
11
- resolver: Joi.object().required(),
12
- });
13
-
14
- /**
15
- * Parse transfer in/out list from transaction
16
- * @param {*} tx
17
- * @param {String} tokenAddress
18
- * @returns {{
19
- * transferInList: { address: String, value: BN, action: String }[],
20
- * transferOutList: { address: String, value: BN, action: String }[]
21
- * }}
22
- */
23
- const getTransferList = (tx, tokenAddress) => {
24
- const transferInList = [];
25
- const transferOutList = [];
26
-
27
- // Parse receipt to get transfer in/out accounts
28
- for (const receipt of tx.receipts || []) {
29
- const changes = receipt.changes.filter((item) => item.target === tokenAddress && item.value !== '0');
30
-
31
- for (const change of changes) {
32
- const value = new BN(change.value);
33
- const item = {
34
- address: receipt.address,
35
- value: value.abs(),
36
- action: change.action,
37
- };
38
- if (value.lt(ZERO)) {
39
- transferOutList.push(item);
40
- } else {
41
- transferInList.push(item);
42
- }
43
- }
44
- }
45
-
46
- return { transferInList, transferOutList };
47
- };
48
-
49
- /**
50
- * Parse transfer flow from transaction
51
- * @param {*} tx
52
- * @param {String} tokenAddress
53
- * @returns {{ from: String, to: String, value: BN, hash: String }[]}
54
- */
55
- const getTransferFlow = (tx, tokenAddress) => {
56
- const { transferInList, transferOutList } = getTransferList(tx, tokenAddress);
57
- const txTransfers = [];
58
-
59
- // Match transfers between accounts
60
- for (const outItem of transferOutList) {
61
- if (outItem.archived) continue;
62
-
63
- // Try to match accounts with exactly equal transfer in/out amounts
64
- const matchedInItem = transferInList.find((x) => x.value.eq(outItem.value));
65
- if (matchedInItem) {
66
- txTransfers.push({
67
- from: outItem.address,
68
- to: matchedInItem.address,
69
- value: outItem.value,
70
- hash: tx.hash,
71
- });
72
- matchedInItem.archived = true;
73
- outItem.archived = true;
74
- continue;
75
- }
76
-
77
- for (const inItem of transferInList) {
78
- if (inItem.archived) continue;
79
- if (outItem.archived) continue;
80
-
81
- if (outItem.value.gt(inItem.value)) {
82
- txTransfers.push({
83
- from: outItem.address,
84
- to: inItem.address,
85
- value: inItem.value,
86
- hash: tx.hash,
87
- });
88
- inItem.archived = true;
89
- outItem.value = outItem.value.sub(inItem.value);
90
- continue;
91
- }
92
-
93
- if (outItem.value.lt(inItem.value)) {
94
- txTransfers.push({
95
- from: outItem.address,
96
- to: inItem.address,
97
- value: outItem.value,
98
- hash: tx.hash,
99
- });
100
- outItem.archived = true;
101
- inItem.value = inItem.value.sub(outItem.value);
102
- continue;
103
- }
104
-
105
- if (outItem.value.eq(inItem.value)) {
106
- txTransfers.push({
107
- from: outItem.address,
108
- to: inItem.address,
109
- value: inItem.value,
110
- hash: tx.hash,
111
- });
112
- inItem.archived = true;
113
- outItem.archived = true;
114
- }
115
- }
116
- }
117
-
118
- return txTransfers;
119
- };
120
-
121
- const getVaultAccounts = (config) => {
122
- return Object.values(config.vaults).flat().concat(FORGE_TOKEN_HOLDER);
123
- };
124
-
125
- const getInitialBalance = (address, config) => {
126
- const account = config?.accounts?.find((x) => isSameDid(x.address, address));
127
- return account ? fromTokenToUnit(account.balance) : ZERO;
128
- };
129
-
130
- const getBalance = async (address, tokenAddress, { resolver, txn }) => {
131
- const state = await resolver.statedb.account.get(address, { txn, traceMigration: false });
132
- if (!state) {
133
- throw new CustomError('INVALID_REQUEST', `Invalid address ${address}`);
134
- }
135
- const balance = state.tokens[tokenAddress] || 0;
136
- return balance;
137
- };
138
-
139
- const fixMigrateReceipts = async (tx, resolver) => {
140
- const migrationChain = await resolver.getMigrationChain();
141
- const txTime = new Date(tx.time);
142
-
143
- tx.receipts?.forEach((receipt) => {
144
- receipt.address = migrationChain.findAddressAtTime(receipt.address, txTime);
145
- });
146
- };
147
-
148
- const verifyAccountRisk = async (
149
- { accountAddress, tokenAddress, accountLimit = 400, txLimit = 10000, tolerance = '0.0000000001' },
150
- resolver,
151
- ctx = {}
152
- ) => {
153
- // validate request params
154
- const validation = paramsSchema.validate({ accountAddress, tokenAddress, resolver });
155
- if (validation.error) {
156
- throw new CustomError('INVALID_PARAMS', validation.error.message);
157
- }
158
-
159
- const { logger } = resolver;
160
- const checkedAccounts = new Map();
161
- const checkedTx = new Map();
162
- const tokenState = await resolver.tokenCache.get(tokenAddress);
163
- const toleranceUnit = fromTokenToUnit(tolerance, tokenState?.decimal);
164
- const vaultAccounts = getVaultAccounts(resolver.config);
165
-
166
- const accountQueue = [[{ address: accountAddress, chain: [] }]];
167
-
168
- const execute = async (depth, txn) => {
169
- const queue = accountQueue[depth];
170
-
171
- for (let i = 0; i < queue.length; i++) {
172
- // limit
173
- if (checkedAccounts.size >= accountLimit) {
174
- logger.warn('Account risk check reached max account size limit', {
175
- address: accountAddress,
176
- tokenAddress,
177
- accountCount: checkedAccounts.size,
178
- txCount: checkedTx.size,
179
- depth,
180
- accountLimit,
181
- txLimit,
182
- tolerance,
183
- });
184
- return {
185
- isRisky: false,
186
- reason: 'MAX_ACCOUNT_SIZE_LIMIT',
187
- data: {
188
- accountCount: checkedAccounts.size,
189
- txCount: checkedTx.size,
190
- },
191
- };
192
- }
193
-
194
- const { address, chain } = queue[i];
195
- chain.push(address);
196
-
197
- // Avoid circular query
198
- if (checkedAccounts.has(address)) continue;
199
-
200
- const trustedConfig = await resolver.filter?.getTrustedAccountConfig(address);
201
- // Skip trusted accounts that do not have tolerance configured
202
- if (trustedConfig && !trustedConfig.tolerance) {
203
- checkedAccounts.set(address, true);
204
- continue;
205
- }
206
-
207
- let balance = 0;
208
- let transactions = [];
209
-
210
- try {
211
- [balance, transactions] = await Promise.all([
212
- getBalance(address, tokenAddress, { resolver, txn }),
213
- resolver._getAllResults(
214
- 'transactions',
215
- (paging) => resolver.listTransactions({ paging, accountFilter: { accounts: [address] } }, ctx),
216
- txLimit
217
- ),
218
- ]);
219
- } catch (e) {
220
- // skip if tx limit exceeded
221
- if (e?.code === 'EXCEED_LIMIT') {
222
- logger.warn('Skip checking account cause tx count exceeding limit', { address, txLimit });
223
- checkedAccounts.set(address, true);
224
- continue;
225
- }
226
- throw e;
227
- }
228
-
229
- let transferIn = getInitialBalance(address, resolver.config);
230
- let transferOut = ZERO;
231
-
232
- // Parse txs to get transfer amounts
233
- for (const tx of transactions) {
234
- // cache tx
235
- if (!checkedTx.has(tx.hash)) {
236
- await fixMigrateReceipts(tx, resolver);
237
- checkedTx.set(tx.hash, getTransferList(tx, tokenAddress));
238
- }
239
- const { transferInList, transferOutList } = checkedTx.get(tx.hash);
240
-
241
- // Calculate the total amount of transfer for this address
242
- const transferInAmount = transferInList
243
- .filter((item) => isSameDid(item.address, address))
244
- .map((item) => item.value)
245
- .reduce((prev, cur) => prev.add(cur), ZERO);
246
-
247
- const transferOutAmount = transferOutList
248
- .filter((item) => isSameDid(item.address, address))
249
- .map((item) => item.value)
250
- .reduce((prev, cur) => prev.add(cur), ZERO);
251
-
252
- transferIn = transferIn.add(transferInAmount);
253
- transferOut = transferOut.add(transferOutAmount);
254
-
255
- // push transferIn accounts to queue for next time check
256
- if (transferInAmount.gt(ZERO)) {
257
- if (!accountQueue[depth + 1]) {
258
- accountQueue[depth + 1] = [];
259
- }
260
- const accountsToQueue = transferOutList
261
- .filter((item) => {
262
- if (checkedAccounts.has(item.address)) return false;
263
- // Skip not token holders
264
- if (schemas.tokenHolder.validate(item.address).error) return false;
265
- // Skip vault accounts
266
- if (vaultAccounts.includes(item.address)) return false;
267
- // skip gas、fee
268
- if (['gas', 'fee'].includes(item.action)) return false;
269
-
270
- return true;
271
- })
272
- .map((item) => item.address);
273
-
274
- accountQueue[depth + 1].push(...uniq(accountsToQueue).map((x) => ({ address: x, chain: chain.concat() })));
275
- }
276
- }
277
-
278
- checkedAccounts.set(address, true);
279
-
280
- // Check if the balance not matches the transfer records
281
- const diff = transferIn
282
- .sub(transferOut)
283
- .sub(new BN(balance))
284
- .add(fromTokenToUnit(trustedConfig?.tolerance || 0));
285
-
286
- if (diff.abs().gt(toleranceUnit)) {
287
- const data = {
288
- address: chain.join('->'),
289
- balance,
290
- transferIn: transferIn.toString(),
291
- transferOut: transferOut.toString(),
292
- accountCount: checkedAccounts.size,
293
- txCount: checkedTx.size,
294
- };
295
- logger.warn('Account balance does not match transfer records', {
296
- ...data,
297
- sourceAccount: accountAddress,
298
- tokenAddress,
299
- diff: diff.toString(),
300
- depth,
301
- accountLimit,
302
- txLimit,
303
- tolerance,
304
- });
305
- return {
306
- isRisky: true,
307
- reason: 'INVALID_BALANCE',
308
- data,
309
- };
310
- }
311
- }
312
- };
313
-
314
- for (let depth = 0; depth < accountQueue.length; depth++) {
315
- let isExecuted = false;
316
- const result = await resolver.runAsLambda(
317
- (txn) => {
318
- if (isExecuted) {
319
- throw new CustomError('INVALID_REQUEST', 'verifyAccountRisk should not retry');
320
- }
321
- isExecuted = true;
322
- return execute(depth, txn);
323
- },
324
- { retryLimit: 0 }
325
- );
326
- if (result) {
327
- return result;
328
- }
329
- }
330
-
331
- logger.info('Account risk check completed', {
332
- address: accountAddress,
333
- tokenAddress,
334
- accountCount: checkedAccounts.size,
335
- txCount: checkedTx.size,
336
- depth: accountQueue.length,
337
- accountLimit,
338
- txLimit,
339
- tolerance,
340
- });
341
-
342
- return {
343
- isRisky: false,
344
- data: {
345
- accountCount: checkedAccounts.size,
346
- txCount: checkedTx.size,
347
- },
348
- };
349
- };
350
-
351
- const listTokenFlows = async (
352
- { accountAddress, tokenAddress, paging = {}, depth = 2, direction = 'OUT' },
353
- resolver,
354
- ctx = {}
355
- ) => {
356
- // validate request params
357
- const { error } = paramsSchema.validate({ accountAddress, tokenAddress, resolver });
358
- if (error) {
359
- throw new CustomError('INVALID_PARAMS', error.message);
360
- }
361
-
362
- const tokenState = await resolver.tokenCache.get(tokenAddress);
363
- const minAmount = fromTokenToUnit(1, tokenState?.decimal || 18);
364
- const maxAccountSize = Math.min(200, paging.size || 200);
365
- const maxDepth = Math.min(5, depth);
366
- const vaultAccounts = getVaultAccounts(resolver.config);
367
-
368
- const tokenFlows = [];
369
- const checkedAccounts = new Map();
370
- const checkedTx = new Map();
371
-
372
- let curDepth = 1;
373
-
374
- const depthQueue = {
375
- [curDepth]: [accountAddress],
376
- };
377
-
378
- while (depthQueue[curDepth]?.length && curDepth <= maxDepth && checkedAccounts.size < maxAccountSize) {
379
- for (const address of depthQueue[curDepth]) {
380
- // Avoid circular query
381
- if (checkedAccounts.has(address)) continue;
382
- // Skip not token holders
383
- if (schemas.tokenHolder.validate(address).error) continue;
384
-
385
- const transactions = await resolver._getAllResults('transactions', (page) =>
386
- resolver.listTransactions(
387
- { paging: page, accountFilter: { accounts: [address] }, tokenFilter: { tokens: [tokenAddress] } },
388
- ctx
389
- )
390
- );
391
- let accountsToQueue = [];
392
-
393
- for (const tx of transactions) {
394
- // cache tx
395
- if (!checkedTx.has(tx.hash)) {
396
- await fixMigrateReceipts(tx, resolver);
397
- checkedTx.set(tx.hash, await getTransferFlow(tx, tokenAddress));
398
- }
399
- const txTransfers = checkedTx.get(tx.hash).filter((item) => {
400
- if (direction === 'OUT' && item.from !== address) return false;
401
- if (direction === 'IN' && item.to !== address) return false;
402
- // Skip too small amount
403
- if (item.value.lt(minAmount)) return false;
404
- return true;
405
- });
406
-
407
- // push to result
408
- tokenFlows.push(...txTransfers.map((item) => ({ ...item, value: item.value.toString() })));
409
-
410
- // push to end of queue
411
- accountsToQueue = accountsToQueue.concat(
412
- txTransfers
413
- .map((item) => (direction === 'IN' ? item.from : item.to))
414
- // Skip vault accounts
415
- .filter((item) => !vaultAccounts.includes(item))
416
- );
417
- }
418
-
419
- // cache account
420
- checkedAccounts.set(address, true);
421
- // limit
422
- if (checkedAccounts.size >= maxAccountSize) {
423
- break;
424
- }
425
-
426
- // push to queue for next depth
427
- if (!depthQueue[curDepth + 1]) {
428
- depthQueue[curDepth + 1] = [];
429
- }
430
- depthQueue[curDepth + 1].push(...uniq(accountsToQueue));
431
- }
432
-
433
- curDepth++;
434
- }
435
-
436
- return tokenFlows;
437
- };
438
-
439
- module.exports = {
440
- getTransferList,
441
- getTransferFlow,
442
- verifyAccountRisk,
443
- listTokenFlows,
444
- fixMigrateReceipts,
445
- };