@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,139 @@
|
|
|
1
|
+
import { IChainConfig, IFilter, IMigrationChain, IPipelineLogger, IResolverPaging, IStateDB, ITokenCache, TTransactionReceipt } from "./types.mjs";
|
|
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 };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { BN, fromTokenToUnit, isSameDid } from "@ocap/util";
|
|
2
|
+
import { Joi, schemas } from "@arcblock/validator";
|
|
3
|
+
import { CustomError } from "@ocap/util/lib/error";
|
|
4
|
+
import uniq from "lodash/uniq.js";
|
|
5
|
+
import { FORGE_TOKEN_HOLDER } from "@ocap/state/lib/states/tx";
|
|
6
|
+
|
|
7
|
+
//#region src/token-flow.ts
|
|
8
|
+
const ZERO = new BN(0);
|
|
9
|
+
const paramsSchema = Joi.object({
|
|
10
|
+
accountAddress: schemas.tokenHolder.required(),
|
|
11
|
+
tokenAddress: Joi.DID().prefix().role("ROLE_TOKEN").required(),
|
|
12
|
+
resolver: Joi.object().required()
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Parse transfer in/out list from transaction
|
|
16
|
+
* @param tx Transaction object
|
|
17
|
+
* @param tokenAddress Token address to filter by
|
|
18
|
+
* @returns Object containing transferInList and transferOutList
|
|
19
|
+
*/
|
|
20
|
+
const getTransferList = (tx, tokenAddress) => {
|
|
21
|
+
const transferInList = [];
|
|
22
|
+
const transferOutList = [];
|
|
23
|
+
for (const receipt of tx.receipts || []) {
|
|
24
|
+
const changes = receipt.changes.filter((item) => item.target === tokenAddress && item.value !== "0");
|
|
25
|
+
for (const change of changes) {
|
|
26
|
+
const value = new BN(change.value);
|
|
27
|
+
const item = {
|
|
28
|
+
address: receipt.address,
|
|
29
|
+
value: value.abs(),
|
|
30
|
+
action: change.action
|
|
31
|
+
};
|
|
32
|
+
if (value.lt(ZERO)) transferOutList.push(item);
|
|
33
|
+
else transferInList.push(item);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
transferInList,
|
|
38
|
+
transferOutList
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Parse transfer flow from transaction
|
|
43
|
+
* @param tx Transaction object
|
|
44
|
+
* @param tokenAddress Token address to filter by
|
|
45
|
+
* @returns Array of transfer flows
|
|
46
|
+
*/
|
|
47
|
+
const getTransferFlow = (tx, tokenAddress) => {
|
|
48
|
+
const { transferInList, transferOutList } = getTransferList(tx, tokenAddress);
|
|
49
|
+
const txTransfers = [];
|
|
50
|
+
for (const outItem of transferOutList) {
|
|
51
|
+
if (outItem.archived) continue;
|
|
52
|
+
const matchedInItem = transferInList.find((x) => x.value.eq(outItem.value));
|
|
53
|
+
if (matchedInItem) {
|
|
54
|
+
txTransfers.push({
|
|
55
|
+
from: outItem.address,
|
|
56
|
+
to: matchedInItem.address,
|
|
57
|
+
value: outItem.value,
|
|
58
|
+
hash: tx.hash
|
|
59
|
+
});
|
|
60
|
+
matchedInItem.archived = true;
|
|
61
|
+
outItem.archived = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const inItem of transferInList) {
|
|
65
|
+
if (inItem.archived) continue;
|
|
66
|
+
if (outItem.archived) continue;
|
|
67
|
+
if (outItem.value.gt(inItem.value)) {
|
|
68
|
+
txTransfers.push({
|
|
69
|
+
from: outItem.address,
|
|
70
|
+
to: inItem.address,
|
|
71
|
+
value: inItem.value,
|
|
72
|
+
hash: tx.hash
|
|
73
|
+
});
|
|
74
|
+
inItem.archived = true;
|
|
75
|
+
outItem.value = outItem.value.sub(inItem.value);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (outItem.value.lt(inItem.value)) {
|
|
79
|
+
txTransfers.push({
|
|
80
|
+
from: outItem.address,
|
|
81
|
+
to: inItem.address,
|
|
82
|
+
value: outItem.value,
|
|
83
|
+
hash: tx.hash
|
|
84
|
+
});
|
|
85
|
+
outItem.archived = true;
|
|
86
|
+
inItem.value = inItem.value.sub(outItem.value);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (outItem.value.eq(inItem.value)) {
|
|
90
|
+
txTransfers.push({
|
|
91
|
+
from: outItem.address,
|
|
92
|
+
to: inItem.address,
|
|
93
|
+
value: inItem.value,
|
|
94
|
+
hash: tx.hash
|
|
95
|
+
});
|
|
96
|
+
inItem.archived = true;
|
|
97
|
+
outItem.archived = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return txTransfers;
|
|
102
|
+
};
|
|
103
|
+
const getVaultAccounts = (config) => {
|
|
104
|
+
return Object.values(config.vaults).flat().concat(FORGE_TOKEN_HOLDER);
|
|
105
|
+
};
|
|
106
|
+
const getInitialBalance = (address, config) => {
|
|
107
|
+
const account = config?.accounts?.find((x) => isSameDid(x.address, address));
|
|
108
|
+
return account ? fromTokenToUnit(account.balance ?? 0) : ZERO;
|
|
109
|
+
};
|
|
110
|
+
const getBalance = async (address, tokenAddress, { resolver, txn }) => {
|
|
111
|
+
const state = await resolver.statedb.account.get(address, {
|
|
112
|
+
txn,
|
|
113
|
+
traceMigration: false
|
|
114
|
+
});
|
|
115
|
+
if (!state) throw new CustomError("INVALID_REQUEST", `Invalid address ${address}`);
|
|
116
|
+
return state.tokens[tokenAddress] || 0;
|
|
117
|
+
};
|
|
118
|
+
const fixMigrateReceipts = async (tx, resolver) => {
|
|
119
|
+
const migrationChain = await resolver.getMigrationChain();
|
|
120
|
+
const txTime = new Date(tx.time);
|
|
121
|
+
tx.receipts?.forEach((receipt) => {
|
|
122
|
+
receipt.address = migrationChain.findAddressAtTime(receipt.address, txTime);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const verifyAccountRisk = async ({ accountAddress, tokenAddress, accountLimit = 400, txLimit = 1e4, tolerance = "0.0000000001" }, resolver, ctx = {}) => {
|
|
126
|
+
const validation = paramsSchema.validate({
|
|
127
|
+
accountAddress,
|
|
128
|
+
tokenAddress,
|
|
129
|
+
resolver
|
|
130
|
+
});
|
|
131
|
+
if (validation.error) throw new CustomError("INVALID_PARAMS", validation.error.message);
|
|
132
|
+
const { logger } = resolver;
|
|
133
|
+
const checkedAccounts = /* @__PURE__ */ new Map();
|
|
134
|
+
const checkedTx = /* @__PURE__ */ new Map();
|
|
135
|
+
const toleranceUnit = fromTokenToUnit(tolerance, (await resolver.tokenCache.get(tokenAddress))?.decimal);
|
|
136
|
+
const vaultAccounts = getVaultAccounts(resolver.config);
|
|
137
|
+
const accountQueue = [[{
|
|
138
|
+
address: accountAddress,
|
|
139
|
+
chain: []
|
|
140
|
+
}]];
|
|
141
|
+
const execute = async (depth, txn) => {
|
|
142
|
+
const queue = accountQueue[depth];
|
|
143
|
+
for (let i = 0; i < queue.length; i++) {
|
|
144
|
+
if (checkedAccounts.size >= accountLimit) {
|
|
145
|
+
logger.warn("Account risk check reached max account size limit", {
|
|
146
|
+
address: accountAddress,
|
|
147
|
+
tokenAddress,
|
|
148
|
+
accountCount: checkedAccounts.size,
|
|
149
|
+
txCount: checkedTx.size,
|
|
150
|
+
depth,
|
|
151
|
+
accountLimit,
|
|
152
|
+
txLimit,
|
|
153
|
+
tolerance
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
isRisky: false,
|
|
157
|
+
reason: "MAX_ACCOUNT_SIZE_LIMIT",
|
|
158
|
+
data: {
|
|
159
|
+
accountCount: checkedAccounts.size,
|
|
160
|
+
txCount: checkedTx.size
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const { address, chain } = queue[i];
|
|
165
|
+
chain.push(address);
|
|
166
|
+
if (checkedAccounts.has(address)) continue;
|
|
167
|
+
const trustedConfig = await resolver.filter?.getTrustedAccountConfig?.(address);
|
|
168
|
+
if (trustedConfig && !trustedConfig.tolerance) {
|
|
169
|
+
checkedAccounts.set(address, true);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let balance = 0;
|
|
173
|
+
let transactions = [];
|
|
174
|
+
try {
|
|
175
|
+
[balance, transactions] = await Promise.all([getBalance(address, tokenAddress, {
|
|
176
|
+
resolver,
|
|
177
|
+
txn
|
|
178
|
+
}), resolver._getAllResults("transactions", (paging) => resolver.listTransactions({
|
|
179
|
+
paging,
|
|
180
|
+
accountFilter: { accounts: [address] }
|
|
181
|
+
}, ctx), txLimit)]);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (e?.code === "EXCEED_LIMIT") {
|
|
184
|
+
logger.warn("Skip checking account cause tx count exceeding limit", {
|
|
185
|
+
address,
|
|
186
|
+
txLimit
|
|
187
|
+
});
|
|
188
|
+
checkedAccounts.set(address, true);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
let transferIn = getInitialBalance(address, resolver.config);
|
|
194
|
+
let transferOut = ZERO;
|
|
195
|
+
for (const tx of transactions) {
|
|
196
|
+
if (!checkedTx.has(tx.hash)) {
|
|
197
|
+
await fixMigrateReceipts(tx, resolver);
|
|
198
|
+
checkedTx.set(tx.hash, getTransferList(tx, tokenAddress));
|
|
199
|
+
}
|
|
200
|
+
const { transferInList, transferOutList } = checkedTx.get(tx.hash);
|
|
201
|
+
const transferInAmount = transferInList.filter((item) => isSameDid(item.address, address)).map((item) => item.value).reduce((prev, cur) => prev.add(cur), ZERO);
|
|
202
|
+
const transferOutAmount = transferOutList.filter((item) => isSameDid(item.address, address)).map((item) => item.value).reduce((prev, cur) => prev.add(cur), ZERO);
|
|
203
|
+
transferIn = transferIn.add(transferInAmount);
|
|
204
|
+
transferOut = transferOut.add(transferOutAmount);
|
|
205
|
+
if (transferInAmount.gt(ZERO)) {
|
|
206
|
+
if (!accountQueue[depth + 1]) accountQueue[depth + 1] = [];
|
|
207
|
+
const accountsToQueue = transferOutList.filter((item) => {
|
|
208
|
+
if (checkedAccounts.has(item.address)) return false;
|
|
209
|
+
if (schemas.tokenHolder.validate(item.address).error) return false;
|
|
210
|
+
if (vaultAccounts.includes(item.address)) return false;
|
|
211
|
+
if (["gas", "fee"].includes(item.action || "")) return false;
|
|
212
|
+
return true;
|
|
213
|
+
}).map((item) => item.address);
|
|
214
|
+
accountQueue[depth + 1].push(...uniq(accountsToQueue).map((x) => ({
|
|
215
|
+
address: x,
|
|
216
|
+
chain: chain.concat()
|
|
217
|
+
})));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
checkedAccounts.set(address, true);
|
|
221
|
+
const diff = transferIn.sub(transferOut).sub(new BN(balance)).add(fromTokenToUnit(trustedConfig?.tolerance || 0));
|
|
222
|
+
if (diff.abs().gt(toleranceUnit)) {
|
|
223
|
+
const data = {
|
|
224
|
+
address: chain.join("->"),
|
|
225
|
+
balance,
|
|
226
|
+
transferIn: transferIn.toString(),
|
|
227
|
+
transferOut: transferOut.toString(),
|
|
228
|
+
accountCount: checkedAccounts.size,
|
|
229
|
+
txCount: checkedTx.size
|
|
230
|
+
};
|
|
231
|
+
logger.warn("Account balance does not match transfer records", {
|
|
232
|
+
...data,
|
|
233
|
+
sourceAccount: accountAddress,
|
|
234
|
+
tokenAddress,
|
|
235
|
+
diff: diff.toString(),
|
|
236
|
+
depth,
|
|
237
|
+
accountLimit,
|
|
238
|
+
txLimit,
|
|
239
|
+
tolerance
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
isRisky: true,
|
|
243
|
+
reason: "INVALID_BALANCE",
|
|
244
|
+
data
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
for (let depth = 0; depth < accountQueue.length; depth++) {
|
|
250
|
+
let isExecuted = false;
|
|
251
|
+
const result = await resolver.runAsLambda((txn) => {
|
|
252
|
+
if (isExecuted) throw new CustomError("INVALID_REQUEST", "verifyAccountRisk should not retry");
|
|
253
|
+
isExecuted = true;
|
|
254
|
+
return execute(depth, txn);
|
|
255
|
+
}, { retryLimit: 0 });
|
|
256
|
+
if (result) return result;
|
|
257
|
+
}
|
|
258
|
+
logger.info("Account risk check completed", {
|
|
259
|
+
address: accountAddress,
|
|
260
|
+
tokenAddress,
|
|
261
|
+
accountCount: checkedAccounts.size,
|
|
262
|
+
txCount: checkedTx.size,
|
|
263
|
+
depth: accountQueue.length,
|
|
264
|
+
accountLimit,
|
|
265
|
+
txLimit,
|
|
266
|
+
tolerance
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
isRisky: false,
|
|
270
|
+
data: {
|
|
271
|
+
accountCount: checkedAccounts.size,
|
|
272
|
+
txCount: checkedTx.size
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const listTokenFlows = async ({ accountAddress, tokenAddress, paging = {}, depth = 2, direction = "OUT" }, resolver, ctx = {}) => {
|
|
277
|
+
const { error } = paramsSchema.validate({
|
|
278
|
+
accountAddress,
|
|
279
|
+
tokenAddress,
|
|
280
|
+
resolver
|
|
281
|
+
});
|
|
282
|
+
if (error) throw new CustomError("INVALID_PARAMS", error.message);
|
|
283
|
+
const minAmount = fromTokenToUnit(1, (await resolver.tokenCache.get(tokenAddress))?.decimal || 18);
|
|
284
|
+
const maxAccountSize = Math.min(200, paging.size || 200);
|
|
285
|
+
const maxDepth = Math.min(5, depth);
|
|
286
|
+
const vaultAccounts = getVaultAccounts(resolver.config);
|
|
287
|
+
const tokenFlows = [];
|
|
288
|
+
const checkedAccounts = /* @__PURE__ */ new Map();
|
|
289
|
+
const checkedTx = /* @__PURE__ */ new Map();
|
|
290
|
+
let curDepth = 1;
|
|
291
|
+
const depthQueue = { [curDepth]: [accountAddress] };
|
|
292
|
+
while (depthQueue[curDepth]?.length && curDepth <= maxDepth && checkedAccounts.size < maxAccountSize) {
|
|
293
|
+
for (const address of depthQueue[curDepth]) {
|
|
294
|
+
if (checkedAccounts.has(address)) continue;
|
|
295
|
+
if (schemas.tokenHolder.validate(address).error) continue;
|
|
296
|
+
const transactions = await resolver._getAllResults("transactions", (page) => resolver.listTransactions({
|
|
297
|
+
paging: page,
|
|
298
|
+
accountFilter: { accounts: [address] },
|
|
299
|
+
tokenFilter: { tokens: [tokenAddress] }
|
|
300
|
+
}, ctx));
|
|
301
|
+
let accountsToQueue = [];
|
|
302
|
+
for (const tx of transactions) {
|
|
303
|
+
if (!checkedTx.has(tx.hash)) {
|
|
304
|
+
await fixMigrateReceipts(tx, resolver);
|
|
305
|
+
checkedTx.set(tx.hash, getTransferFlow(tx, tokenAddress));
|
|
306
|
+
}
|
|
307
|
+
const txTransfers = checkedTx.get(tx.hash).filter((item) => {
|
|
308
|
+
if (direction === "OUT" && item.from !== address) return false;
|
|
309
|
+
if (direction === "IN" && item.to !== address) return false;
|
|
310
|
+
if (item.value.lt(minAmount)) return false;
|
|
311
|
+
return true;
|
|
312
|
+
});
|
|
313
|
+
tokenFlows.push(...txTransfers.map((item) => ({
|
|
314
|
+
...item,
|
|
315
|
+
value: item.value.toString()
|
|
316
|
+
})));
|
|
317
|
+
accountsToQueue = accountsToQueue.concat(txTransfers.map((item) => direction === "IN" ? item.from : item.to).filter((item) => !vaultAccounts.includes(item)));
|
|
318
|
+
}
|
|
319
|
+
checkedAccounts.set(address, true);
|
|
320
|
+
if (checkedAccounts.size >= maxAccountSize) break;
|
|
321
|
+
if (!depthQueue[curDepth + 1]) depthQueue[curDepth + 1] = [];
|
|
322
|
+
depthQueue[curDepth + 1].push(...uniq(accountsToQueue));
|
|
323
|
+
}
|
|
324
|
+
curDepth++;
|
|
325
|
+
}
|
|
326
|
+
return tokenFlows;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
//#endregion
|
|
330
|
+
export { fixMigrateReceipts, getTransferFlow, getTransferList, listTokenFlows, verifyAccountRisk };
|
package/esm/types.d.mts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Address, BN, DirectionMap, Hash, IAccountState, IAddressArgs, IAny, IAssetFactoryState, IAssetState, IBalanceTable, IChainConfig, IChainConfig as IChainConfig$1, IChainState, IDelegateState, IEvidenceState, IExecutedContext, IForeignToken, IGetAccountStateArgs, IGetAccountTokensArgs, IIndexDB, IIndexDB as IIndexDB$1, IIndexTable, IIndexTable as IIndexTable$1, IListAccountsResult, IListAssetsResult, IListDelegationsResult, IListFactoriesResult, IListRollupBlocksResult, IListRollupValidatorsResult, IListRollupsResult, IListStakesResult, IListTokenFactoriesResult, IListTokensResult, IListTransactionsResult, IOperationContext, IPipelineLogger, IPipelineLogger as IPipelineLogger$1, IResolver, IResolverAccountState, IResolverChainInfo, IResolverContext, IResolverContext as IResolverContext$1, IResolverFactoryState, IResolverForgeStats, IResolverListAccountsResult, IResolverListAssetsResult, IResolverListDelegationsResult, IResolverListFactoriesResult, IResolverListRollupBlocksResult, IResolverListRollupValidatorsResult, IResolverListRollupsResult, IResolverListStakesResult, IResolverListTokenFactoriesResult, IResolverListTokensResult, IResolverListTransactionsResult, IResolverNetInfo, IResolverNodeInfo, IResolverPaging, IResolverPaging as IResolverPaging$1, IResolverRollupState, IResolverStakeState, IResolverTransaction, IResolverTxItxJson, IResolverValidatorsInfo, IRollupBlock, IRollupState, IRollupValidator, IStakeState, IStateContext, IStateDB, IStateDB as IStateDB$1, IStateTable, IStateTable as IStateTable$1, ITokenConfig, ITokenFactoryState, ITokenFlowsResult, ITokenInfo, ITokenInfo as ITokenInfo$1, ITokenMetadata, ITokenState, ITransactionConfig, ITxGasConfig, ITxStakeConfig, ITxState, IVaultsConfig, IndexTableTypeMap, Promisable, TAccountFilter, TAccountToken, TAddressFilter, TAssetFilter, TDelegationFilter, TFactoryFilter, TIndexedAccountState, TIndexedAssetState, TIndexedDelegationState, TIndexedFactoryState, TIndexedRollupBlock, TIndexedRollupState, TIndexedRollupValidator, TIndexedStakeState, TIndexedTokenFactoryState, TIndexedTokenState, TIndexedTokenState as TIndexedTokenState$1, TIndexedTransaction, TPage, TPageInfo, TPageOrder, TRangeFilter, TReceiptChange, TRequestEstimateGas, TRequestGetEvidenceState, TRequestGetRollupBlock, TRequestGetTokenDistribution, TRequestGetTx, TRequestListAssetTransactions, TRequestListAssets, TRequestListDelegations, TRequestListFactories, TRequestListRollupBlocks, TRequestListRollupValidators, TRequestListRollups, TRequestListStakes, TRequestListTokenFactories, TRequestListTokenFlows, TRequestListTokens, TRequestListTopAccounts, TRequestListTransactions, TRequestSearch, TRequestVerifyAccountRisk, TRollupFilter, TSearchResult, TStakeFilter, TTimeFilter, TTokenDistribution, TTokenFactoryFilter, TTokenFilter, TTokenMeta, TTransactionReceipt, TTxFilter, TTypeFilter, TValidatorFilter, TValidityFilter, TVerifyAccountRiskResult, TokenBalanceMap, ValidityMap } from "@ocap/types";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flexible StateDB type that supports dynamic table access
|
|
7
|
+
*/
|
|
8
|
+
type FlexibleStateDB = IStateDB$1 & {
|
|
9
|
+
[key: string]: IStateTable$1<unknown>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Flexible IndexDB type that supports dynamic table access
|
|
13
|
+
*/
|
|
14
|
+
type FlexibleIndexDB = IIndexDB$1 & {
|
|
15
|
+
[key: string]: IIndexTable$1<unknown> | ((...args: unknown[]) => Promise<unknown>);
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Data object for encoded data fields (typeUrl + value)
|
|
19
|
+
*/
|
|
20
|
+
interface IDataObject {
|
|
21
|
+
type_url?: string;
|
|
22
|
+
typeUrl?: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Delegation operation
|
|
29
|
+
*/
|
|
30
|
+
interface IDelegationOp {
|
|
31
|
+
key: string;
|
|
32
|
+
value: {
|
|
33
|
+
limit?: {
|
|
34
|
+
tokens?: ITokenInfo$1[];
|
|
35
|
+
};
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Chunk iterator for paginated data fetching
|
|
41
|
+
*/
|
|
42
|
+
interface IChunkIterator<T> {
|
|
43
|
+
next: () => Promise<T[]>;
|
|
44
|
+
done: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parameters for list chunks operation
|
|
48
|
+
*/
|
|
49
|
+
interface IListChunksParams {
|
|
50
|
+
total: number;
|
|
51
|
+
concurrency?: number;
|
|
52
|
+
chunkSize?: number;
|
|
53
|
+
pageSize?: number;
|
|
54
|
+
timeKey: string;
|
|
55
|
+
dataKey: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generic paginated result
|
|
59
|
+
*/
|
|
60
|
+
interface IPaginatedResult<T> {
|
|
61
|
+
paging?: IResolverPaging$1;
|
|
62
|
+
[key: string]: T[] | IResolverPaging$1 | unknown;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Filter interface for risk verification
|
|
66
|
+
*/
|
|
67
|
+
interface IFilter {
|
|
68
|
+
getTrustedAccountConfig?: (address: string) => Promise<ITrustedAccountConfig | null>;
|
|
69
|
+
[key: string]: any;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Trusted account configuration
|
|
73
|
+
*/
|
|
74
|
+
interface ITrustedAccountConfig {
|
|
75
|
+
tolerance?: string | number;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolver constructor parameters
|
|
80
|
+
*/
|
|
81
|
+
interface IResolverParams {
|
|
82
|
+
statedb: IStateDB$1;
|
|
83
|
+
indexdb: IIndexDB$1;
|
|
84
|
+
config: IChainConfig$1;
|
|
85
|
+
filter?: IFilter;
|
|
86
|
+
validateTokenConfig?: boolean;
|
|
87
|
+
logger?: IPipelineLogger$1;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get state parameters
|
|
91
|
+
*/
|
|
92
|
+
interface IGetStateParams {
|
|
93
|
+
table: string;
|
|
94
|
+
id: string;
|
|
95
|
+
dataKey?: string | string[] | null;
|
|
96
|
+
extraParams?: Record<string, unknown>;
|
|
97
|
+
onRead?: (state: any) => any;
|
|
98
|
+
expandContext?: boolean;
|
|
99
|
+
ctx?: IResolverContext$1;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Token cache interface
|
|
103
|
+
*/
|
|
104
|
+
interface ITokenCache {
|
|
105
|
+
get: (address: string) => Promise<TIndexedTokenState$1 | null>;
|
|
106
|
+
set: (address: string, token: TIndexedTokenState$1) => void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Migration chain interface
|
|
110
|
+
*/
|
|
111
|
+
interface IMigrationChain {
|
|
112
|
+
findAddressAtTime: (address: string, time: Date) => string;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { type Address, type BN, type DirectionMap, FlexibleIndexDB, FlexibleStateDB, type Hash, type IAccountState, type IAddressArgs, type IAny, type IAssetFactoryState, type IAssetState, type IBalanceTable, type IChainConfig, type IChainState, IChunkIterator, IDataObject, type IDelegateState, IDelegationOp, type IEvidenceState, type IExecutedContext, IFilter, type IForeignToken, type IGetAccountStateArgs, type IGetAccountTokensArgs, IGetStateParams, type IIndexDB, type IIndexTable, type IListAccountsResult, type IListAssetsResult, IListChunksParams, type IListDelegationsResult, type IListFactoriesResult, type IListRollupBlocksResult, type IListRollupValidatorsResult, type IListRollupsResult, type IListStakesResult, type IListTokenFactoriesResult, type IListTokensResult, type IListTransactionsResult, IMigrationChain, type IOperationContext, IPaginatedResult, type IPipelineLogger, type IResolver, type IResolverAccountState, type IResolverChainInfo, type IResolverContext, type IResolverFactoryState, type IResolverForgeStats, type IResolverListAccountsResult, type IResolverListAssetsResult, type IResolverListDelegationsResult, type IResolverListFactoriesResult, type IResolverListRollupBlocksResult, type IResolverListRollupValidatorsResult, type IResolverListRollupsResult, type IResolverListStakesResult, type IResolverListTokenFactoriesResult, type IResolverListTokensResult, type IResolverListTransactionsResult, type IResolverNetInfo, type IResolverNodeInfo, type IResolverPaging, IResolverParams, type IResolverRollupState, type IResolverStakeState, type IResolverTransaction, type IResolverTxItxJson, type IResolverValidatorsInfo, type IRollupBlock, type IRollupState, type IRollupValidator, type IStakeState, type IStateContext, type IStateDB, type IStateTable, ITokenCache, type ITokenConfig, type ITokenFactoryState, type ITokenFlowsResult, type ITokenInfo, type ITokenMetadata, type ITokenState, type ITransactionConfig, ITrustedAccountConfig, type ITxGasConfig, type ITxStakeConfig, type ITxState, type IVaultsConfig, type IndexTableTypeMap, type Promisable, type TAccountFilter, type TAccountToken, type TAddressFilter, type TAssetFilter, type TDelegationFilter, type TFactoryFilter, type TIndexedAccountState, type TIndexedAssetState, type TIndexedDelegationState, type TIndexedFactoryState, type TIndexedRollupBlock, type TIndexedRollupState, type TIndexedRollupValidator, type TIndexedStakeState, type TIndexedTokenFactoryState, type TIndexedTokenState, type TIndexedTransaction, type TPage, type TPageInfo, type TPageOrder, type TRangeFilter, type TReceiptChange, type TRequestEstimateGas, type TRequestGetEvidenceState, type TRequestGetRollupBlock, type TRequestGetTokenDistribution, type TRequestGetTx, type TRequestListAssetTransactions, type TRequestListAssets, type TRequestListDelegations, type TRequestListFactories, type TRequestListRollupBlocks, type TRequestListRollupValidators, type TRequestListRollups, type TRequestListStakes, type TRequestListTokenFactories, type TRequestListTokenFlows, type TRequestListTokens, type TRequestListTopAccounts, type TRequestListTransactions, type TRequestSearch, type TRequestVerifyAccountRisk, type TRollupFilter, type TSearchResult, type TStakeFilter, type TTimeFilter, type TTokenDistribution, type TTokenFactoryFilter, type TTokenFilter, type TTokenMeta, type TTransactionReceipt, type TTxFilter, type TTypeFilter, type TValidatorFilter, type TValidityFilter, type TVerifyAccountRiskResult, type TokenBalanceMap, type ValidityMap };
|
package/esm/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
|
|
29
|
+
exports.__toESM = __toESM;
|
package/lib/api.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let _arcblock_nft_display = require("@arcblock/nft-display");
|
|
3
|
+
let express = require("express");
|
|
4
|
+
|
|
5
|
+
//#region src/api.ts
|
|
6
|
+
function createAPIHandler({ resolver }) {
|
|
7
|
+
const router = (0, express.Router)();
|
|
8
|
+
router.get("/token/display", async (req, res) => {
|
|
9
|
+
const { address } = req.query;
|
|
10
|
+
if (!address) return res.status(400).json({ error: "address is required" });
|
|
11
|
+
const token = await resolver.getTokenState({ address });
|
|
12
|
+
if (!token) return res.status(400).json({ error: "token not found" });
|
|
13
|
+
let iconUri = "";
|
|
14
|
+
if (token.icon) {
|
|
15
|
+
let iconWidth = 40;
|
|
16
|
+
let iconHeight = 40;
|
|
17
|
+
const viewBoxMatch = token.icon.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
|
18
|
+
if (viewBoxMatch) {
|
|
19
|
+
const viewBoxValues = viewBoxMatch[1].split(/\s+/);
|
|
20
|
+
if (viewBoxValues.length >= 4) {
|
|
21
|
+
iconWidth = +viewBoxValues[2] || iconWidth;
|
|
22
|
+
iconHeight = +viewBoxValues[3] || iconHeight;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const fixedSvg = token.icon.replace(/<svg([^>]*)>/, `<svg$1 width="${iconWidth}" height="${iconHeight}">`);
|
|
26
|
+
iconUri = `data:image/svg+xml;base64,${Buffer.from(fixedSvg).toString("base64")}`;
|
|
27
|
+
}
|
|
28
|
+
const svg = (0, _arcblock_nft_display.getSvg)({
|
|
29
|
+
color: (0, _arcblock_nft_display.getNftBGColorFromDid)(address),
|
|
30
|
+
did: address,
|
|
31
|
+
variant: "app-passport",
|
|
32
|
+
verifiable: true,
|
|
33
|
+
chain: "arcblock",
|
|
34
|
+
header: {
|
|
35
|
+
icon: iconUri,
|
|
36
|
+
name: token.symbol
|
|
37
|
+
},
|
|
38
|
+
issuer: {
|
|
39
|
+
name: token.symbol,
|
|
40
|
+
icon: iconUri
|
|
41
|
+
},
|
|
42
|
+
extra: {
|
|
43
|
+
key: "Name",
|
|
44
|
+
value: token.name
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
48
|
+
res.send(svg);
|
|
49
|
+
});
|
|
50
|
+
return router;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
exports.createAPIHandler = createAPIHandler;
|