agentex-creator-sdk 1.0.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
- package/dist/packages/contracts/src/deployer-investigation.js +101 -0
- package/dist/packages/contracts/src/index.d.ts +1701 -0
- package/dist/packages/contracts/src/index.js +380 -0
- package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
- package/dist/packages/contracts/src/indexed-activity.js +71 -0
- package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
- package/dist/packages/contracts/src/indexed-agents.js +131 -0
- package/dist/packages/contracts/src/inspection.d.ts +906 -0
- package/dist/packages/contracts/src/inspection.js +114 -0
- package/dist/packages/contracts/src/kinds.d.ts +5398 -0
- package/dist/packages/contracts/src/kinds.js +156 -0
- package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
- package/dist/packages/contracts/src/report-presentation.js +120 -0
- package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
- package/dist/packages/contracts/src/solana-inspection.js +94 -0
- package/dist/packages/contracts/src/token-market.d.ts +193 -0
- package/dist/packages/contracts/src/token-market.js +335 -0
- package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
- package/dist/packages/contracts/src/wallet-analysis.js +89 -0
- package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
- package/dist/packages/contracts/src/watchtower.js +196 -0
- package/dist/packages/contracts/src/workflow.d.ts +1568 -0
- package/dist/packages/contracts/src/workflow.js +651 -0
- package/dist/packages/inspector/src/decode.d.ts +23 -0
- package/dist/packages/inspector/src/decode.js +150 -0
- package/dist/packages/inspector/src/scope.d.ts +87 -0
- package/dist/packages/inspector/src/scope.js +64 -0
- package/dist/packages/model/src/analysis.d.ts +149 -0
- package/dist/packages/model/src/analysis.js +387 -0
- package/dist/packages/model/src/pricing.d.ts +38 -0
- package/dist/packages/model/src/pricing.js +49 -0
- package/dist/packages/model/src/retry.d.ts +20 -0
- package/dist/packages/model/src/retry.js +31 -0
- package/dist/packages/model/src/schema.d.ts +10 -0
- package/dist/packages/model/src/schema.js +51 -0
- package/dist/packages/model/src/summary.d.ts +91 -0
- package/dist/packages/model/src/summary.js +177 -0
- package/dist/packages/model/src/types.d.ts +81 -0
- package/dist/packages/model/src/types.js +19 -0
- package/dist/packages/monitoring/src/delivery.d.ts +32 -0
- package/dist/packages/monitoring/src/delivery.js +53 -0
- package/dist/packages/providers/src/chain-transport.d.ts +42 -0
- package/dist/packages/providers/src/chain-transport.js +57 -0
- package/dist/packages/providers/src/coverage.d.ts +105 -0
- package/dist/packages/providers/src/coverage.js +260 -0
- package/dist/packages/providers/src/health.d.ts +273 -0
- package/dist/packages/providers/src/health.js +505 -0
- package/dist/packages/providers/src/keyed.d.ts +96 -0
- package/dist/packages/providers/src/keyed.js +240 -0
- package/dist/packages/providers/src/snapshot.d.ts +61 -0
- package/dist/packages/providers/src/snapshot.js +77 -0
- package/dist/packages/publication/src/fixtures.d.ts +45 -0
- package/dist/packages/publication/src/fixtures.js +350 -0
- package/dist/packages/research/src/index.d.ts +188 -0
- package/dist/packages/research/src/index.js +829 -0
- package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
- package/dist/packages/runtime/src/checkpoints.js +214 -0
- package/dist/packages/runtime/src/policy.d.ts +57 -0
- package/dist/packages/runtime/src/policy.js +296 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
- package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex.js +3 -0
- package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
- package/dist/packages/sdk/src/buyer-cli.js +210 -0
- package/dist/packages/sdk/src/buyer.d.ts +534 -0
- package/dist/packages/sdk/src/buyer.js +441 -0
- package/dist/packages/sdk/src/cli.d.ts +14 -0
- package/dist/packages/sdk/src/cli.js +149 -0
- package/dist/packages/sdk/src/errors.d.ts +31 -0
- package/dist/packages/sdk/src/errors.js +24 -0
- package/dist/packages/sdk/src/index.d.ts +236 -0
- package/dist/packages/sdk/src/index.js +150 -0
- package/dist/packages/sdk/src/local.d.ts +11 -0
- package/dist/packages/sdk/src/local.js +110 -0
- package/dist/packages/sdk/src/rails.d.ts +59 -0
- package/dist/packages/sdk/src/rails.js +96 -0
- package/dist/packages/sdk/src/report.d.ts +121 -0
- package/dist/packages/sdk/src/report.js +114 -0
- package/dist/packages/sdk/src/version.d.ts +2 -0
- package/dist/packages/sdk/src/version.js +2 -0
- package/dist/packages/watchtower/src/index.d.ts +150 -0
- package/dist/packages/watchtower/src/index.js +786 -0
- package/dist/packages/workflow/src/registry.d.ts +61 -0
- package/dist/packages/workflow/src/registry.js +76 -0
- package/examples/README.md +34 -0
- package/examples/cli-usage.sh +30 -0
- package/examples/fixtures/base-weth-input.json +4 -0
- package/examples/focused-researcher.json +127 -0
- package/examples/pay-with-eth-robinhood.mts +37 -0
- package/examples/pay-with-usdc.mts +41 -0
- package/examples/quickstart.mts +73 -0
- package/package.json +50 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
import { type EvidenceItem, type MarketPolicy, type TokenMarket } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Token Researcher 0.3.0 market reads (P01): observed DEX pools on listed venues and balances of recent transfer recipients.
|
|
5
|
+
*
|
|
6
|
+
* - Pools are found only by factory lookups (`getPair` / `getPool`) on the venues and quote tokens listed below, in ONE Multicall3
|
|
7
|
+
* `aggregate3` eth_call at the pinned block. Other venues, other quote tokens, Uniswap V4 and aggregators are never checked.
|
|
8
|
+
* - Recent recipients come from ONE `alchemy_getAssetTransfers` page (newest first, at or before the pinned block). They are recent
|
|
9
|
+
* activity, never a holder list, ranking or distribution.
|
|
10
|
+
* - Pool token balances, reserves, V3 price and active liquidity, and recipient balances are read in ONE more `aggregate3` eth_call.
|
|
11
|
+
*
|
|
12
|
+
* Every factory address and quote token below was checked live (code present, known pair returned, symbol and decimals read) on
|
|
13
|
+
* 15 September 2026. Pure functions only: the executor, checkpoint scope, publication suite and result contract all derive the
|
|
14
|
+
* same section from captured receipts, so no value can be asserted without its receipt.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
17
|
+
export declare const MULTICALL3_DOCUMENTATION = "https://www.multicall3.com/deployments";
|
|
18
|
+
/** Gas for one market aggregate3 eth_call (1,500,000). eth_call gas is not spent on chain; the subcall count is bounded below. */
|
|
19
|
+
export declare const MARKET_CALL_GAS = "0x16e360";
|
|
20
|
+
export declare const MAXIMUM_MARKET_POOLS = 8;
|
|
21
|
+
export declare const MAXIMUM_RECENT_TRANSFERS = 25;
|
|
22
|
+
export declare const MAXIMUM_RECENT_RECIPIENTS = 10;
|
|
23
|
+
export declare const HOLDER_ACTIVITY_METHOD = "alchemy_getAssetTransfers";
|
|
24
|
+
export declare const HOLDER_ACTIVITY_DOCUMENTATION = "https://www.alchemy.com/docs/reference/alchemy-getassettransfers";
|
|
25
|
+
/** Coverage rows (packages/providers coverage.ts) that gate each market read on a chain. */
|
|
26
|
+
export declare const MARKET_COVERAGE_TOOLS: Readonly<{
|
|
27
|
+
pools: "evm.dex-pool-discovery@0.1.0";
|
|
28
|
+
holders: "evm.recent-holder-activity@0.1.0";
|
|
29
|
+
}>;
|
|
30
|
+
export type MarketProtocol = 'uniswap-v2' | 'uniswap-v3' | 'aerodrome';
|
|
31
|
+
export interface MarketVenue {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
protocol: MarketProtocol;
|
|
35
|
+
factory: string;
|
|
36
|
+
feeTiers: readonly number[] | null;
|
|
37
|
+
stableFlags: readonly boolean[] | null;
|
|
38
|
+
documentationUrl: string;
|
|
39
|
+
}
|
|
40
|
+
export interface MarketQuoteToken {
|
|
41
|
+
address: string;
|
|
42
|
+
symbol: string;
|
|
43
|
+
decimals: number;
|
|
44
|
+
}
|
|
45
|
+
export interface MarketVenueSet {
|
|
46
|
+
chain: 'ethereum' | 'base' | 'robinhood';
|
|
47
|
+
venues: readonly MarketVenue[];
|
|
48
|
+
quotes: readonly MarketQuoteToken[];
|
|
49
|
+
}
|
|
50
|
+
/** Keyed by chain id: Robinhood Chain testnet (46630) has no listed venue, so its pool section is unsupported. */
|
|
51
|
+
export declare const MARKET_VENUES: Readonly<Record<number, MarketVenueSet>>;
|
|
52
|
+
export declare const marketVenuesFor: (chainId: number | null | undefined) => MarketVenueSet | null;
|
|
53
|
+
/** Exported for fixtures and verifiers that decode or answer aggregate3 requests. */
|
|
54
|
+
export declare const MULTICALL3_ABI: readonly [{
|
|
55
|
+
readonly name: "aggregate3";
|
|
56
|
+
readonly type: "function";
|
|
57
|
+
readonly stateMutability: "payable";
|
|
58
|
+
readonly inputs: readonly [{
|
|
59
|
+
readonly type: "tuple[]";
|
|
60
|
+
readonly components: readonly [{
|
|
61
|
+
readonly type: "address";
|
|
62
|
+
readonly name: "target";
|
|
63
|
+
}, {
|
|
64
|
+
readonly type: "bool";
|
|
65
|
+
readonly name: "allowFailure";
|
|
66
|
+
}, {
|
|
67
|
+
readonly type: "bytes";
|
|
68
|
+
readonly name: "callData";
|
|
69
|
+
}];
|
|
70
|
+
readonly name: "calls";
|
|
71
|
+
}];
|
|
72
|
+
readonly outputs: readonly [{
|
|
73
|
+
readonly type: "tuple[]";
|
|
74
|
+
readonly components: readonly [{
|
|
75
|
+
readonly type: "bool";
|
|
76
|
+
readonly name: "success";
|
|
77
|
+
}, {
|
|
78
|
+
readonly type: "bytes";
|
|
79
|
+
readonly name: "returnData";
|
|
80
|
+
}];
|
|
81
|
+
readonly name: "returnData";
|
|
82
|
+
}];
|
|
83
|
+
}];
|
|
84
|
+
export declare const MARKET_SELECTORS: Readonly<{
|
|
85
|
+
balanceOf: "0x70a08231";
|
|
86
|
+
getReserves: "0x0902f1ac";
|
|
87
|
+
slot0: "0x3850c7bd";
|
|
88
|
+
liquidity: "0x1a686502";
|
|
89
|
+
}>;
|
|
90
|
+
export declare class MarketDecodeError extends Error {
|
|
91
|
+
constructor(message: string);
|
|
92
|
+
}
|
|
93
|
+
type VenueFields = {
|
|
94
|
+
venue: string;
|
|
95
|
+
protocol: MarketProtocol;
|
|
96
|
+
factory: string;
|
|
97
|
+
quoteToken: string;
|
|
98
|
+
quoteSymbol: string;
|
|
99
|
+
quoteDecimals: number;
|
|
100
|
+
feeTier: number | null;
|
|
101
|
+
stable: boolean | null;
|
|
102
|
+
};
|
|
103
|
+
export interface DiscoveryEntry extends VenueFields {
|
|
104
|
+
callData: Hex;
|
|
105
|
+
}
|
|
106
|
+
/** Factory lookups for every listed (quote token, venue, fee tier or stable flag), quote tokens first so WETH pools lead. The token itself is never its own quote. */
|
|
107
|
+
export declare function discoveryPlan(set: MarketVenueSet, token: string): DiscoveryEntry[];
|
|
108
|
+
/** Multicall3 aggregate3 with allowFailure on every subcall: one failed lookup is recorded, never hidden, and never fails the others. */
|
|
109
|
+
export declare function encodeAggregate(calls: readonly {
|
|
110
|
+
target: string;
|
|
111
|
+
callData: Hex;
|
|
112
|
+
}[]): Hex;
|
|
113
|
+
/** The discovery multicall: each lookup targets its venue's factory. */
|
|
114
|
+
export declare const encodeDiscovery: (plan: readonly DiscoveryEntry[]) => Hex;
|
|
115
|
+
/** The eth_call object for a market multicall. Key order is fixed so checkpoint requests hash identically. */
|
|
116
|
+
export declare const marketCall: (data: Hex) => {
|
|
117
|
+
to: string;
|
|
118
|
+
data: `0x${string}`;
|
|
119
|
+
gas: string;
|
|
120
|
+
};
|
|
121
|
+
export declare function decodeAggregate(result: unknown, expected: number): {
|
|
122
|
+
success: boolean;
|
|
123
|
+
returnData: Hex;
|
|
124
|
+
}[];
|
|
125
|
+
export interface CheckedVenue extends VenueFields {
|
|
126
|
+
lookup: 'pool' | 'no-pool' | 'call-failed';
|
|
127
|
+
pool: string | null;
|
|
128
|
+
}
|
|
129
|
+
export interface FoundPool extends VenueFields {
|
|
130
|
+
address: string;
|
|
131
|
+
}
|
|
132
|
+
export declare function poolsFromDiscovery(plan: readonly DiscoveryEntry[], results: readonly {
|
|
133
|
+
success: boolean;
|
|
134
|
+
returnData: Hex;
|
|
135
|
+
}[]): {
|
|
136
|
+
checked: CheckedVenue[];
|
|
137
|
+
found: FoundPool[];
|
|
138
|
+
};
|
|
139
|
+
/** The exact indexed request: newest ERC-20 transfers of this token at or before the pinned block, zero values excluded. */
|
|
140
|
+
export declare function holderTransferRequest(token: string, snapshotBlock: string): {
|
|
141
|
+
fromBlock: string;
|
|
142
|
+
toBlock: string;
|
|
143
|
+
contractAddresses: string[];
|
|
144
|
+
category: string[];
|
|
145
|
+
order: string;
|
|
146
|
+
maxCount: string;
|
|
147
|
+
excludeZeroValue: boolean;
|
|
148
|
+
withMetadata: boolean;
|
|
149
|
+
};
|
|
150
|
+
export interface RecentRecipient {
|
|
151
|
+
address: string;
|
|
152
|
+
latestReceiptBlock: string;
|
|
153
|
+
}
|
|
154
|
+
export declare function recipientsFromTransfers(result: unknown, token: string, snapshotBlock: string): {
|
|
155
|
+
examined: number;
|
|
156
|
+
truncated: boolean;
|
|
157
|
+
recipients: RecentRecipient[];
|
|
158
|
+
};
|
|
159
|
+
export interface BalanceEntry {
|
|
160
|
+
kind: 'pool-token' | 'pool-quote' | 'pool-reserves' | 'pool-slot0' | 'pool-liquidity' | 'recipient';
|
|
161
|
+
subject: string;
|
|
162
|
+
target: string;
|
|
163
|
+
callData: Hex;
|
|
164
|
+
}
|
|
165
|
+
export declare function balancesPlan(token: string, pools: readonly FoundPool[], recipients: readonly RecentRecipient[]): BalanceEntry[];
|
|
166
|
+
type EvidenceLike = Pick<EvidenceItem, 'id' | 'source' | 'request' | 'result' | 'completeness'>;
|
|
167
|
+
/**
|
|
168
|
+
* Derives the whole market section from captured receipts. The executor and every verifier call this same function, so a report's
|
|
169
|
+
* market section is valid only when it equals the derivation from its own evidence. `unsupported` carries server-side availability
|
|
170
|
+
* decisions (coverage or credential), which are not derivable from receipts.
|
|
171
|
+
*/
|
|
172
|
+
export declare function buildMarketSection(args: {
|
|
173
|
+
policy: MarketPolicy;
|
|
174
|
+
chainId: number | null;
|
|
175
|
+
token: string;
|
|
176
|
+
snapshotBlock: string | null;
|
|
177
|
+
evidence: readonly EvidenceLike[];
|
|
178
|
+
unsupported: {
|
|
179
|
+
pools: string | null;
|
|
180
|
+
holders: string | null;
|
|
181
|
+
};
|
|
182
|
+
}): TokenMarket;
|
|
183
|
+
/** True when the balances multicall for a derived plan is still unread (the executor reads it once). */
|
|
184
|
+
export declare const marketBalancesData: (token: string, pools: readonly FoundPool[], recipients: readonly RecentRecipient[]) => {
|
|
185
|
+
plan: BalanceEntry[];
|
|
186
|
+
data: `0x${string}`;
|
|
187
|
+
} | null;
|
|
188
|
+
export declare function marketLimitations(chainId: number | null, policy: MarketPolicy): string[];
|
|
189
|
+
/** Checkpoint scope: a market eth_call is exactly the discovery lookup for this token, or a bounded balances/pool-state read. */
|
|
190
|
+
export declare function marketCallInScope(call: unknown, token: string, chainId: number): boolean;
|
|
191
|
+
/** Checkpoint scope for the one indexed transfer page. */
|
|
192
|
+
export declare function marketTransferRequestInScope(request: readonly unknown[], token: string, snapshotBlock: string): boolean;
|
|
193
|
+
export {};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { decodeAbiParameters, decodeFunctionData, decodeFunctionResult, encodeFunctionData, getAddress, isAddress, parseAbi } from 'viem';
|
|
2
|
+
import { contentHash } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Token Researcher 0.3.0 market reads (P01): observed DEX pools on listed venues and balances of recent transfer recipients.
|
|
5
|
+
*
|
|
6
|
+
* - Pools are found only by factory lookups (`getPair` / `getPool`) on the venues and quote tokens listed below, in ONE Multicall3
|
|
7
|
+
* `aggregate3` eth_call at the pinned block. Other venues, other quote tokens, Uniswap V4 and aggregators are never checked.
|
|
8
|
+
* - Recent recipients come from ONE `alchemy_getAssetTransfers` page (newest first, at or before the pinned block). They are recent
|
|
9
|
+
* activity, never a holder list, ranking or distribution.
|
|
10
|
+
* - Pool token balances, reserves, V3 price and active liquidity, and recipient balances are read in ONE more `aggregate3` eth_call.
|
|
11
|
+
*
|
|
12
|
+
* Every factory address and quote token below was checked live (code present, known pair returned, symbol and decimals read) on
|
|
13
|
+
* 15 September 2026. Pure functions only: the executor, checkpoint scope, publication suite and result contract all derive the
|
|
14
|
+
* same section from captured receipts, so no value can be asserted without its receipt.
|
|
15
|
+
*/
|
|
16
|
+
export const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
|
|
17
|
+
export const MULTICALL3_DOCUMENTATION = 'https://www.multicall3.com/deployments';
|
|
18
|
+
/** Gas for one market aggregate3 eth_call (1,500,000). eth_call gas is not spent on chain; the subcall count is bounded below. */
|
|
19
|
+
export const MARKET_CALL_GAS = '0x16e360';
|
|
20
|
+
export const MAXIMUM_MARKET_POOLS = 8;
|
|
21
|
+
export const MAXIMUM_RECENT_TRANSFERS = 25;
|
|
22
|
+
export const MAXIMUM_RECENT_RECIPIENTS = 10;
|
|
23
|
+
export const HOLDER_ACTIVITY_METHOD = 'alchemy_getAssetTransfers';
|
|
24
|
+
export const HOLDER_ACTIVITY_DOCUMENTATION = 'https://www.alchemy.com/docs/reference/alchemy-getassettransfers';
|
|
25
|
+
/** Coverage rows (packages/providers coverage.ts) that gate each market read on a chain. */
|
|
26
|
+
export const MARKET_COVERAGE_TOOLS = Object.freeze({ pools: 'evm.dex-pool-discovery@0.1.0', holders: 'evm.recent-holder-activity@0.1.0' });
|
|
27
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
28
|
+
const V3_FEES = Object.freeze([100, 500, 3000, 10000]);
|
|
29
|
+
const UNISWAP_V2_DOCS = 'https://developers.uniswap.org/docs/protocols/v2/deployments';
|
|
30
|
+
const UNISWAP_V3_DOCS = 'https://developers.uniswap.org/docs/protocols/v3/deployments';
|
|
31
|
+
const venue = (id, label, protocol, factory, documentationUrl, extra = {}) => Object.freeze({ id, label, protocol, factory: getAddress(factory), feeTiers: extra.feeTiers ?? null, stableFlags: extra.stableFlags ?? null, documentationUrl });
|
|
32
|
+
const quote = (address, symbol, decimals) => Object.freeze({ address: getAddress(address), symbol, decimals });
|
|
33
|
+
/** Keyed by chain id: Robinhood Chain testnet (46630) has no listed venue, so its pool section is unsupported. */
|
|
34
|
+
export const MARKET_VENUES = Object.freeze({
|
|
35
|
+
1: Object.freeze({ chain: 'ethereum', venues: Object.freeze([
|
|
36
|
+
venue('uniswap-v2', 'Uniswap V2', 'uniswap-v2', '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', UNISWAP_V2_DOCS),
|
|
37
|
+
venue('sushiswap-v2', 'SushiSwap V2', 'uniswap-v2', '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac', 'https://docs.sushi.com/contracts/cpamm'),
|
|
38
|
+
venue('uniswap-v3', 'Uniswap V3', 'uniswap-v3', '0x1F98431c8aD98523631AE4a59f267346ea31F984', UNISWAP_V3_DOCS, { feeTiers: V3_FEES }),
|
|
39
|
+
]), quotes: Object.freeze([quote('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'WETH', 18), quote('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'USDC', 6), quote('0xdAC17F958D2ee523a2206206994597C13D831ec7', 'USDT', 6)]) }),
|
|
40
|
+
8453: Object.freeze({ chain: 'base', venues: Object.freeze([
|
|
41
|
+
venue('uniswap-v2', 'Uniswap V2', 'uniswap-v2', '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6', UNISWAP_V2_DOCS),
|
|
42
|
+
venue('uniswap-v3', 'Uniswap V3', 'uniswap-v3', '0x33128a8fC17869897dcE68Ed026d694621f6FDfD', UNISWAP_V3_DOCS, { feeTiers: V3_FEES }),
|
|
43
|
+
venue('aerodrome', 'Aerodrome', 'aerodrome', '0x420DD381b31aEf6683db6B902084cB0FFECe40Da', 'https://github.com/aerodrome-finance/contracts', { stableFlags: Object.freeze([false, true]) }),
|
|
44
|
+
]), quotes: Object.freeze([quote('0x4200000000000000000000000000000000000006', 'WETH', 18), quote('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 'USDC', 6)]) }),
|
|
45
|
+
4663: Object.freeze({ chain: 'robinhood', venues: Object.freeze([
|
|
46
|
+
venue('uniswap-v2', 'Uniswap V2', 'uniswap-v2', '0x8bceaa40b9acdfaedf85adf4ff01f5ad6517937f', UNISWAP_V2_DOCS),
|
|
47
|
+
venue('uniswap-v3', 'Uniswap V3', 'uniswap-v3', '0x1f7d7550b1b028f7571e69a784071f0205fd2efa', UNISWAP_V3_DOCS, { feeTiers: V3_FEES }),
|
|
48
|
+
]), quotes: Object.freeze([quote('0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73', 'WETH', 18), quote('0x5fc5360d0400a0fd4f2af552add042d716f1d168', 'USDG', 6)]) }),
|
|
49
|
+
});
|
|
50
|
+
export const marketVenuesFor = (chainId) => (typeof chainId === 'number' ? MARKET_VENUES[chainId] ?? null : null);
|
|
51
|
+
const MULTICALL_ABI = parseAbi(['struct Call3 { address target; bool allowFailure; bytes callData; }', 'struct Result3 { bool success; bytes returnData; }', 'function aggregate3(Call3[] calls) payable returns (Result3[] returnData)']);
|
|
52
|
+
const FACTORY_ABI = parseAbi(['function getPair(address tokenA, address tokenB) view returns (address pair)', 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)']);
|
|
53
|
+
const AERODROME_ABI = parseAbi(['function getPool(address tokenA, address tokenB, bool stable) view returns (address pool)']);
|
|
54
|
+
const BALANCE_ABI = parseAbi(['function balanceOf(address account) view returns (uint256)']);
|
|
55
|
+
/** Exported for fixtures and verifiers that decode or answer aggregate3 requests. */
|
|
56
|
+
export const MULTICALL3_ABI = MULTICALL_ABI;
|
|
57
|
+
export const MARKET_SELECTORS = Object.freeze({ balanceOf: '0x70a08231', getReserves: '0x0902f1ac', slot0: '0x3850c7bd', liquidity: '0x1a686502' });
|
|
58
|
+
export class MarketDecodeError extends Error {
|
|
59
|
+
constructor(message) { super(message); this.name = 'MarketDecodeError'; }
|
|
60
|
+
}
|
|
61
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
/** Factory lookups for every listed (quote token, venue, fee tier or stable flag), quote tokens first so WETH pools lead. The token itself is never its own quote. */
|
|
63
|
+
export function discoveryPlan(set, token) {
|
|
64
|
+
const subject = getAddress(token);
|
|
65
|
+
const entries = [];
|
|
66
|
+
for (const q of set.quotes) {
|
|
67
|
+
if (q.address.toLowerCase() === subject.toLowerCase())
|
|
68
|
+
continue;
|
|
69
|
+
const base = { quoteToken: q.address, quoteSymbol: q.symbol, quoteDecimals: q.decimals };
|
|
70
|
+
for (const v of set.venues) {
|
|
71
|
+
const fields = { venue: v.id, protocol: v.protocol, factory: v.factory, ...base };
|
|
72
|
+
if (v.protocol === 'uniswap-v2')
|
|
73
|
+
entries.push({ ...fields, feeTier: null, stable: null, callData: encodeFunctionData({ abi: FACTORY_ABI, functionName: 'getPair', args: [subject, q.address] }) });
|
|
74
|
+
else if (v.protocol === 'uniswap-v3')
|
|
75
|
+
for (const fee of v.feeTiers ?? [])
|
|
76
|
+
entries.push({ ...fields, feeTier: fee, stable: null, callData: encodeFunctionData({ abi: FACTORY_ABI, functionName: 'getPool', args: [subject, q.address, fee] }) });
|
|
77
|
+
else
|
|
78
|
+
for (const stable of v.stableFlags ?? [])
|
|
79
|
+
entries.push({ ...fields, feeTier: null, stable, callData: encodeFunctionData({ abi: AERODROME_ABI, functionName: 'getPool', args: [subject, q.address, stable] }) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
/** Multicall3 aggregate3 with allowFailure on every subcall: one failed lookup is recorded, never hidden, and never fails the others. */
|
|
85
|
+
export function encodeAggregate(calls) {
|
|
86
|
+
return encodeFunctionData({ abi: MULTICALL_ABI, functionName: 'aggregate3', args: [calls.map((call) => ({ target: getAddress(call.target), allowFailure: true, callData: call.callData }))] });
|
|
87
|
+
}
|
|
88
|
+
/** The discovery multicall: each lookup targets its venue's factory. */
|
|
89
|
+
export const encodeDiscovery = (plan) => encodeAggregate(plan.map((entry) => ({ target: entry.factory, callData: entry.callData })));
|
|
90
|
+
/** The eth_call object for a market multicall. Key order is fixed so checkpoint requests hash identically. */
|
|
91
|
+
export const marketCall = (data) => ({ to: MULTICALL3_ADDRESS, data, gas: MARKET_CALL_GAS });
|
|
92
|
+
export function decodeAggregate(result, expected) {
|
|
93
|
+
if (typeof result !== 'string' || !/^0x(?:[0-9a-fA-F]{2})*$/.test(result) || result === '0x')
|
|
94
|
+
throw new MarketDecodeError('The multicall returned no data.');
|
|
95
|
+
let decoded;
|
|
96
|
+
try {
|
|
97
|
+
decoded = decodeFunctionResult({ abi: MULTICALL_ABI, functionName: 'aggregate3', data: result });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new MarketDecodeError('The multicall response is not an aggregate3 result.');
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(decoded) || decoded.length !== expected)
|
|
103
|
+
throw new MarketDecodeError('The multicall response does not match the request.');
|
|
104
|
+
return decoded.map((item) => ({ success: item.success === true, returnData: item.returnData }));
|
|
105
|
+
}
|
|
106
|
+
const word = (data, index) => data.length >= 2 + 64 * (index + 1) ? `0x${data.slice(2 + 64 * index, 2 + 64 * (index + 1))}` : null;
|
|
107
|
+
const addressWord = (data) => { const first = data.length === 66 ? word(data, 0) : null; return first && /^0x0{24}[0-9a-fA-F]{40}$/.test(first) ? getAddress(`0x${first.slice(26)}`) : null; };
|
|
108
|
+
const uintWord = (data, index = 0) => { const value = word(data, index); return value ? BigInt(value).toString() : null; };
|
|
109
|
+
export function poolsFromDiscovery(plan, results) {
|
|
110
|
+
const checked = [];
|
|
111
|
+
const found = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
plan.forEach(({ callData: _callData, ...fields }, index) => {
|
|
114
|
+
const result = results[index];
|
|
115
|
+
const pool = result.success ? addressWord(result.returnData) : null;
|
|
116
|
+
const lookup = !result.success || (pool === null && result.returnData !== '0x' && result.returnData.length !== 66) ? 'call-failed' : pool === null || pool === ZERO_ADDRESS ? 'no-pool' : 'pool';
|
|
117
|
+
checked.push({ ...fields, lookup: result.success && result.returnData === '0x' ? 'call-failed' : lookup, pool: lookup === 'pool' ? pool : null });
|
|
118
|
+
if (lookup === 'pool' && !seen.has(pool.toLowerCase())) {
|
|
119
|
+
seen.add(pool.toLowerCase());
|
|
120
|
+
found.push({ ...fields, address: pool });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return { checked, found };
|
|
124
|
+
}
|
|
125
|
+
/** The exact indexed request: newest ERC-20 transfers of this token at or before the pinned block, zero values excluded. */
|
|
126
|
+
export function holderTransferRequest(token, snapshotBlock) {
|
|
127
|
+
return { fromBlock: '0x0', toBlock: `0x${BigInt(snapshotBlock).toString(16)}`, contractAddresses: [token.toLowerCase()], category: ['erc20'], order: 'desc', maxCount: `0x${MAXIMUM_RECENT_TRANSFERS.toString(16)}`, excludeZeroValue: true, withMetadata: false };
|
|
128
|
+
}
|
|
129
|
+
export function recipientsFromTransfers(result, token, snapshotBlock) {
|
|
130
|
+
if (!object(result) || !Array.isArray(result.transfers) || result.transfers.length > MAXIMUM_RECENT_TRANSFERS)
|
|
131
|
+
throw new MarketDecodeError('The transfer index response is outside the requested page.');
|
|
132
|
+
const recipients = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
let capped = false;
|
|
135
|
+
for (const transfer of result.transfers) {
|
|
136
|
+
if (!object(transfer) || typeof transfer.blockNum !== 'string' || !/^0x[0-9a-fA-F]{1,16}$/.test(transfer.blockNum))
|
|
137
|
+
throw new MarketDecodeError('A transfer has no block number.');
|
|
138
|
+
const block = BigInt(transfer.blockNum);
|
|
139
|
+
if (block > BigInt(snapshotBlock))
|
|
140
|
+
throw new MarketDecodeError('The index returned a transfer after the pinned block.');
|
|
141
|
+
const contract = object(transfer.rawContract) ? transfer.rawContract.address : undefined;
|
|
142
|
+
if (typeof contract !== 'string' || contract.toLowerCase() !== token.toLowerCase())
|
|
143
|
+
throw new MarketDecodeError('The index returned a transfer of another token.');
|
|
144
|
+
if (typeof transfer.to !== 'string' || !isAddress(transfer.to, { strict: false }))
|
|
145
|
+
continue;
|
|
146
|
+
const to = getAddress(transfer.to);
|
|
147
|
+
const key = to.toLowerCase();
|
|
148
|
+
if (to === ZERO_ADDRESS || seen.has(key))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(key);
|
|
151
|
+
if (recipients.length >= MAXIMUM_RECENT_RECIPIENTS) {
|
|
152
|
+
capped = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
recipients.push({ address: to, latestReceiptBlock: block.toString() });
|
|
156
|
+
}
|
|
157
|
+
return { examined: result.transfers.length, truncated: capped || (typeof result.pageKey === 'string' && result.pageKey.length > 0), recipients };
|
|
158
|
+
}
|
|
159
|
+
const balanceOf = (account) => encodeFunctionData({ abi: BALANCE_ABI, functionName: 'balanceOf', args: [getAddress(account)] });
|
|
160
|
+
export function balancesPlan(token, pools, recipients) {
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const pool of pools.slice(0, MAXIMUM_MARKET_POOLS)) {
|
|
163
|
+
entries.push({ kind: 'pool-token', subject: pool.address, target: getAddress(token), callData: balanceOf(pool.address) }, { kind: 'pool-quote', subject: pool.address, target: pool.quoteToken, callData: balanceOf(pool.address) });
|
|
164
|
+
if (pool.protocol === 'uniswap-v3')
|
|
165
|
+
entries.push({ kind: 'pool-slot0', subject: pool.address, target: pool.address, callData: MARKET_SELECTORS.slot0 }, { kind: 'pool-liquidity', subject: pool.address, target: pool.address, callData: MARKET_SELECTORS.liquidity });
|
|
166
|
+
else
|
|
167
|
+
entries.push({ kind: 'pool-reserves', subject: pool.address, target: pool.address, callData: MARKET_SELECTORS.getReserves });
|
|
168
|
+
}
|
|
169
|
+
for (const recipient of recipients.slice(0, MAXIMUM_RECENT_RECIPIENTS))
|
|
170
|
+
entries.push({ kind: 'recipient', subject: recipient.address, target: getAddress(token), callData: balanceOf(recipient.address) });
|
|
171
|
+
return entries;
|
|
172
|
+
}
|
|
173
|
+
const REASONS = {
|
|
174
|
+
poolsSkipped: 'Pool discovery is switched off by this version\'s market policy; pools are not inspected.',
|
|
175
|
+
holdersSkipped: 'Recent holder activity is switched off by this version\'s market policy.',
|
|
176
|
+
notRead: 'Not read: the run stopped before this read.',
|
|
177
|
+
poolsError: 'The pool discovery multicall failed; pool presence on the listed venues is unknown.',
|
|
178
|
+
holdersError: 'The transfer index request failed; recent recipients are unknown.',
|
|
179
|
+
noVenues: 'No verified DEX venue is listed for this chain, so pools were not looked up.',
|
|
180
|
+
poolsNone: 'No pool exists at the pinned block for the listed venue and quote-token pairs. Pools on other venues, with other quote tokens or on Uniswap V4 are not checked, so this is not proof of no liquidity.',
|
|
181
|
+
poolsObserved: 'Pools found by factory lookup on the listed venues at the pinned block, with token balances held by each pool. This is not every pool, not total liquidity and not a price or depth quote.',
|
|
182
|
+
holdersNone: 'The index returned no non-zero transfer to a recipient at or before the pinned block.',
|
|
183
|
+
holdersObserved: 'Current balances (at the pinned block) of accounts that received this token in its most recent indexed transfers. This is recent activity, not a holder list, ranking or distribution; an account can be a pool, exchange or contract.',
|
|
184
|
+
};
|
|
185
|
+
const multicallData = (item) => item.source.method === 'eth_call' && object(item.request[0]) && typeof item.request[0].to === 'string' && item.request[0].to.toLowerCase() === MULTICALL3_ADDRESS.toLowerCase() && typeof item.request[0].data === 'string' ? item.request[0].data : null;
|
|
186
|
+
/**
|
|
187
|
+
* Derives the whole market section from captured receipts. The executor and every verifier call this same function, so a report's
|
|
188
|
+
* market section is valid only when it equals the derivation from its own evidence. `unsupported` carries server-side availability
|
|
189
|
+
* decisions (coverage or credential), which are not derivable from receipts.
|
|
190
|
+
*/
|
|
191
|
+
export function buildMarketSection(args) {
|
|
192
|
+
const { policy, chainId, token, snapshotBlock, evidence } = args;
|
|
193
|
+
const set = marketVenuesFor(chainId);
|
|
194
|
+
const pools = { status: 'skipped', reason: REASONS.poolsSkipped, venuesChecked: [], pools: [], poolsFound: 0, truncated: false, evidenceIds: [] };
|
|
195
|
+
const holders = { status: 'skipped', reason: REASONS.holdersSkipped, basis: 'recent-transfer-recipients-not-a-holder-list', transfersExamined: 0, truncated: false, accounts: [], evidenceIds: [] };
|
|
196
|
+
let found = [];
|
|
197
|
+
let recipients = [];
|
|
198
|
+
if (policy.discoverPools) {
|
|
199
|
+
if (args.unsupported.pools)
|
|
200
|
+
Object.assign(pools, { status: 'unsupported', reason: args.unsupported.pools.slice(0, 1000) });
|
|
201
|
+
else if (!set)
|
|
202
|
+
Object.assign(pools, { status: 'unsupported', reason: REASONS.noVenues });
|
|
203
|
+
else if (snapshotBlock === null)
|
|
204
|
+
Object.assign(pools, { status: 'not-read', reason: REASONS.notRead });
|
|
205
|
+
else {
|
|
206
|
+
const plan = discoveryPlan(set, token);
|
|
207
|
+
const data = encodeDiscovery(plan);
|
|
208
|
+
const receipts = evidence.filter((item) => multicallData(item) === data);
|
|
209
|
+
const ok = receipts.find((item) => item.completeness === 'complete-for-request');
|
|
210
|
+
if (ok) {
|
|
211
|
+
try {
|
|
212
|
+
const decoded = poolsFromDiscovery(plan, decodeAggregate(ok.result, plan.length));
|
|
213
|
+
found = decoded.found;
|
|
214
|
+
Object.assign(pools, { status: found.length ? 'observed' : 'none-found', reason: found.length ? REASONS.poolsObserved : REASONS.poolsNone, venuesChecked: decoded.checked, poolsFound: found.length, truncated: found.length > MAXIMUM_MARKET_POOLS, evidenceIds: [ok.id] });
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
Object.assign(pools, { status: 'error', reason: REASONS.poolsError, evidenceIds: [ok.id] });
|
|
218
|
+
found = [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (receipts.length)
|
|
222
|
+
Object.assign(pools, { status: 'error', reason: REASONS.poolsError, evidenceIds: receipts.map((item) => item.id).slice(0, 1) });
|
|
223
|
+
else
|
|
224
|
+
Object.assign(pools, { status: 'not-read', reason: REASONS.notRead });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (policy.recentHolderActivity) {
|
|
228
|
+
if (args.unsupported.holders)
|
|
229
|
+
Object.assign(holders, { status: 'unsupported', reason: args.unsupported.holders.slice(0, 1000) });
|
|
230
|
+
else if (snapshotBlock === null)
|
|
231
|
+
Object.assign(holders, { status: 'not-read', reason: REASONS.notRead });
|
|
232
|
+
else {
|
|
233
|
+
const request = contentHash(holderTransferRequest(token, snapshotBlock));
|
|
234
|
+
const receipts = evidence.filter((item) => item.source.method === HOLDER_ACTIVITY_METHOD && item.request.length === 1 && contentHash(item.request[0]) === request);
|
|
235
|
+
const ok = receipts.find((item) => item.completeness === 'complete-for-request');
|
|
236
|
+
if (ok) {
|
|
237
|
+
try {
|
|
238
|
+
const page = recipientsFromTransfers(ok.result, token, snapshotBlock);
|
|
239
|
+
recipients = page.recipients;
|
|
240
|
+
Object.assign(holders, { status: recipients.length ? 'observed' : 'none-found', reason: recipients.length ? REASONS.holdersObserved : REASONS.holdersNone, transfersExamined: page.examined, truncated: page.truncated, evidenceIds: [ok.id] });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
Object.assign(holders, { status: 'error', reason: REASONS.holdersError, evidenceIds: [ok.id] });
|
|
244
|
+
recipients = [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (receipts.length)
|
|
248
|
+
Object.assign(holders, { status: 'error', reason: REASONS.holdersError, evidenceIds: receipts.map((item) => item.id).slice(0, 1) });
|
|
249
|
+
else
|
|
250
|
+
Object.assign(holders, { status: 'not-read', reason: REASONS.notRead });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const shownPools = found.slice(0, MAXIMUM_MARKET_POOLS);
|
|
254
|
+
const plan = shownPools.length || recipients.length ? balancesPlan(token, shownPools, recipients) : [];
|
|
255
|
+
const balanceReceipt = plan.length ? (() => { const data = encodeAggregate(plan); const receipts = evidence.filter((item) => multicallData(item) === data); return receipts.find((item) => item.completeness === 'complete-for-request') ?? receipts[0] ?? null; })() : null;
|
|
256
|
+
let results = null;
|
|
257
|
+
if (balanceReceipt?.completeness === 'complete-for-request') {
|
|
258
|
+
try {
|
|
259
|
+
results = decodeAggregate(balanceReceipt.result, plan.length);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
results = null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const value = (kind, subject) => {
|
|
266
|
+
const index = plan.findIndex((entry) => entry.kind === kind && entry.subject === subject);
|
|
267
|
+
const result = index >= 0 && results ? results[index] : undefined;
|
|
268
|
+
return result?.success ? result.returnData : null;
|
|
269
|
+
};
|
|
270
|
+
const allFound = new Set(found.map((pool) => pool.address.toLowerCase()));
|
|
271
|
+
pools.pools = shownPools.map((pool) => {
|
|
272
|
+
const token0 = pool.quoteToken.toLowerCase() < token.toLowerCase() ? pool.quoteToken : getAddress(token);
|
|
273
|
+
const reserves = value('pool-reserves', pool.address);
|
|
274
|
+
const slot0 = value('pool-slot0', pool.address);
|
|
275
|
+
const liquidity = value('pool-liquidity', pool.address);
|
|
276
|
+
const tokenBalance = value('pool-token', pool.address);
|
|
277
|
+
const quoteBalance = value('pool-quote', pool.address);
|
|
278
|
+
const tickWord = slot0 ? word(slot0, 1) : null;
|
|
279
|
+
const { address, ...fields } = pool;
|
|
280
|
+
return { ...fields, address, token0, tokenBalanceAtomic: tokenBalance && tokenBalance.length === 66 ? uintWord(tokenBalance) : null, quoteBalanceAtomic: quoteBalance && quoteBalance.length === 66 ? uintWord(quoteBalance) : null,
|
|
281
|
+
reserve0Atomic: reserves ? uintWord(reserves, 0) : null, reserve1Atomic: reserves ? uintWord(reserves, 1) : null,
|
|
282
|
+
sqrtPriceX96: slot0 && word(slot0, 0) ? decodeAbiParameters([{ type: 'uint160' }], word(slot0, 0))[0].toString() : null,
|
|
283
|
+
tick: tickWord ? Number(decodeAbiParameters([{ type: 'int24' }], tickWord)[0]) : null,
|
|
284
|
+
activeLiquidity: liquidity && liquidity.length === 66 ? uintWord(liquidity) : null,
|
|
285
|
+
evidenceIds: [...pools.evidenceIds, ...(balanceReceipt ? [balanceReceipt.id] : [])] };
|
|
286
|
+
});
|
|
287
|
+
holders.accounts = recipients.map((recipient) => {
|
|
288
|
+
const balance = value('recipient', recipient.address);
|
|
289
|
+
return { address: recipient.address, latestReceiptBlock: recipient.latestReceiptBlock, balanceAtomic: balance && balance.length === 66 ? uintWord(balance) : null, isDiscoveredPool: allFound.has(recipient.address.toLowerCase()),
|
|
290
|
+
evidenceIds: [...holders.evidenceIds, ...(balanceReceipt ? [balanceReceipt.id] : [])] };
|
|
291
|
+
});
|
|
292
|
+
return { pools, recentHolderActivity: holders, limitations: marketLimitations(chainId, policy) };
|
|
293
|
+
}
|
|
294
|
+
/** True when the balances multicall for a derived plan is still unread (the executor reads it once). */
|
|
295
|
+
export const marketBalancesData = (token, pools, recipients) => { const plan = balancesPlan(token, pools, recipients); return plan.length ? { plan, data: encodeAggregate(plan) } : null; };
|
|
296
|
+
export function marketLimitations(chainId, policy) {
|
|
297
|
+
const set = marketVenuesFor(chainId);
|
|
298
|
+
const lines = [];
|
|
299
|
+
if (policy.discoverPools) {
|
|
300
|
+
lines.push(set ? `Pools are found only by factory lookups on ${set.venues.map((item) => item.label).join(', ')} against ${set.quotes.map((item) => item.symbol).join(', ')} at the pinned block. Other venues, other quote tokens, Uniswap V4 and aggregators are not checked, and no total liquidity is claimed.` : 'No verified DEX venue is listed for this chain; pools are not inspected.');
|
|
301
|
+
lines.push('Pool token balances are balanceOf values at the pinned block. For concentrated-liquidity pools they include out-of-range liquidity and uncollected fees; they are not executable depth, a price or a valuation.');
|
|
302
|
+
}
|
|
303
|
+
if (policy.recentHolderActivity)
|
|
304
|
+
lines.push('Recent recipients come from one provider index page of the newest transfers at or before the pinned block; completeness is the index\'s claim. They are not a holder list, ranking or distribution, and no concentration figure is computed.');
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
/** Checkpoint scope: a market eth_call is exactly the discovery lookup for this token, or a bounded balances/pool-state read. */
|
|
308
|
+
export function marketCallInScope(call, token, chainId) {
|
|
309
|
+
if (!object(call) || Object.keys(call).length !== 3 || typeof call.to !== 'string' || call.to.toLowerCase() !== MULTICALL3_ADDRESS.toLowerCase() || call.gas !== MARKET_CALL_GAS || typeof call.data !== 'string')
|
|
310
|
+
return false;
|
|
311
|
+
const set = marketVenuesFor(chainId);
|
|
312
|
+
if (!set)
|
|
313
|
+
return false;
|
|
314
|
+
if (call.data === encodeDiscovery(discoveryPlan(set, token)))
|
|
315
|
+
return true;
|
|
316
|
+
let calls;
|
|
317
|
+
try {
|
|
318
|
+
const decoded = decodeFunctionData({ abi: MULTICALL_ABI, data: call.data });
|
|
319
|
+
if (decoded.functionName !== 'aggregate3')
|
|
320
|
+
return false;
|
|
321
|
+
calls = decoded.args[0];
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (!calls.length || calls.length > MAXIMUM_MARKET_POOLS * 4 + MAXIMUM_RECENT_RECIPIENTS)
|
|
327
|
+
return false;
|
|
328
|
+
const tokens = new Set([token, ...set.quotes.map((item) => item.address)].map((value) => value.toLowerCase()));
|
|
329
|
+
const poolState = new Set([MARKET_SELECTORS.getReserves, MARKET_SELECTORS.slot0, MARKET_SELECTORS.liquidity]);
|
|
330
|
+
return calls.every((item) => item.allowFailure === true && ((tokens.has(item.target.toLowerCase()) && item.callData.length === 74 && item.callData.toLowerCase().startsWith(MARKET_SELECTORS.balanceOf)) || poolState.has(item.callData.toLowerCase())));
|
|
331
|
+
}
|
|
332
|
+
/** Checkpoint scope for the one indexed transfer page. */
|
|
333
|
+
export function marketTransferRequestInScope(request, token, snapshotBlock) {
|
|
334
|
+
return request.length === 1 && contentHash(request[0]) === contentHash(holderTransferRequest(token, snapshotBlock));
|
|
335
|
+
}
|