@ocap/resolver 1.18.140 → 1.18.143

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
@@ -20,6 +20,7 @@ const { BN, toBN, fromTokenToUnit, toAddress } = require('@ocap/util');
20
20
  const { DEFAULT_TOKEN_DECIMAL } = require('@ocap/util/lib/constant');
21
21
  const { createExecutor } = require('@ocap/tx-protocols');
22
22
  const { decodeAnySafe } = require('@ocap/tx-protocols/lib/util');
23
+
23
24
  const {
24
25
  createIndexedAccount,
25
26
  createIndexedAsset,
@@ -44,6 +45,7 @@ const {
44
45
 
45
46
  const debug = require('debug')(require('../package.json').name);
46
47
  const hooks = require('./hooks');
48
+ const tokenFlow = require('./token-flow');
47
49
  const { getInstance: getTokenCacheInstance } = require('./token-cache');
48
50
 
49
51
  const noop = (x) => x;
@@ -677,6 +679,14 @@ module.exports = class OCAPResolver {
677
679
  return this._doPaginatedSearch('listDelegations', args, 'delegations', 'data', ctx);
678
680
  }
679
681
 
682
+ async verifyAccountRisk(args, ctx) {
683
+ return tokenFlow.verifyAccountRisk(args, this, ctx);
684
+ }
685
+
686
+ async listTokenFlows(args, ctx) {
687
+ return tokenFlow.listTokenFlows(args, this, ctx);
688
+ }
689
+
680
690
  async search(args) {
681
691
  if (!args.keyword) {
682
692
  return { results: [] };
@@ -966,6 +976,27 @@ module.exports = class OCAPResolver {
966
976
  return attachPaidTxGas(tx);
967
977
  }
968
978
 
979
+ async _getAllResults(dataKey, fn) {
980
+ const results = [];
981
+ const pageSize = 100;
982
+
983
+ const { paging, [dataKey]: firstPage } = await fn({ size: pageSize });
984
+ if (paging.total < pageSize) {
985
+ return firstPage;
986
+ }
987
+
988
+ results.push(...firstPage);
989
+
990
+ const total = Math.floor(paging.total / pageSize);
991
+ for (let i = 1; i <= total; i++) {
992
+ // eslint-disable-next-line no-await-in-loop
993
+ const { [dataKey]: nextPage } = await fn({ size: pageSize, cursor: i * pageSize });
994
+ results.push(...nextPage);
995
+ }
996
+
997
+ return results;
998
+ }
999
+
969
1000
  async formatTokenArray(tokens) {
970
1001
  const uniqTokens = uniqBy(tokens, 'address');
971
1002
  const tokenStates = await Promise.all(uniqTokens.map((token) => this.tokenCache.get(token.address)));
@@ -0,0 +1,330 @@
1
+ /* eslint-disable no-await-in-loop */
2
+
3
+ const { BN, fromTokenToUnit } = require('@ocap/util');
4
+ const { schemas, Joi } = require('@arcblock/validator');
5
+ const { CustomError } = require('@ocap/util/lib/error');
6
+ const uniq = require('lodash/uniq');
7
+ const debug = require('debug')(require('../package.json').name);
8
+
9
+ const ZERO = new BN(0);
10
+ const paramsSchema = Joi.object({
11
+ accountAddress: schemas.tokenHolder.required(),
12
+ tokenAddress: Joi.DID().prefix().role('ROLE_TOKEN').required(),
13
+ resolver: Joi.object().required(),
14
+ });
15
+
16
+ /**
17
+ * Parse transfer in/out list from transaction
18
+ * @param {*} tx
19
+ * @param {String} tokenAddress
20
+ * @returns {{
21
+ * transferInList: { address: String, value: BN, action: String }[],
22
+ * transferOutList: { address: String, value: BN, action: String }[]
23
+ * }}
24
+ */
25
+ const getTransferList = (tx, tokenAddress) => {
26
+ const transferInList = [];
27
+ const transferOutList = [];
28
+
29
+ // Parse receipt to get transfer in/out accounts
30
+ for (const receipt of tx.receipts || []) {
31
+ const changes = receipt.changes.filter((item) => item.target === tokenAddress && item.value !== '0');
32
+
33
+ for (const change of changes) {
34
+ const value = new BN(change.value);
35
+ const item = {
36
+ address: receipt.address,
37
+ value: value.abs(),
38
+ action: change.action,
39
+ };
40
+ if (value.lt(ZERO)) {
41
+ transferOutList.push(item);
42
+ } else {
43
+ transferInList.push(item);
44
+ }
45
+ }
46
+ }
47
+
48
+ return { transferInList, transferOutList };
49
+ };
50
+
51
+ /**
52
+ * Parse transfer flow from transaction
53
+ * @param {*} tx
54
+ * @param {String} tokenAddress
55
+ * @returns {{ from: String, to: String, value: BN, hash: String }[]}
56
+ */
57
+ const getTransferFlow = (tx, tokenAddress) => {
58
+ const { transferInList, transferOutList } = getTransferList(tx, tokenAddress);
59
+ const txTransfers = [];
60
+
61
+ // Match transfers between accounts
62
+ for (const outItem of transferOutList) {
63
+ if (outItem.archived) continue;
64
+
65
+ // Try to match accounts with exactly equal transfer in/out amounts
66
+ const matchedInItem = transferInList.find((x) => x.value.eq(outItem.value));
67
+ if (matchedInItem) {
68
+ txTransfers.push({
69
+ from: outItem.address,
70
+ to: matchedInItem.address,
71
+ value: outItem.value,
72
+ hash: tx.hash,
73
+ });
74
+ matchedInItem.archived = true;
75
+ outItem.archived = true;
76
+ continue;
77
+ }
78
+
79
+ for (const inItem of transferInList) {
80
+ if (inItem.archived) continue;
81
+ if (outItem.archived) continue;
82
+
83
+ if (outItem.value.gt(inItem.value)) {
84
+ txTransfers.push({
85
+ from: outItem.address,
86
+ to: inItem.address,
87
+ value: inItem.value,
88
+ hash: tx.hash,
89
+ });
90
+ inItem.archived = true;
91
+ outItem.value = outItem.value.sub(inItem.value);
92
+ continue;
93
+ }
94
+
95
+ if (outItem.value.lt(inItem.value)) {
96
+ txTransfers.push({
97
+ from: outItem.address,
98
+ to: inItem.address,
99
+ value: outItem.value,
100
+ hash: tx.hash,
101
+ });
102
+ outItem.archived = true;
103
+ inItem.value = inItem.value.sub(outItem.value);
104
+ continue;
105
+ }
106
+
107
+ if (outItem.value.eq(inItem.value)) {
108
+ txTransfers.push({
109
+ from: outItem.address,
110
+ to: inItem.address,
111
+ value: inItem.value,
112
+ hash: tx.hash,
113
+ });
114
+ inItem.archived = true;
115
+ outItem.archived = true;
116
+ continue;
117
+ }
118
+ }
119
+ }
120
+
121
+ return txTransfers;
122
+ };
123
+
124
+ const verifyAccountRisk = async ({ accountAddress, tokenAddress }, resolver, ctx = {}) => {
125
+ // validate request params
126
+ const { error } = paramsSchema.validate({ accountAddress, tokenAddress, resolver });
127
+ if (error) {
128
+ throw new CustomError('INVALID_PARAMS', error.message);
129
+ }
130
+
131
+ const checkedAccounts = new Map();
132
+ const checkedTx = new Map();
133
+ const accountQueue = [accountAddress];
134
+ const maxAccountSize = 400;
135
+
136
+ while (accountQueue.length) {
137
+ // limit
138
+ if (checkedAccounts.size >= maxAccountSize) {
139
+ return {
140
+ isRisky: false,
141
+ reason: 'MAX_ACCOUNT_SIZE_LIMIT',
142
+ data: {
143
+ accountCount: checkedAccounts.size,
144
+ txCount: checkedTx.size,
145
+ },
146
+ };
147
+ }
148
+
149
+ const address = accountQueue.pop();
150
+ // Avoid circular query
151
+ if (checkedAccounts.has(address)) continue;
152
+
153
+ const transactions = await resolver._getAllResults('transactions', (paging) =>
154
+ resolver.listTransactions({ paging, accountFilter: { accounts: address } }, ctx)
155
+ );
156
+ const accountState = await resolver.getAccountState({ address }, ctx);
157
+ if (!accountState) {
158
+ throw new CustomError('INVALID_REQUEST', `Invalid address ${address}`);
159
+ }
160
+ const balance = accountState.tokens.find((item) => item.address === tokenAddress)?.value || 0;
161
+
162
+ let transferIn = ZERO;
163
+ let transferOut = ZERO;
164
+
165
+ // Parse txs to get transfer amounts
166
+ for (const tx of transactions) {
167
+ // cache tx
168
+ if (!checkedTx.has(tx.hash)) {
169
+ checkedTx.set(tx.hash, await getTransferList(tx, tokenAddress));
170
+ }
171
+
172
+ const { transferInList, transferOutList } = checkedTx.get(tx.hash);
173
+
174
+ // Calculate the total amount of transfer for this address
175
+ transferIn = transferIn.add(
176
+ transferInList
177
+ .filter((item) => item.address === address)
178
+ .map((item) => item.value)
179
+ .reduce((prev, cur) => prev.add(cur), ZERO)
180
+ );
181
+
182
+ transferOut = transferOut.add(
183
+ transferOutList
184
+ .filter((item) => item.address === address)
185
+ .map((item) => item.value)
186
+ .reduce((prev, cur) => prev.add(cur), ZERO)
187
+ );
188
+
189
+ // push transferIn accounts to queue for next time check
190
+ if (transferInList.some((item) => item.address === address)) {
191
+ const accountsToQueue = transferOutList
192
+ .filter((item) => {
193
+ if (accountQueue.includes(item.address)) return false;
194
+ // skip gas、fee
195
+ if (['gas', 'fee'].includes(item.action)) return false;
196
+ // Skip not token holders
197
+ if (schemas.tokenHolder.validate(item.address).error) return false;
198
+
199
+ return true;
200
+ })
201
+ .map((item) => item.address);
202
+
203
+ accountQueue.push(...uniq(accountsToQueue));
204
+ }
205
+ }
206
+
207
+ checkedAccounts.set(address, true);
208
+
209
+ // Check if the balance not matches the transfer records
210
+ if (!transferIn.eq(transferOut.add(new BN(balance)))) {
211
+ debug('Account balance does not match transfer records', {
212
+ address,
213
+ transferIn: transferIn.toString(),
214
+ transferOut: transferOut.toString(),
215
+ balance,
216
+ sourceAccount: accountAddress,
217
+ });
218
+ return {
219
+ isRisky: true,
220
+ reason: 'INVALID_BALANCE',
221
+ data: {
222
+ address,
223
+ balance,
224
+ transferIn: transferIn.toString(),
225
+ transferOut: transferOut.toString(),
226
+ accountCount: checkedAccounts.size,
227
+ txCount: checkedTx.size,
228
+ },
229
+ };
230
+ }
231
+ }
232
+
233
+ return {
234
+ isRisky: false,
235
+ data: {
236
+ accountCount: checkedAccounts.size,
237
+ txCount: checkedTx.size,
238
+ },
239
+ };
240
+ };
241
+
242
+ const listTokenFlows = async (
243
+ { accountAddress, tokenAddress, paging = {}, depth = 2, direction = 'OUT' },
244
+ resolver,
245
+ ctx = {}
246
+ ) => {
247
+ // validate request params
248
+ const { error } = paramsSchema.validate({ accountAddress, tokenAddress, resolver });
249
+ if (error) {
250
+ throw new CustomError('INVALID_PARAMS', error.message);
251
+ }
252
+
253
+ const tokenState = await resolver.tokenCache.get(tokenAddress);
254
+ const minAmount = fromTokenToUnit(1, tokenState?.decimal || 18);
255
+ const maxAccountSize = Math.min(400, paging.size || 400);
256
+ const maxDepth = Math.min(6, depth);
257
+
258
+ const tokenFlows = [];
259
+ const checkedAccounts = new Map();
260
+ const checkedTx = new Map();
261
+
262
+ let curDepth = 1;
263
+
264
+ const depthQueue = {
265
+ [curDepth]: [accountAddress],
266
+ };
267
+
268
+ while (depthQueue[curDepth]?.length && curDepth <= maxDepth && checkedAccounts.size < maxAccountSize) {
269
+ for (const address of depthQueue[curDepth]) {
270
+ // Avoid circular query
271
+ if (checkedAccounts.has(address)) continue;
272
+ // Skip not token holders
273
+ if (schemas.tokenHolder.validate(address).error) continue;
274
+
275
+ const transactions = await resolver._getAllResults('transactions', (page) =>
276
+ resolver.listTransactions(
277
+ { paging: page, accountFilter: { accounts: address }, tokenFilter: { tokens: tokenAddress } },
278
+ ctx
279
+ )
280
+ );
281
+ let accountsToQueue = [];
282
+
283
+ for (const tx of transactions) {
284
+ // cache tx
285
+ if (!checkedTx.has(tx.hash)) {
286
+ checkedTx.set(tx.hash, await getTransferFlow(tx, tokenAddress));
287
+ }
288
+ const txTransfers = checkedTx.get(tx.hash).filter((item) => {
289
+ if (direction === 'OUT' && item.from !== address) return false;
290
+ if (direction === 'IN' && item.to !== address) return false;
291
+ return true;
292
+ });
293
+
294
+ // push to result
295
+ tokenFlows.push(...txTransfers.map((item) => ({ ...item, value: item.value.toString() })));
296
+
297
+ // push to end of queue
298
+ accountsToQueue = accountsToQueue.concat(
299
+ txTransfers
300
+ .filter((item) => item.value.gte(minAmount))
301
+ .map((item) => (direction === 'IN' ? item.from : item.to))
302
+ );
303
+ }
304
+
305
+ // cache account
306
+ checkedAccounts.set(address, true);
307
+ // limit
308
+ if (checkedAccounts.size >= maxAccountSize) {
309
+ break;
310
+ }
311
+
312
+ // push to queue for next depth
313
+ if (!depthQueue[curDepth + 1]) {
314
+ depthQueue[curDepth + 1] = [];
315
+ }
316
+ depthQueue[curDepth + 1].push(...uniq(accountsToQueue));
317
+ }
318
+
319
+ curDepth++;
320
+ }
321
+
322
+ return tokenFlows;
323
+ };
324
+
325
+ module.exports = {
326
+ getTransferList,
327
+ getTransferFlow,
328
+ verifyAccountRisk,
329
+ listTokenFlows,
330
+ };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.18.140",
6
+ "version": "1.18.143",
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.18.140",
26
- "@arcblock/did-util": "1.18.140",
27
- "@arcblock/validator": "1.18.140",
28
- "@ocap/config": "1.18.140",
29
- "@ocap/indexdb": "1.18.140",
30
- "@ocap/mcrypto": "1.18.140",
31
- "@ocap/message": "1.18.140",
32
- "@ocap/state": "1.18.140",
33
- "@ocap/tx-protocols": "1.18.140",
34
- "@ocap/util": "1.18.140",
25
+ "@arcblock/did": "1.18.143",
26
+ "@arcblock/did-util": "1.18.143",
27
+ "@arcblock/validator": "1.18.143",
28
+ "@ocap/config": "1.18.143",
29
+ "@ocap/indexdb": "1.18.143",
30
+ "@ocap/mcrypto": "1.18.143",
31
+ "@ocap/message": "1.18.143",
32
+ "@ocap/state": "1.18.143",
33
+ "@ocap/tx-protocols": "1.18.143",
34
+ "@ocap/util": "1.18.143",
35
35
  "debug": "^4.3.6",
36
36
  "lodash": "^4.17.21"
37
37
  },
38
- "gitHead": "dff40db208094d9c1af520bd10658e2da61fd515"
38
+ "gitHead": "d60db9ab9a62de1d99cf1fba59866592860bed22"
39
39
  }