@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.
- package/esm/api.d.mts +24 -0
- package/esm/api.mjs +53 -0
- package/esm/hooks.d.mts +153 -0
- package/esm/hooks.mjs +267 -0
- package/esm/index.d.mts +201 -0
- package/esm/index.mjs +1327 -0
- package/esm/migration-chain.d.mts +52 -0
- package/esm/migration-chain.mjs +97 -0
- package/esm/package.mjs +5 -0
- package/esm/token-cache.d.mts +20 -0
- package/esm/token-cache.mjs +26 -0
- package/esm/token-distribution.d.mts +166 -0
- package/esm/token-distribution.mjs +241 -0
- package/esm/token-flow.d.mts +139 -0
- package/esm/token-flow.mjs +330 -0
- package/esm/types.d.mts +115 -0
- package/esm/types.mjs +1 -0
- package/lib/_virtual/rolldown_runtime.cjs +29 -0
- package/lib/api.cjs +54 -0
- package/lib/api.d.cts +24 -0
- package/lib/hooks.cjs +274 -0
- package/lib/hooks.d.cts +153 -0
- package/lib/index.cjs +1343 -0
- package/lib/index.d.cts +201 -0
- package/lib/migration-chain.cjs +99 -0
- package/lib/migration-chain.d.cts +52 -0
- package/lib/package.cjs +11 -0
- package/lib/token-cache.cjs +27 -0
- package/lib/token-cache.d.cts +20 -0
- package/lib/token-distribution.cjs +243 -0
- package/lib/token-distribution.d.cts +166 -0
- package/lib/token-flow.cjs +336 -0
- package/lib/token-flow.d.cts +139 -0
- package/lib/types.cjs +0 -0
- package/lib/types.d.cts +115 -0
- package/package.json +49 -21
- package/lib/api.js +0 -71
- package/lib/hooks.js +0 -339
- package/lib/index.js +0 -1486
- package/lib/migration-chain.js +0 -144
- package/lib/token-cache.js +0 -40
- package/lib/token-distribution.js +0 -358
- package/lib/token-flow.js +0 -445
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { IChainConfig, IChunkIterator, IIndexDB, IIndexTable, IPipelineLogger, IResolverTransaction, IStateDB, IStateTable, ITokenInfo, TTokenDistribution } from "./types.cjs";
|
|
2
|
+
import { BN } from "@ocap/util";
|
|
3
|
+
|
|
4
|
+
//#region src/token-distribution.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Token balance in array format (used in stake state for distribution)
|
|
8
|
+
*/
|
|
9
|
+
interface ITokenBalance {
|
|
10
|
+
address: string;
|
|
11
|
+
balance: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Stake state with array-format tokens (for distribution calculation)
|
|
16
|
+
*/
|
|
17
|
+
interface IDistributionStakeState {
|
|
18
|
+
address: string;
|
|
19
|
+
sender: string;
|
|
20
|
+
message: string;
|
|
21
|
+
tokens: ITokenBalance[];
|
|
22
|
+
revokedTokens: ITokenBalance[];
|
|
23
|
+
renaissanceTime?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* State snapshot for tracking token changes
|
|
27
|
+
*/
|
|
28
|
+
interface IStateSnapshot {
|
|
29
|
+
[address: string]: {
|
|
30
|
+
tokens: Record<string, string>;
|
|
31
|
+
revokedTokens: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Transaction context for distribution calculation
|
|
36
|
+
*/
|
|
37
|
+
interface IResolverTransactionContext {
|
|
38
|
+
txn?: unknown;
|
|
39
|
+
stateSnapshot?: IStateSnapshot;
|
|
40
|
+
stakeState?: {
|
|
41
|
+
tokens: Record<string, string>;
|
|
42
|
+
revokedTokens: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Distribution with BN values (for calculation)
|
|
48
|
+
*/
|
|
49
|
+
interface IDistribution {
|
|
50
|
+
tokenAddress: string;
|
|
51
|
+
account: InstanceType<typeof BN>;
|
|
52
|
+
gas: InstanceType<typeof BN>;
|
|
53
|
+
fee: InstanceType<typeof BN>;
|
|
54
|
+
slashedVault: InstanceType<typeof BN>;
|
|
55
|
+
stake: InstanceType<typeof BN>;
|
|
56
|
+
revokedStake: InstanceType<typeof BN>;
|
|
57
|
+
gasStake: InstanceType<typeof BN>;
|
|
58
|
+
other: InstanceType<typeof BN>;
|
|
59
|
+
txTime: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Raw distribution input (string/number values)
|
|
63
|
+
*/
|
|
64
|
+
interface IRawDistribution {
|
|
65
|
+
tokenAddress: string;
|
|
66
|
+
account?: string | number;
|
|
67
|
+
gas?: string | number;
|
|
68
|
+
fee?: string | number;
|
|
69
|
+
slashedVault?: string | number;
|
|
70
|
+
stake?: string | number;
|
|
71
|
+
revokedStake?: string | number;
|
|
72
|
+
gasStake?: string | number;
|
|
73
|
+
other?: string | number;
|
|
74
|
+
txTime?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Options for handleTx method
|
|
78
|
+
*/
|
|
79
|
+
interface IHandleTxOptions {
|
|
80
|
+
context?: IResolverTransactionContext;
|
|
81
|
+
isHandleStake?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolver interface for TokenDistributionManager
|
|
85
|
+
*/
|
|
86
|
+
interface IDistributionResolver {
|
|
87
|
+
indexdb: IIndexDB;
|
|
88
|
+
statedb: IStateDB & {
|
|
89
|
+
stake: IStateTable<IDistributionStakeState>;
|
|
90
|
+
};
|
|
91
|
+
config: IChainConfig;
|
|
92
|
+
logger?: IPipelineLogger;
|
|
93
|
+
formatTx: (tx: IResolverTransaction) => Promise<IResolverTransaction & {
|
|
94
|
+
tokenSymbols: ITokenInfo[];
|
|
95
|
+
}>;
|
|
96
|
+
listTransactionsChunks: (params: {
|
|
97
|
+
timeFilter?: {
|
|
98
|
+
startDateTime: string;
|
|
99
|
+
};
|
|
100
|
+
tokenFilter?: {
|
|
101
|
+
tokenFilter?: {
|
|
102
|
+
tokens: string[];
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}) => Promise<IChunkIterator<IResolverTransaction>>;
|
|
106
|
+
listStakeChunks: () => Promise<IChunkIterator<IDistributionStakeState>>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* TokenDistribution IndexDB table interface
|
|
110
|
+
*/
|
|
111
|
+
type ITokenDistributionTable = IIndexTable<TTokenDistribution> & {
|
|
112
|
+
insert: (data: TTokenDistribution) => Promise<void>;
|
|
113
|
+
update: (tokenAddress: string, data: TTokenDistribution) => Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
declare class TokenDistributionManager {
|
|
116
|
+
resolver: IDistributionResolver;
|
|
117
|
+
indexdb: IIndexDB & {
|
|
118
|
+
tokenDistribution: ITokenDistributionTable;
|
|
119
|
+
};
|
|
120
|
+
isProcessing: boolean;
|
|
121
|
+
constructor(resolver: IDistributionResolver);
|
|
122
|
+
formatDistribution(distribution: IRawDistribution): IDistribution;
|
|
123
|
+
getDistribution(tokenAddress: string): Promise<TTokenDistribution | null>;
|
|
124
|
+
saveDistribution(distribution: IDistribution, isEnsureLatest?: boolean): Promise<TTokenDistribution>;
|
|
125
|
+
/**
|
|
126
|
+
* Calculate token distribution based on all transaction data.
|
|
127
|
+
* This method is usually used to update historical token distribution data
|
|
128
|
+
*
|
|
129
|
+
* @param tokenAddress Token address
|
|
130
|
+
* @param force If force is false, only calculate distributions for new transactions after txTime in indexdb. default is false
|
|
131
|
+
* @returns The updated token distribution or null if failed
|
|
132
|
+
*/
|
|
133
|
+
updateByToken(tokenAddress: string, force?: boolean): Promise<TTokenDistribution | null | undefined>;
|
|
134
|
+
/**
|
|
135
|
+
* Split out revokedStake / gasStake from stake
|
|
136
|
+
*
|
|
137
|
+
* @param distribution The distribution object to update
|
|
138
|
+
* @returns The updated distribution
|
|
139
|
+
*/
|
|
140
|
+
splitStake(distribution: IDistribution): Promise<IDistribution>;
|
|
141
|
+
/**
|
|
142
|
+
* Update token distribution by a single transaction.
|
|
143
|
+
* This method is usually used when a tx is completed.
|
|
144
|
+
*
|
|
145
|
+
* @param tx The transaction object
|
|
146
|
+
* @param context The transaction context
|
|
147
|
+
* @returns The updated token distributions
|
|
148
|
+
*/
|
|
149
|
+
updateByTx(tx: IResolverTransaction, context: IResolverTransactionContext): Promise<TTokenDistribution[] | undefined>;
|
|
150
|
+
/**
|
|
151
|
+
* Parse token distribution for a single transaction
|
|
152
|
+
* @param tx The transaction
|
|
153
|
+
* @param distribution The distribution to update
|
|
154
|
+
* @param options Handle options
|
|
155
|
+
* @returns The updated token distribution
|
|
156
|
+
*/
|
|
157
|
+
handleTx(tx: IResolverTransaction, distribution: IDistribution, {
|
|
158
|
+
context,
|
|
159
|
+
isHandleStake
|
|
160
|
+
}?: IHandleTxOptions): Promise<IDistribution>;
|
|
161
|
+
handleModerator(distribution: IDistribution): IDistribution;
|
|
162
|
+
getStakeState(address: string, ctx: IResolverTransactionContext): Promise<IDistributionStakeState>;
|
|
163
|
+
isGasStake(stakeState: IDistributionStakeState): boolean;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { TokenDistributionManager };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let _ocap_util = require("@ocap/util");
|
|
3
|
+
let _arcblock_validator = require("@arcblock/validator");
|
|
4
|
+
let _ocap_util_lib_error = require("@ocap/util/lib/error");
|
|
5
|
+
let lodash_uniq = require("lodash/uniq");
|
|
6
|
+
lodash_uniq = require_rolldown_runtime.__toESM(lodash_uniq);
|
|
7
|
+
let _ocap_state_lib_states_tx = require("@ocap/state/lib/states/tx");
|
|
8
|
+
|
|
9
|
+
//#region src/token-flow.ts
|
|
10
|
+
const ZERO = new _ocap_util.BN(0);
|
|
11
|
+
const paramsSchema = _arcblock_validator.Joi.object({
|
|
12
|
+
accountAddress: _arcblock_validator.schemas.tokenHolder.required(),
|
|
13
|
+
tokenAddress: _arcblock_validator.Joi.DID().prefix().role("ROLE_TOKEN").required(),
|
|
14
|
+
resolver: _arcblock_validator.Joi.object().required()
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Parse transfer in/out list from transaction
|
|
18
|
+
* @param tx Transaction object
|
|
19
|
+
* @param tokenAddress Token address to filter by
|
|
20
|
+
* @returns Object containing transferInList and transferOutList
|
|
21
|
+
*/
|
|
22
|
+
const getTransferList = (tx, tokenAddress) => {
|
|
23
|
+
const transferInList = [];
|
|
24
|
+
const transferOutList = [];
|
|
25
|
+
for (const receipt of tx.receipts || []) {
|
|
26
|
+
const changes = receipt.changes.filter((item) => item.target === tokenAddress && item.value !== "0");
|
|
27
|
+
for (const change of changes) {
|
|
28
|
+
const value = new _ocap_util.BN(change.value);
|
|
29
|
+
const item = {
|
|
30
|
+
address: receipt.address,
|
|
31
|
+
value: value.abs(),
|
|
32
|
+
action: change.action
|
|
33
|
+
};
|
|
34
|
+
if (value.lt(ZERO)) transferOutList.push(item);
|
|
35
|
+
else transferInList.push(item);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
transferInList,
|
|
40
|
+
transferOutList
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Parse transfer flow from transaction
|
|
45
|
+
* @param tx Transaction object
|
|
46
|
+
* @param tokenAddress Token address to filter by
|
|
47
|
+
* @returns Array of transfer flows
|
|
48
|
+
*/
|
|
49
|
+
const getTransferFlow = (tx, tokenAddress) => {
|
|
50
|
+
const { transferInList, transferOutList } = getTransferList(tx, tokenAddress);
|
|
51
|
+
const txTransfers = [];
|
|
52
|
+
for (const outItem of transferOutList) {
|
|
53
|
+
if (outItem.archived) continue;
|
|
54
|
+
const matchedInItem = transferInList.find((x) => x.value.eq(outItem.value));
|
|
55
|
+
if (matchedInItem) {
|
|
56
|
+
txTransfers.push({
|
|
57
|
+
from: outItem.address,
|
|
58
|
+
to: matchedInItem.address,
|
|
59
|
+
value: outItem.value,
|
|
60
|
+
hash: tx.hash
|
|
61
|
+
});
|
|
62
|
+
matchedInItem.archived = true;
|
|
63
|
+
outItem.archived = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const inItem of transferInList) {
|
|
67
|
+
if (inItem.archived) continue;
|
|
68
|
+
if (outItem.archived) continue;
|
|
69
|
+
if (outItem.value.gt(inItem.value)) {
|
|
70
|
+
txTransfers.push({
|
|
71
|
+
from: outItem.address,
|
|
72
|
+
to: inItem.address,
|
|
73
|
+
value: inItem.value,
|
|
74
|
+
hash: tx.hash
|
|
75
|
+
});
|
|
76
|
+
inItem.archived = true;
|
|
77
|
+
outItem.value = outItem.value.sub(inItem.value);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (outItem.value.lt(inItem.value)) {
|
|
81
|
+
txTransfers.push({
|
|
82
|
+
from: outItem.address,
|
|
83
|
+
to: inItem.address,
|
|
84
|
+
value: outItem.value,
|
|
85
|
+
hash: tx.hash
|
|
86
|
+
});
|
|
87
|
+
outItem.archived = true;
|
|
88
|
+
inItem.value = inItem.value.sub(outItem.value);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (outItem.value.eq(inItem.value)) {
|
|
92
|
+
txTransfers.push({
|
|
93
|
+
from: outItem.address,
|
|
94
|
+
to: inItem.address,
|
|
95
|
+
value: inItem.value,
|
|
96
|
+
hash: tx.hash
|
|
97
|
+
});
|
|
98
|
+
inItem.archived = true;
|
|
99
|
+
outItem.archived = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return txTransfers;
|
|
104
|
+
};
|
|
105
|
+
const getVaultAccounts = (config) => {
|
|
106
|
+
return Object.values(config.vaults).flat().concat(_ocap_state_lib_states_tx.FORGE_TOKEN_HOLDER);
|
|
107
|
+
};
|
|
108
|
+
const getInitialBalance = (address, config) => {
|
|
109
|
+
const account = config?.accounts?.find((x) => (0, _ocap_util.isSameDid)(x.address, address));
|
|
110
|
+
return account ? (0, _ocap_util.fromTokenToUnit)(account.balance ?? 0) : ZERO;
|
|
111
|
+
};
|
|
112
|
+
const getBalance = async (address, tokenAddress, { resolver, txn }) => {
|
|
113
|
+
const state = await resolver.statedb.account.get(address, {
|
|
114
|
+
txn,
|
|
115
|
+
traceMigration: false
|
|
116
|
+
});
|
|
117
|
+
if (!state) throw new _ocap_util_lib_error.CustomError("INVALID_REQUEST", `Invalid address ${address}`);
|
|
118
|
+
return state.tokens[tokenAddress] || 0;
|
|
119
|
+
};
|
|
120
|
+
const fixMigrateReceipts = async (tx, resolver) => {
|
|
121
|
+
const migrationChain = await resolver.getMigrationChain();
|
|
122
|
+
const txTime = new Date(tx.time);
|
|
123
|
+
tx.receipts?.forEach((receipt) => {
|
|
124
|
+
receipt.address = migrationChain.findAddressAtTime(receipt.address, txTime);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
const verifyAccountRisk = async ({ accountAddress, tokenAddress, accountLimit = 400, txLimit = 1e4, tolerance = "0.0000000001" }, resolver, ctx = {}) => {
|
|
128
|
+
const validation = paramsSchema.validate({
|
|
129
|
+
accountAddress,
|
|
130
|
+
tokenAddress,
|
|
131
|
+
resolver
|
|
132
|
+
});
|
|
133
|
+
if (validation.error) throw new _ocap_util_lib_error.CustomError("INVALID_PARAMS", validation.error.message);
|
|
134
|
+
const { logger } = resolver;
|
|
135
|
+
const checkedAccounts = /* @__PURE__ */ new Map();
|
|
136
|
+
const checkedTx = /* @__PURE__ */ new Map();
|
|
137
|
+
const toleranceUnit = (0, _ocap_util.fromTokenToUnit)(tolerance, (await resolver.tokenCache.get(tokenAddress))?.decimal);
|
|
138
|
+
const vaultAccounts = getVaultAccounts(resolver.config);
|
|
139
|
+
const accountQueue = [[{
|
|
140
|
+
address: accountAddress,
|
|
141
|
+
chain: []
|
|
142
|
+
}]];
|
|
143
|
+
const execute = async (depth, txn) => {
|
|
144
|
+
const queue = accountQueue[depth];
|
|
145
|
+
for (let i = 0; i < queue.length; i++) {
|
|
146
|
+
if (checkedAccounts.size >= accountLimit) {
|
|
147
|
+
logger.warn("Account risk check reached max account size limit", {
|
|
148
|
+
address: accountAddress,
|
|
149
|
+
tokenAddress,
|
|
150
|
+
accountCount: checkedAccounts.size,
|
|
151
|
+
txCount: checkedTx.size,
|
|
152
|
+
depth,
|
|
153
|
+
accountLimit,
|
|
154
|
+
txLimit,
|
|
155
|
+
tolerance
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
isRisky: false,
|
|
159
|
+
reason: "MAX_ACCOUNT_SIZE_LIMIT",
|
|
160
|
+
data: {
|
|
161
|
+
accountCount: checkedAccounts.size,
|
|
162
|
+
txCount: checkedTx.size
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const { address, chain } = queue[i];
|
|
167
|
+
chain.push(address);
|
|
168
|
+
if (checkedAccounts.has(address)) continue;
|
|
169
|
+
const trustedConfig = await resolver.filter?.getTrustedAccountConfig?.(address);
|
|
170
|
+
if (trustedConfig && !trustedConfig.tolerance) {
|
|
171
|
+
checkedAccounts.set(address, true);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let balance = 0;
|
|
175
|
+
let transactions = [];
|
|
176
|
+
try {
|
|
177
|
+
[balance, transactions] = await Promise.all([getBalance(address, tokenAddress, {
|
|
178
|
+
resolver,
|
|
179
|
+
txn
|
|
180
|
+
}), resolver._getAllResults("transactions", (paging) => resolver.listTransactions({
|
|
181
|
+
paging,
|
|
182
|
+
accountFilter: { accounts: [address] }
|
|
183
|
+
}, ctx), txLimit)]);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (e?.code === "EXCEED_LIMIT") {
|
|
186
|
+
logger.warn("Skip checking account cause tx count exceeding limit", {
|
|
187
|
+
address,
|
|
188
|
+
txLimit
|
|
189
|
+
});
|
|
190
|
+
checkedAccounts.set(address, true);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
let transferIn = getInitialBalance(address, resolver.config);
|
|
196
|
+
let transferOut = ZERO;
|
|
197
|
+
for (const tx of transactions) {
|
|
198
|
+
if (!checkedTx.has(tx.hash)) {
|
|
199
|
+
await fixMigrateReceipts(tx, resolver);
|
|
200
|
+
checkedTx.set(tx.hash, getTransferList(tx, tokenAddress));
|
|
201
|
+
}
|
|
202
|
+
const { transferInList, transferOutList } = checkedTx.get(tx.hash);
|
|
203
|
+
const transferInAmount = transferInList.filter((item) => (0, _ocap_util.isSameDid)(item.address, address)).map((item) => item.value).reduce((prev, cur) => prev.add(cur), ZERO);
|
|
204
|
+
const transferOutAmount = transferOutList.filter((item) => (0, _ocap_util.isSameDid)(item.address, address)).map((item) => item.value).reduce((prev, cur) => prev.add(cur), ZERO);
|
|
205
|
+
transferIn = transferIn.add(transferInAmount);
|
|
206
|
+
transferOut = transferOut.add(transferOutAmount);
|
|
207
|
+
if (transferInAmount.gt(ZERO)) {
|
|
208
|
+
if (!accountQueue[depth + 1]) accountQueue[depth + 1] = [];
|
|
209
|
+
const accountsToQueue = transferOutList.filter((item) => {
|
|
210
|
+
if (checkedAccounts.has(item.address)) return false;
|
|
211
|
+
if (_arcblock_validator.schemas.tokenHolder.validate(item.address).error) return false;
|
|
212
|
+
if (vaultAccounts.includes(item.address)) return false;
|
|
213
|
+
if (["gas", "fee"].includes(item.action || "")) return false;
|
|
214
|
+
return true;
|
|
215
|
+
}).map((item) => item.address);
|
|
216
|
+
accountQueue[depth + 1].push(...(0, lodash_uniq.default)(accountsToQueue).map((x) => ({
|
|
217
|
+
address: x,
|
|
218
|
+
chain: chain.concat()
|
|
219
|
+
})));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
checkedAccounts.set(address, true);
|
|
223
|
+
const diff = transferIn.sub(transferOut).sub(new _ocap_util.BN(balance)).add((0, _ocap_util.fromTokenToUnit)(trustedConfig?.tolerance || 0));
|
|
224
|
+
if (diff.abs().gt(toleranceUnit)) {
|
|
225
|
+
const data = {
|
|
226
|
+
address: chain.join("->"),
|
|
227
|
+
balance,
|
|
228
|
+
transferIn: transferIn.toString(),
|
|
229
|
+
transferOut: transferOut.toString(),
|
|
230
|
+
accountCount: checkedAccounts.size,
|
|
231
|
+
txCount: checkedTx.size
|
|
232
|
+
};
|
|
233
|
+
logger.warn("Account balance does not match transfer records", {
|
|
234
|
+
...data,
|
|
235
|
+
sourceAccount: accountAddress,
|
|
236
|
+
tokenAddress,
|
|
237
|
+
diff: diff.toString(),
|
|
238
|
+
depth,
|
|
239
|
+
accountLimit,
|
|
240
|
+
txLimit,
|
|
241
|
+
tolerance
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
isRisky: true,
|
|
245
|
+
reason: "INVALID_BALANCE",
|
|
246
|
+
data
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
for (let depth = 0; depth < accountQueue.length; depth++) {
|
|
252
|
+
let isExecuted = false;
|
|
253
|
+
const result = await resolver.runAsLambda((txn) => {
|
|
254
|
+
if (isExecuted) throw new _ocap_util_lib_error.CustomError("INVALID_REQUEST", "verifyAccountRisk should not retry");
|
|
255
|
+
isExecuted = true;
|
|
256
|
+
return execute(depth, txn);
|
|
257
|
+
}, { retryLimit: 0 });
|
|
258
|
+
if (result) return result;
|
|
259
|
+
}
|
|
260
|
+
logger.info("Account risk check completed", {
|
|
261
|
+
address: accountAddress,
|
|
262
|
+
tokenAddress,
|
|
263
|
+
accountCount: checkedAccounts.size,
|
|
264
|
+
txCount: checkedTx.size,
|
|
265
|
+
depth: accountQueue.length,
|
|
266
|
+
accountLimit,
|
|
267
|
+
txLimit,
|
|
268
|
+
tolerance
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
isRisky: false,
|
|
272
|
+
data: {
|
|
273
|
+
accountCount: checkedAccounts.size,
|
|
274
|
+
txCount: checkedTx.size
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
const listTokenFlows = async ({ accountAddress, tokenAddress, paging = {}, depth = 2, direction = "OUT" }, resolver, ctx = {}) => {
|
|
279
|
+
const { error } = paramsSchema.validate({
|
|
280
|
+
accountAddress,
|
|
281
|
+
tokenAddress,
|
|
282
|
+
resolver
|
|
283
|
+
});
|
|
284
|
+
if (error) throw new _ocap_util_lib_error.CustomError("INVALID_PARAMS", error.message);
|
|
285
|
+
const minAmount = (0, _ocap_util.fromTokenToUnit)(1, (await resolver.tokenCache.get(tokenAddress))?.decimal || 18);
|
|
286
|
+
const maxAccountSize = Math.min(200, paging.size || 200);
|
|
287
|
+
const maxDepth = Math.min(5, depth);
|
|
288
|
+
const vaultAccounts = getVaultAccounts(resolver.config);
|
|
289
|
+
const tokenFlows = [];
|
|
290
|
+
const checkedAccounts = /* @__PURE__ */ new Map();
|
|
291
|
+
const checkedTx = /* @__PURE__ */ new Map();
|
|
292
|
+
let curDepth = 1;
|
|
293
|
+
const depthQueue = { [curDepth]: [accountAddress] };
|
|
294
|
+
while (depthQueue[curDepth]?.length && curDepth <= maxDepth && checkedAccounts.size < maxAccountSize) {
|
|
295
|
+
for (const address of depthQueue[curDepth]) {
|
|
296
|
+
if (checkedAccounts.has(address)) continue;
|
|
297
|
+
if (_arcblock_validator.schemas.tokenHolder.validate(address).error) continue;
|
|
298
|
+
const transactions = await resolver._getAllResults("transactions", (page) => resolver.listTransactions({
|
|
299
|
+
paging: page,
|
|
300
|
+
accountFilter: { accounts: [address] },
|
|
301
|
+
tokenFilter: { tokens: [tokenAddress] }
|
|
302
|
+
}, ctx));
|
|
303
|
+
let accountsToQueue = [];
|
|
304
|
+
for (const tx of transactions) {
|
|
305
|
+
if (!checkedTx.has(tx.hash)) {
|
|
306
|
+
await fixMigrateReceipts(tx, resolver);
|
|
307
|
+
checkedTx.set(tx.hash, getTransferFlow(tx, tokenAddress));
|
|
308
|
+
}
|
|
309
|
+
const txTransfers = checkedTx.get(tx.hash).filter((item) => {
|
|
310
|
+
if (direction === "OUT" && item.from !== address) return false;
|
|
311
|
+
if (direction === "IN" && item.to !== address) return false;
|
|
312
|
+
if (item.value.lt(minAmount)) return false;
|
|
313
|
+
return true;
|
|
314
|
+
});
|
|
315
|
+
tokenFlows.push(...txTransfers.map((item) => ({
|
|
316
|
+
...item,
|
|
317
|
+
value: item.value.toString()
|
|
318
|
+
})));
|
|
319
|
+
accountsToQueue = accountsToQueue.concat(txTransfers.map((item) => direction === "IN" ? item.from : item.to).filter((item) => !vaultAccounts.includes(item)));
|
|
320
|
+
}
|
|
321
|
+
checkedAccounts.set(address, true);
|
|
322
|
+
if (checkedAccounts.size >= maxAccountSize) break;
|
|
323
|
+
if (!depthQueue[curDepth + 1]) depthQueue[curDepth + 1] = [];
|
|
324
|
+
depthQueue[curDepth + 1].push(...(0, lodash_uniq.default)(accountsToQueue));
|
|
325
|
+
}
|
|
326
|
+
curDepth++;
|
|
327
|
+
}
|
|
328
|
+
return tokenFlows;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
exports.fixMigrateReceipts = fixMigrateReceipts;
|
|
333
|
+
exports.getTransferFlow = getTransferFlow;
|
|
334
|
+
exports.getTransferList = getTransferList;
|
|
335
|
+
exports.listTokenFlows = listTokenFlows;
|
|
336
|
+
exports.verifyAccountRisk = verifyAccountRisk;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { IChainConfig, IFilter, IMigrationChain, IPipelineLogger, IResolverPaging, IStateDB, ITokenCache, TTransactionReceipt } from "./types.cjs";
|
|
2
|
+
import { BN } from "@ocap/util";
|
|
3
|
+
|
|
4
|
+
//#region src/token-flow.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Transfer item with BN value for calculation
|
|
8
|
+
*/
|
|
9
|
+
interface ITransferItem {
|
|
10
|
+
address: string;
|
|
11
|
+
value: InstanceType<typeof BN>;
|
|
12
|
+
action?: string;
|
|
13
|
+
archived?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Transfer flow with BN value
|
|
17
|
+
*/
|
|
18
|
+
interface ITransferFlow {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
value: InstanceType<typeof BN>;
|
|
22
|
+
hash: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Transfer flow result with string value
|
|
26
|
+
*/
|
|
27
|
+
interface ITransferFlowResult {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
value: string;
|
|
31
|
+
hash: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Risk check result
|
|
35
|
+
*/
|
|
36
|
+
interface IRiskCheckResult {
|
|
37
|
+
isRisky: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
data: {
|
|
40
|
+
address?: string;
|
|
41
|
+
balance?: string | number;
|
|
42
|
+
transferIn?: string;
|
|
43
|
+
transferOut?: string;
|
|
44
|
+
accountCount: number;
|
|
45
|
+
txCount: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parameters for verifyAccountRisk
|
|
50
|
+
*/
|
|
51
|
+
interface IVerifyAccountRiskParams {
|
|
52
|
+
accountAddress: string;
|
|
53
|
+
tokenAddress: string;
|
|
54
|
+
accountLimit?: number;
|
|
55
|
+
txLimit?: number;
|
|
56
|
+
tolerance?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parameters for listTokenFlows
|
|
60
|
+
*/
|
|
61
|
+
interface IListTokenFlowsParams {
|
|
62
|
+
accountAddress: string;
|
|
63
|
+
tokenAddress: string;
|
|
64
|
+
paging?: IResolverPaging;
|
|
65
|
+
depth?: number;
|
|
66
|
+
direction?: 'IN' | 'OUT';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Simplified transaction for token flow
|
|
70
|
+
*/
|
|
71
|
+
interface ITokenFlowTx {
|
|
72
|
+
hash: string;
|
|
73
|
+
time: string;
|
|
74
|
+
receipts?: TTransactionReceipt[];
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolver interface for token flow operations
|
|
79
|
+
*/
|
|
80
|
+
interface ITokenFlowResolver {
|
|
81
|
+
config: IChainConfig;
|
|
82
|
+
tokenCache: ITokenCache;
|
|
83
|
+
filter?: IFilter;
|
|
84
|
+
logger: IPipelineLogger;
|
|
85
|
+
statedb: IStateDB;
|
|
86
|
+
getMigrationChain: () => Promise<IMigrationChain>;
|
|
87
|
+
_getAllResults: <T>(type: string, fetcher: (paging: IResolverPaging) => Promise<{
|
|
88
|
+
transactions?: T[];
|
|
89
|
+
data?: T[];
|
|
90
|
+
}>, limit?: number) => Promise<T[]>;
|
|
91
|
+
listTransactions: (params: {
|
|
92
|
+
paging?: IResolverPaging;
|
|
93
|
+
accountFilter?: {
|
|
94
|
+
accounts: string[];
|
|
95
|
+
};
|
|
96
|
+
tokenFilter?: {
|
|
97
|
+
tokens: string[];
|
|
98
|
+
};
|
|
99
|
+
}, ctx?: Record<string, unknown>) => Promise<{
|
|
100
|
+
transactions: ITokenFlowTx[];
|
|
101
|
+
}>;
|
|
102
|
+
runAsLambda: <T>(fn: (txn: unknown) => Promise<T>, options?: {
|
|
103
|
+
retryLimit?: number;
|
|
104
|
+
}) => Promise<T>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse transfer in/out list from transaction
|
|
108
|
+
* @param tx Transaction object
|
|
109
|
+
* @param tokenAddress Token address to filter by
|
|
110
|
+
* @returns Object containing transferInList and transferOutList
|
|
111
|
+
*/
|
|
112
|
+
declare const getTransferList: (tx: ITokenFlowTx, tokenAddress: string) => {
|
|
113
|
+
transferInList: ITransferItem[];
|
|
114
|
+
transferOutList: ITransferItem[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Parse transfer flow from transaction
|
|
118
|
+
* @param tx Transaction object
|
|
119
|
+
* @param tokenAddress Token address to filter by
|
|
120
|
+
* @returns Array of transfer flows
|
|
121
|
+
*/
|
|
122
|
+
declare const getTransferFlow: (tx: ITokenFlowTx, tokenAddress: string) => ITransferFlow[];
|
|
123
|
+
declare const fixMigrateReceipts: (tx: ITokenFlowTx, resolver: ITokenFlowResolver) => Promise<void>;
|
|
124
|
+
declare const verifyAccountRisk: ({
|
|
125
|
+
accountAddress,
|
|
126
|
+
tokenAddress,
|
|
127
|
+
accountLimit,
|
|
128
|
+
txLimit,
|
|
129
|
+
tolerance
|
|
130
|
+
}: IVerifyAccountRiskParams, resolver: ITokenFlowResolver, ctx?: Record<string, unknown>) => Promise<IRiskCheckResult>;
|
|
131
|
+
declare const listTokenFlows: ({
|
|
132
|
+
accountAddress,
|
|
133
|
+
tokenAddress,
|
|
134
|
+
paging,
|
|
135
|
+
depth,
|
|
136
|
+
direction
|
|
137
|
+
}: IListTokenFlowsParams, resolver: ITokenFlowResolver, ctx?: Record<string, unknown>) => Promise<ITransferFlowResult[]>;
|
|
138
|
+
//#endregion
|
|
139
|
+
export { fixMigrateReceipts, getTransferFlow, getTransferList, listTokenFlows, verifyAccountRisk };
|
package/lib/types.cjs
ADDED
|
File without changes
|