voltaire-effect 0.3.0 → 1.0.1
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/dist/{X25519Test-D5Q-5fL9.d.ts → X25519Test-avt1DUgp.d.ts} +231 -6
- package/dist/crypto/index.d.ts +2 -2
- package/dist/crypto/index.js +72 -2
- package/dist/{index-3UKSP3cd.d.ts → index-DxwZo3xo.d.ts} +7781 -5513
- package/dist/index.d.ts +990 -3096
- package/dist/index.js +2374 -1652
- package/dist/native/index.d.ts +6 -6
- package/dist/native/index.js +2399 -1677
- package/dist/primitives/index.d.ts +7 -6
- package/dist/primitives/index.js +2966 -2361
- package/dist/services/index.d.ts +1631 -1255
- package/dist/services/index.js +4493 -3977
- package/package.json +7 -3
- package/src/crypto/Signers/SignersService.ts +1 -1
- package/src/crypto/Signers/errors.ts +29 -0
- package/src/crypto/Signers/index.ts +1 -0
- package/src/crypto/Signers/operations.ts +26 -8
- package/src/crypto/index.ts +10 -11
- package/src/index.ts +1 -2
- package/src/jsonrpc/Anvil.ts +13 -8
- package/src/jsonrpc/Eth.ts +13 -8
- package/src/jsonrpc/Hardhat.ts +13 -8
- package/src/jsonrpc/IdCounter.ts +21 -5
- package/src/jsonrpc/JsonRpc.test.ts +126 -61
- package/src/jsonrpc/Net.ts +13 -8
- package/src/jsonrpc/Request.ts +16 -8
- package/src/jsonrpc/Txpool.ts +13 -8
- package/src/jsonrpc/Wallet.ts +13 -8
- package/src/jsonrpc/Web3.ts +13 -8
- package/src/jsonrpc/index.ts +1 -1
- package/src/primitives/Abi/AbiSchema.ts +3 -4
- package/src/primitives/Abi/fromBytecode.test.ts +47 -0
- package/src/primitives/Abi/fromBytecode.ts +81 -0
- package/src/primitives/Abi/index.ts +3 -0
- package/src/primitives/AccessList/from.ts +12 -9
- package/src/primitives/Address/Checksummed.ts +21 -27
- package/src/primitives/Address/from.ts +12 -15
- package/src/primitives/Address/toHex.ts +2 -1
- package/src/primitives/Base64/from.ts +21 -4
- package/src/primitives/Blob/from.ts +12 -4
- package/src/primitives/BlockHash/index.ts +2 -2
- package/src/primitives/BlockNumber/index.ts +3 -3
- package/src/primitives/Bytecode/from.ts +11 -2
- package/src/primitives/ContractSignature/verifySignature.ts +3 -5
- package/src/primitives/Ens/from.ts +12 -11
- package/src/primitives/Hex/from.ts +12 -4
- package/src/primitives/Signature/from.ts +11 -2
- package/src/primitives/Transaction/EIP2930/index.ts +12 -12
- package/src/primitives/Transaction/EIP4844/index.ts +14 -14
- package/src/primitives/Transaction/EIP7702/index.ts +13 -13
- package/src/primitives/Transaction/Legacy/index.ts +13 -13
- package/src/primitives/TransactionHash/index.ts +3 -2
- package/src/primitives/TransactionIndex/index.ts +2 -2
- package/src/primitives/Trie/Trie.test.ts +70 -0
- package/src/primitives/Trie/TrieSchema.ts +26 -0
- package/src/primitives/Trie/clear.ts +16 -0
- package/src/primitives/Trie/del.ts +18 -0
- package/src/primitives/Trie/get.ts +18 -0
- package/src/primitives/Trie/index.ts +30 -0
- package/src/primitives/Trie/init.ts +13 -0
- package/src/primitives/Trie/prove.ts +19 -0
- package/src/primitives/Trie/put.ts +20 -0
- package/src/primitives/Trie/rootHash.ts +14 -0
- package/src/primitives/Trie/verify.ts +18 -0
- package/src/primitives/Uint/from.ts +11 -2
- package/src/primitives/Uint16/index.ts +5 -4
- package/src/primitives/Uint64/index.ts +5 -4
- package/src/primitives/Uint8/index.ts +5 -4
- package/src/primitives/index.ts +3 -2
- package/src/services/BlockExplorerApi/BlockExplorerApi.test.ts +789 -0
- package/src/services/BlockExplorerApi/BlockExplorerApi.ts +797 -0
- package/src/services/BlockExplorerApi/BlockExplorerApiErrors.ts +176 -0
- package/src/services/BlockExplorerApi/BlockExplorerApiService.ts +60 -0
- package/src/services/BlockExplorerApi/BlockExplorerApiTypes.ts +225 -0
- package/src/services/BlockExplorerApi/index.ts +42 -0
- package/src/services/Contract/Contract.test.ts +2 -6
- package/src/services/Contract/ContractTypes.ts +26 -8
- package/src/services/Contract/estimateGas.test.ts +4 -7
- package/src/services/Provider/actions/multicall.ts +28 -9
- package/src/services/Provider/actions/readContract.test.ts +8 -11
- package/src/services/Provider/actions/readContract.ts +28 -9
- package/src/services/Provider/functions/getBlock.ts +2 -1
- package/src/services/Provider/functions/getBlockReceipts.ts +2 -1
- package/src/services/Provider/functions/getBlockTransactionCount.ts +2 -1
- package/src/services/Provider/functions/getUncle.ts +2 -1
- package/src/services/Provider/functions/getUncleCount.ts +2 -1
- package/src/services/Signer/actions/deployContract.ts +1 -1
- package/src/services/index.ts +25 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Block Explorer API layer factory.
|
|
3
|
+
*
|
|
4
|
+
* @module BlockExplorerApi
|
|
5
|
+
* @since 0.0.1
|
|
6
|
+
*
|
|
7
|
+
* @description
|
|
8
|
+
* Factory functions for creating BlockExplorerApiService layers.
|
|
9
|
+
* Supports explicit configuration and environment-based configuration.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Address,
|
|
14
|
+
BrandedAbi,
|
|
15
|
+
type BrandedHex,
|
|
16
|
+
Hex,
|
|
17
|
+
} from "@tevm/voltaire";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Layer from "effect/Layer";
|
|
20
|
+
import * as Runtime from "effect/Runtime";
|
|
21
|
+
import { Redacted } from "effect";
|
|
22
|
+
import { ContractCallError, ContractWriteError } from "../Contract/ContractTypes.js";
|
|
23
|
+
import { ProviderService } from "../Provider/ProviderService.js";
|
|
24
|
+
import { SignerService } from "../Signer/SignerService.js";
|
|
25
|
+
import { getCode, call, getStorageAt } from "../Provider/functions/index.js";
|
|
26
|
+
|
|
27
|
+
type HexType = BrandedHex.HexType;
|
|
28
|
+
import {
|
|
29
|
+
BlockExplorerConfigError,
|
|
30
|
+
BlockExplorerNotFoundError,
|
|
31
|
+
BlockExplorerRateLimitError,
|
|
32
|
+
BlockExplorerResponseError,
|
|
33
|
+
BlockExplorerDecodeError,
|
|
34
|
+
BlockExplorerUnexpectedError,
|
|
35
|
+
BlockExplorerProxyResolutionError,
|
|
36
|
+
} from "./BlockExplorerApiErrors.js";
|
|
37
|
+
import { BlockExplorerApiService, type BlockExplorerApiShape } from "./BlockExplorerApiService.js";
|
|
38
|
+
import type {
|
|
39
|
+
AbiItem,
|
|
40
|
+
BlockExplorerApiConfig,
|
|
41
|
+
ContractSourceFile,
|
|
42
|
+
ExplorerContractInstance,
|
|
43
|
+
ExplorerSourceId,
|
|
44
|
+
GetAbiOptions,
|
|
45
|
+
GetContractOptions,
|
|
46
|
+
GetSourcesOptions,
|
|
47
|
+
} from "./BlockExplorerApiTypes.js";
|
|
48
|
+
import type { BlockExplorerApiError } from "./BlockExplorerApiErrors.js";
|
|
49
|
+
import { ChainService } from "../Chain/ChainService.js";
|
|
50
|
+
|
|
51
|
+
// WhatsAbi imports
|
|
52
|
+
import { loaders, autoload, abiFromBytecode } from "@shazow/whatsabi";
|
|
53
|
+
|
|
54
|
+
// Type for WhatsAbi ABI (ABIFunction | ABIEvent)[]
|
|
55
|
+
type WhatsAbiItem = {
|
|
56
|
+
type: "function" | "event";
|
|
57
|
+
selector?: string;
|
|
58
|
+
hash?: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
outputs?: Array<{ type: string; name: string; components?: Array<{ type: string; name: string }> }>;
|
|
61
|
+
inputs?: Array<{ type: string; name: string; components?: Array<{ type: string; name: string }> }>;
|
|
62
|
+
sig?: string;
|
|
63
|
+
sigAlts?: string[];
|
|
64
|
+
payable?: boolean;
|
|
65
|
+
stateMutability?: "nonpayable" | "payable" | "view" | "pure";
|
|
66
|
+
};
|
|
67
|
+
type WhatsABI = WhatsAbiItem[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default source order for resolution.
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_SOURCE_ORDER: ReadonlyArray<ExplorerSourceId> = [
|
|
73
|
+
"sourcify",
|
|
74
|
+
"etherscanV2",
|
|
75
|
+
"blockscout",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalize ABI from WhatsAbi format to our standard format.
|
|
80
|
+
*/
|
|
81
|
+
function normalizeAbi(rawAbi: WhatsABI): ReadonlyArray<AbiItem> {
|
|
82
|
+
return rawAbi.map((item: WhatsAbiItem): AbiItem | null => {
|
|
83
|
+
if (item.type === "function") {
|
|
84
|
+
return {
|
|
85
|
+
type: "function",
|
|
86
|
+
name: item.name,
|
|
87
|
+
inputs: item.inputs?.map((i: { type: string; name: string; components?: Array<{ type: string; name: string }> }) => ({
|
|
88
|
+
name: i.name,
|
|
89
|
+
type: i.type,
|
|
90
|
+
components: i.components as AbiItem["inputs"],
|
|
91
|
+
})),
|
|
92
|
+
outputs: item.outputs?.map((o: { type: string; name: string; components?: Array<{ type: string; name: string }> }) => ({
|
|
93
|
+
name: o.name,
|
|
94
|
+
type: o.type,
|
|
95
|
+
components: o.components as AbiItem["outputs"],
|
|
96
|
+
})),
|
|
97
|
+
stateMutability: item.stateMutability,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (item.type === "event") {
|
|
101
|
+
return {
|
|
102
|
+
type: "event",
|
|
103
|
+
name: item.name,
|
|
104
|
+
anonymous: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}).filter((item: AbiItem | null): item is AbiItem => item !== null);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detect rate limit from response.
|
|
113
|
+
*/
|
|
114
|
+
function isRateLimitResponse(body: string): boolean {
|
|
115
|
+
const lower = body.toLowerCase();
|
|
116
|
+
return (
|
|
117
|
+
lower.includes("rate limit") ||
|
|
118
|
+
lower.includes("too many requests") ||
|
|
119
|
+
lower.includes("max rate limit reached")
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse retry-after from rate limit response.
|
|
125
|
+
*/
|
|
126
|
+
function parseRetryAfter(body: string): number | undefined {
|
|
127
|
+
const match = body.match(/(\d+)\s*seconds?/i);
|
|
128
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get API key as string from config (handles Redacted).
|
|
133
|
+
*/
|
|
134
|
+
function getApiKey(
|
|
135
|
+
key: Redacted.Redacted<string> | string | undefined,
|
|
136
|
+
): string | undefined {
|
|
137
|
+
if (key === undefined) return undefined;
|
|
138
|
+
if (typeof key === "string") return key;
|
|
139
|
+
return Redacted.value(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create loaders based on configuration.
|
|
144
|
+
*/
|
|
145
|
+
function createLoaders(
|
|
146
|
+
config: BlockExplorerApiConfig,
|
|
147
|
+
chainId: number,
|
|
148
|
+
): loaders.ABILoader[] {
|
|
149
|
+
const result: loaders.ABILoader[] = [];
|
|
150
|
+
const sourceOrder = config.sourceOrder ?? DEFAULT_SOURCE_ORDER;
|
|
151
|
+
|
|
152
|
+
for (const source of sourceOrder) {
|
|
153
|
+
const sourceConfig = config.sources[source];
|
|
154
|
+
if (!sourceConfig?.enabled) continue;
|
|
155
|
+
|
|
156
|
+
switch (source) {
|
|
157
|
+
case "sourcify": {
|
|
158
|
+
result.push(
|
|
159
|
+
new loaders.SourcifyABILoader({
|
|
160
|
+
chainId,
|
|
161
|
+
...(sourceConfig.baseUrl && { baseURL: sourceConfig.baseUrl }),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "etherscanV2": {
|
|
167
|
+
const apiKey = getApiKey(
|
|
168
|
+
(sourceConfig as typeof config.sources.etherscanV2)?.apiKey,
|
|
169
|
+
);
|
|
170
|
+
if (apiKey) {
|
|
171
|
+
result.push(
|
|
172
|
+
new loaders.EtherscanABILoader({
|
|
173
|
+
apiKey,
|
|
174
|
+
...(sourceConfig.baseUrl && { baseURL: sourceConfig.baseUrl }),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "blockscout": {
|
|
181
|
+
const apiKey = getApiKey(
|
|
182
|
+
(sourceConfig as typeof config.sources.blockscout)?.apiKey,
|
|
183
|
+
);
|
|
184
|
+
result.push(
|
|
185
|
+
new loaders.BlockscoutABILoader({
|
|
186
|
+
...(apiKey && { apiKey }),
|
|
187
|
+
...(sourceConfig.baseUrl && { baseURL: sourceConfig.baseUrl }),
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Map WhatsAbi loader error to typed error.
|
|
200
|
+
*/
|
|
201
|
+
function mapLoaderError(
|
|
202
|
+
error: unknown,
|
|
203
|
+
source: ExplorerSourceId,
|
|
204
|
+
address: `0x${string}`,
|
|
205
|
+
): BlockExplorerApiError {
|
|
206
|
+
if (error instanceof Error) {
|
|
207
|
+
const message = error.message;
|
|
208
|
+
|
|
209
|
+
if (isRateLimitResponse(message)) {
|
|
210
|
+
return new BlockExplorerRateLimitError(
|
|
211
|
+
source,
|
|
212
|
+
address,
|
|
213
|
+
message,
|
|
214
|
+
parseRetryAfter(message),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
message.includes("not found") ||
|
|
220
|
+
message.includes("not verified") ||
|
|
221
|
+
message.includes("404")
|
|
222
|
+
) {
|
|
223
|
+
return new BlockExplorerNotFoundError(address, [source]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
message.includes("JSON") ||
|
|
228
|
+
message.includes("parse") ||
|
|
229
|
+
message.includes("decode")
|
|
230
|
+
) {
|
|
231
|
+
return new BlockExplorerDecodeError(
|
|
232
|
+
source,
|
|
233
|
+
address,
|
|
234
|
+
message,
|
|
235
|
+
message.slice(0, 200),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return new BlockExplorerResponseError(source, address, message);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new BlockExplorerUnexpectedError("getContract", String(error), error);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Determine which source returned the ABI based on the loader instance.
|
|
247
|
+
*/
|
|
248
|
+
function getSourceFromLoader(
|
|
249
|
+
loader: loaders.ABILoader,
|
|
250
|
+
): ExplorerSourceId {
|
|
251
|
+
if (loader instanceof loaders.SourcifyABILoader) return "sourcify";
|
|
252
|
+
if (loader instanceof loaders.EtherscanABILoader) return "etherscanV2";
|
|
253
|
+
if (loader instanceof loaders.BlockscoutABILoader) return "blockscout";
|
|
254
|
+
return "sourcify"; // Default fallback
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* WhatsAbi provider interface for autoload.
|
|
259
|
+
*/
|
|
260
|
+
interface WhatsAbiProvider {
|
|
261
|
+
getCode(address: string): Promise<string>;
|
|
262
|
+
getStorageAt(address: string, slot: number | string): Promise<string>;
|
|
263
|
+
call(tx: { to: string; data: string }): Promise<string>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Contract method building helpers
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
function getFunctionSignature(item: AbiItem): string {
|
|
271
|
+
const inputs = item.inputs ?? [];
|
|
272
|
+
const types = inputs.map((i) => i.type).join(",");
|
|
273
|
+
return `${item.name ?? ""}(${types})`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isViewFunction(item: AbiItem): boolean {
|
|
277
|
+
return (
|
|
278
|
+
item.type === "function" &&
|
|
279
|
+
(item.stateMutability === "view" || item.stateMutability === "pure")
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isWriteFunction(item: AbiItem): boolean {
|
|
284
|
+
return (
|
|
285
|
+
item.type === "function" &&
|
|
286
|
+
(item.stateMutability === "nonpayable" || item.stateMutability === "payable")
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function encodeArgs(
|
|
291
|
+
abi: ReadonlyArray<AbiItem>,
|
|
292
|
+
functionName: string,
|
|
293
|
+
args: ReadonlyArray<unknown>,
|
|
294
|
+
): HexType {
|
|
295
|
+
return BrandedAbi.encodeFunction(
|
|
296
|
+
abi as unknown as BrandedAbi.Abi,
|
|
297
|
+
functionName,
|
|
298
|
+
args as unknown[],
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function decodeResult(
|
|
303
|
+
abi: ReadonlyArray<AbiItem>,
|
|
304
|
+
functionName: string,
|
|
305
|
+
data: HexType,
|
|
306
|
+
): Effect.Effect<unknown, ContractCallError> {
|
|
307
|
+
return Effect.try({
|
|
308
|
+
try: () => {
|
|
309
|
+
const fn = abi.find(
|
|
310
|
+
(item): item is BrandedAbi.Function.FunctionType =>
|
|
311
|
+
item.type === "function" && (item as any).name === functionName,
|
|
312
|
+
) as BrandedAbi.Function.FunctionType | undefined;
|
|
313
|
+
if (!fn) throw new Error(`Function ${functionName} not found in ABI`);
|
|
314
|
+
|
|
315
|
+
const bytes = Hex.toBytes(data);
|
|
316
|
+
const decoded = BrandedAbi.Function.decodeResult(fn, bytes);
|
|
317
|
+
if (fn.outputs.length === 1) {
|
|
318
|
+
return decoded[0];
|
|
319
|
+
}
|
|
320
|
+
return decoded;
|
|
321
|
+
},
|
|
322
|
+
catch: (e) =>
|
|
323
|
+
new ContractCallError(
|
|
324
|
+
{ address: "", method: functionName, args: [] },
|
|
325
|
+
e instanceof Error ? e.message : "Decode error",
|
|
326
|
+
{ cause: e instanceof Error ? e : undefined },
|
|
327
|
+
),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildMethodMaps(
|
|
332
|
+
address: `0x${string}`,
|
|
333
|
+
abi: ReadonlyArray<AbiItem>,
|
|
334
|
+
): Pick<ExplorerContractInstance, "read" | "simulate" | "write" | "call"> {
|
|
335
|
+
const functions = abi.filter((item) => item.type === "function");
|
|
336
|
+
|
|
337
|
+
const byName = new Map<string, AbiItem[]>();
|
|
338
|
+
for (const fn of functions) {
|
|
339
|
+
if (!fn.name) continue;
|
|
340
|
+
const existing = byName.get(fn.name) ?? [];
|
|
341
|
+
existing.push(fn);
|
|
342
|
+
byName.set(fn.name, existing);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const bySignature = new Map<string, AbiItem>();
|
|
346
|
+
for (const fn of functions) {
|
|
347
|
+
if (!fn.name) continue;
|
|
348
|
+
const sig = getFunctionSignature(fn);
|
|
349
|
+
bySignature.set(sig, fn);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const createReadMethod = (
|
|
353
|
+
fn: AbiItem,
|
|
354
|
+
): ((...args: ReadonlyArray<unknown>) => Effect.Effect<unknown, ContractCallError, ProviderService>) => {
|
|
355
|
+
return (...args: ReadonlyArray<unknown>) =>
|
|
356
|
+
Effect.gen(function* () {
|
|
357
|
+
const data = encodeArgs(abi, fn.name!, args);
|
|
358
|
+
const result = yield* call({ to: address, data }).pipe(
|
|
359
|
+
Effect.mapError(
|
|
360
|
+
(e) =>
|
|
361
|
+
new ContractCallError(
|
|
362
|
+
{ address, method: fn.name!, args: args as unknown[] },
|
|
363
|
+
e.message,
|
|
364
|
+
{ cause: e },
|
|
365
|
+
),
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
return yield* decodeResult(abi, fn.name!, result as HexType);
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const createSimulateMethod = (
|
|
373
|
+
fn: AbiItem,
|
|
374
|
+
): ((...args: ReadonlyArray<unknown>) => Effect.Effect<unknown, ContractCallError, ProviderService>) => {
|
|
375
|
+
return (...args: ReadonlyArray<unknown>) =>
|
|
376
|
+
Effect.gen(function* () {
|
|
377
|
+
const data = encodeArgs(abi, fn.name!, args);
|
|
378
|
+
const result = yield* call({ to: address, data }).pipe(
|
|
379
|
+
Effect.mapError(
|
|
380
|
+
(e) =>
|
|
381
|
+
new ContractCallError(
|
|
382
|
+
{ address, method: fn.name!, args: args as unknown[], simulate: true },
|
|
383
|
+
e.message,
|
|
384
|
+
{ cause: e },
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
return yield* decodeResult(abi, fn.name!, result as HexType);
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const createWriteMethod = (
|
|
393
|
+
fn: AbiItem,
|
|
394
|
+
): ((...args: ReadonlyArray<unknown>) => Effect.Effect<`0x${string}`, ContractWriteError, SignerService>) => {
|
|
395
|
+
return (...args: ReadonlyArray<unknown>) =>
|
|
396
|
+
Effect.gen(function* () {
|
|
397
|
+
const signer = yield* SignerService;
|
|
398
|
+
const data = encodeArgs(abi, fn.name!, args);
|
|
399
|
+
const brandedAddress = Address.fromHex(address);
|
|
400
|
+
const txHash = yield* signer
|
|
401
|
+
.sendTransaction({
|
|
402
|
+
to: brandedAddress as unknown as undefined,
|
|
403
|
+
data: data as unknown as undefined,
|
|
404
|
+
})
|
|
405
|
+
.pipe(
|
|
406
|
+
Effect.mapError(
|
|
407
|
+
(e) =>
|
|
408
|
+
new ContractWriteError(
|
|
409
|
+
{ address, method: fn.name!, args: args as unknown[] },
|
|
410
|
+
e.message,
|
|
411
|
+
{ cause: e instanceof Error ? e : undefined },
|
|
412
|
+
),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
return txHash as unknown as `0x${string}`;
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const read: Record<
|
|
420
|
+
string,
|
|
421
|
+
(...args: ReadonlyArray<unknown>) => Effect.Effect<unknown, ContractCallError, ProviderService>
|
|
422
|
+
> = {};
|
|
423
|
+
|
|
424
|
+
for (const fn of functions) {
|
|
425
|
+
if (!fn.name || !isViewFunction(fn)) continue;
|
|
426
|
+
|
|
427
|
+
const sig = getFunctionSignature(fn);
|
|
428
|
+
const overloads = byName.get(fn.name) ?? [];
|
|
429
|
+
|
|
430
|
+
read[sig] = createReadMethod(fn);
|
|
431
|
+
|
|
432
|
+
if (overloads.length === 1) {
|
|
433
|
+
read[fn.name] = createReadMethod(fn);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const simulate: Record<
|
|
438
|
+
string,
|
|
439
|
+
(...args: ReadonlyArray<unknown>) => Effect.Effect<unknown, ContractCallError, ProviderService>
|
|
440
|
+
> = {};
|
|
441
|
+
|
|
442
|
+
for (const fn of functions) {
|
|
443
|
+
if (!fn.name || !isWriteFunction(fn)) continue;
|
|
444
|
+
|
|
445
|
+
const sig = getFunctionSignature(fn);
|
|
446
|
+
const overloads = byName.get(fn.name) ?? [];
|
|
447
|
+
|
|
448
|
+
simulate[sig] = createSimulateMethod(fn);
|
|
449
|
+
if (overloads.length === 1) {
|
|
450
|
+
simulate[fn.name] = createSimulateMethod(fn);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const write: Record<
|
|
455
|
+
string,
|
|
456
|
+
(...args: ReadonlyArray<unknown>) => Effect.Effect<`0x${string}`, ContractWriteError, SignerService>
|
|
457
|
+
> = {};
|
|
458
|
+
|
|
459
|
+
for (const fn of functions) {
|
|
460
|
+
if (!fn.name || !isWriteFunction(fn)) continue;
|
|
461
|
+
|
|
462
|
+
const sig = getFunctionSignature(fn);
|
|
463
|
+
const overloads = byName.get(fn.name) ?? [];
|
|
464
|
+
|
|
465
|
+
write[sig] = createWriteMethod(fn);
|
|
466
|
+
if (overloads.length === 1) {
|
|
467
|
+
write[fn.name] = createWriteMethod(fn);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const callFn = (
|
|
472
|
+
signature: string,
|
|
473
|
+
args: ReadonlyArray<unknown>,
|
|
474
|
+
): Effect.Effect<unknown, ContractCallError, ProviderService> => {
|
|
475
|
+
const fn = bySignature.get(signature);
|
|
476
|
+
if (!fn) {
|
|
477
|
+
return Effect.fail(
|
|
478
|
+
new ContractCallError(
|
|
479
|
+
{ address, method: signature, args: args as unknown[] },
|
|
480
|
+
`Function not found: ${signature}`,
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return createReadMethod(fn)(...args);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
return { read, simulate, write, call: callFn };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Create the BlockExplorerApiService implementation.
|
|
492
|
+
*/
|
|
493
|
+
function makeBlockExplorerApi(
|
|
494
|
+
config: BlockExplorerApiConfig,
|
|
495
|
+
): Effect.Effect<
|
|
496
|
+
BlockExplorerApiShape,
|
|
497
|
+
BlockExplorerConfigError,
|
|
498
|
+
ChainService | ProviderService
|
|
499
|
+
> {
|
|
500
|
+
return Effect.gen(function* () {
|
|
501
|
+
const chain = yield* ChainService;
|
|
502
|
+
const chainId = chain.id;
|
|
503
|
+
|
|
504
|
+
// Capture runtime for bridging Effect to Promise in proxy resolution
|
|
505
|
+
const runtime = yield* Effect.runtime<ProviderService>();
|
|
506
|
+
const runPromise = Runtime.runPromise(runtime);
|
|
507
|
+
|
|
508
|
+
const enabledSources = (
|
|
509
|
+
Object.entries(config.sources) as [ExplorerSourceId, { enabled: boolean } | undefined][]
|
|
510
|
+
).filter(([_, v]) => v?.enabled);
|
|
511
|
+
|
|
512
|
+
if (enabledSources.length === 0) {
|
|
513
|
+
return yield* Effect.fail(
|
|
514
|
+
new BlockExplorerConfigError(
|
|
515
|
+
"At least one explorer source must be enabled",
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const abiLoaders = createLoaders(config, chainId);
|
|
521
|
+
|
|
522
|
+
const cache = new Map<
|
|
523
|
+
string,
|
|
524
|
+
{ value: ExplorerContractInstance; expiry: number }
|
|
525
|
+
>();
|
|
526
|
+
const cacheEnabled = config.cache?.enabled ?? false;
|
|
527
|
+
const cacheTtl = config.cache?.ttlMillis ?? 300_000;
|
|
528
|
+
const cacheCapacity = config.cache?.capacity ?? 100;
|
|
529
|
+
|
|
530
|
+
function getCacheKey(
|
|
531
|
+
address: `0x${string}`,
|
|
532
|
+
options?: GetContractOptions,
|
|
533
|
+
): string {
|
|
534
|
+
return `${chainId}:${address.toLowerCase()}:${options?.followProxies ?? config.followProxiesByDefault ?? false}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getFromCache(
|
|
538
|
+
key: string,
|
|
539
|
+
): ExplorerContractInstance | undefined {
|
|
540
|
+
if (!cacheEnabled) return undefined;
|
|
541
|
+
const entry = cache.get(key);
|
|
542
|
+
if (!entry) return undefined;
|
|
543
|
+
if (Date.now() > entry.expiry) {
|
|
544
|
+
cache.delete(key);
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
return entry.value;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function setCache(key: string, value: ExplorerContractInstance): void {
|
|
551
|
+
if (!cacheEnabled) return;
|
|
552
|
+
if (cache.size >= cacheCapacity) {
|
|
553
|
+
const oldest = cache.keys().next().value;
|
|
554
|
+
if (oldest) cache.delete(oldest);
|
|
555
|
+
}
|
|
556
|
+
cache.set(key, { value, expiry: Date.now() + cacheTtl });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const getContract = (
|
|
560
|
+
address: `0x${string}`,
|
|
561
|
+
options?: GetContractOptions,
|
|
562
|
+
): Effect.Effect<ExplorerContractInstance, BlockExplorerApiError> =>
|
|
563
|
+
Effect.gen(function* () {
|
|
564
|
+
const cacheKey = getCacheKey(address, options);
|
|
565
|
+
const cached = getFromCache(cacheKey);
|
|
566
|
+
if (cached) return cached;
|
|
567
|
+
|
|
568
|
+
const resolution = options?.resolution ?? "verified-first";
|
|
569
|
+
const includeSources = options?.includeSources ?? false;
|
|
570
|
+
const shouldFollowProxies = options?.followProxies ?? config.followProxiesByDefault ?? false;
|
|
571
|
+
|
|
572
|
+
// Create WhatsAbi-compatible provider using our Effect-based provider
|
|
573
|
+
const whatsabiProvider: WhatsAbiProvider = {
|
|
574
|
+
getCode: (addr: string) =>
|
|
575
|
+
runPromise(getCode(addr as `0x${string}`)),
|
|
576
|
+
getStorageAt: (addr: string, slot: number | string) => {
|
|
577
|
+
const slotHex = typeof slot === "number"
|
|
578
|
+
? `0x${slot.toString(16)}` as `0x${string}`
|
|
579
|
+
: slot.startsWith("0x") ? slot as `0x${string}` : `0x${slot}` as `0x${string}`;
|
|
580
|
+
return runPromise(getStorageAt(addr as `0x${string}`, slotHex));
|
|
581
|
+
},
|
|
582
|
+
call: (tx: { to: string; data: string }) =>
|
|
583
|
+
runPromise(call({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}` })),
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Create combined loader from our configured sources
|
|
587
|
+
const combinedLoader = abiLoaders.length > 0
|
|
588
|
+
? new loaders.MultiABILoader(abiLoaders)
|
|
589
|
+
: false;
|
|
590
|
+
|
|
591
|
+
// Use autoload for unified ABI loading + proxy resolution
|
|
592
|
+
const result = yield* Effect.tryPromise({
|
|
593
|
+
try: () => autoload(address, {
|
|
594
|
+
provider: whatsabiProvider,
|
|
595
|
+
abiLoader: combinedLoader,
|
|
596
|
+
followProxies: shouldFollowProxies,
|
|
597
|
+
signatureLookup: false, // Don't do 4byte lookups
|
|
598
|
+
}),
|
|
599
|
+
catch: (e) => {
|
|
600
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
601
|
+
if (isRateLimitResponse(message)) {
|
|
602
|
+
return new BlockExplorerRateLimitError(
|
|
603
|
+
"etherscanV2",
|
|
604
|
+
address,
|
|
605
|
+
message,
|
|
606
|
+
parseRetryAfter(message),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (message.includes("not found") || message.includes("not verified")) {
|
|
610
|
+
return new BlockExplorerNotFoundError(address, ["autoload"]);
|
|
611
|
+
}
|
|
612
|
+
return new BlockExplorerUnexpectedError("getContract", message, e);
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Handle case where no ABI was found from verified sources
|
|
617
|
+
if (!result.abi || result.abi.length === 0) {
|
|
618
|
+
if (resolution === "best-effort" && config.enableBestEffortAbiRecovery) {
|
|
619
|
+
// Try to recover ABI from bytecode using static analysis
|
|
620
|
+
const bytecode = yield* Effect.tryPromise({
|
|
621
|
+
try: () => whatsabiProvider.getCode(result.address),
|
|
622
|
+
catch: () => new BlockExplorerUnexpectedError(
|
|
623
|
+
"getContract",
|
|
624
|
+
"Failed to fetch bytecode for best-effort recovery",
|
|
625
|
+
undefined,
|
|
626
|
+
),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
if (bytecode && bytecode !== "0x" && bytecode !== "0x0") {
|
|
630
|
+
const recoveredAbi = yield* Effect.try({
|
|
631
|
+
try: () => abiFromBytecode(bytecode),
|
|
632
|
+
catch: (e) => new BlockExplorerUnexpectedError(
|
|
633
|
+
"getContract",
|
|
634
|
+
`Failed to recover ABI from bytecode: ${e instanceof Error ? e.message : String(e)}`,
|
|
635
|
+
e,
|
|
636
|
+
),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
if (recoveredAbi && recoveredAbi.length > 0) {
|
|
640
|
+
const normalizedAbi = normalizeAbi(recoveredAbi as WhatsABI);
|
|
641
|
+
const resolvedAddr = result.address as `0x${string}`;
|
|
642
|
+
const methods = buildMethodMaps(resolvedAddr, normalizedAbi);
|
|
643
|
+
|
|
644
|
+
const contract: ExplorerContractInstance = {
|
|
645
|
+
address: resolvedAddr,
|
|
646
|
+
requestedAddress: address,
|
|
647
|
+
abi: normalizedAbi,
|
|
648
|
+
resolution: { mode: "best-effort", source: "whatsabi" },
|
|
649
|
+
...(result.proxies.length > 0 && {
|
|
650
|
+
proxies: result.proxies.map(p => ({
|
|
651
|
+
kind: p.name,
|
|
652
|
+
address: address,
|
|
653
|
+
})),
|
|
654
|
+
}),
|
|
655
|
+
...methods,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
setCache(cacheKey, contract);
|
|
659
|
+
return contract;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return yield* Effect.fail(
|
|
664
|
+
new BlockExplorerNotFoundError(address, ["autoload"]),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Extract proxy chain from result
|
|
669
|
+
const proxyChain: Array<{ kind: string; address: `0x${string}` }> = result.proxies.map(p => ({
|
|
670
|
+
kind: p.name,
|
|
671
|
+
address: address, // Original address was the proxy
|
|
672
|
+
}));
|
|
673
|
+
|
|
674
|
+
// Determine which source returned the ABI
|
|
675
|
+
const source: ExplorerSourceId = result.abiLoadedFrom
|
|
676
|
+
? getSourceFromLoader(result.abiLoadedFrom)
|
|
677
|
+
: "sourcify";
|
|
678
|
+
|
|
679
|
+
const normalizedAbi = normalizeAbi(result.abi as WhatsABI);
|
|
680
|
+
const resolvedAddr = result.address as `0x${string}`;
|
|
681
|
+
const methods = buildMethodMaps(resolvedAddr, normalizedAbi);
|
|
682
|
+
|
|
683
|
+
const contract: ExplorerContractInstance = {
|
|
684
|
+
address: resolvedAddr,
|
|
685
|
+
requestedAddress: address,
|
|
686
|
+
abi: normalizedAbi,
|
|
687
|
+
resolution: { mode: "verified", source },
|
|
688
|
+
...(proxyChain.length > 0 && { proxies: proxyChain }),
|
|
689
|
+
...(includeSources && { sources: [] as ContractSourceFile[] }),
|
|
690
|
+
...methods,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
setCache(cacheKey, contract);
|
|
694
|
+
return contract;
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const getAbi = (
|
|
698
|
+
address: `0x${string}`,
|
|
699
|
+
options?: GetAbiOptions,
|
|
700
|
+
): Effect.Effect<ReadonlyArray<AbiItem>, BlockExplorerApiError> =>
|
|
701
|
+
Effect.map(
|
|
702
|
+
getContract(address, {
|
|
703
|
+
resolution: options?.resolution,
|
|
704
|
+
followProxies: options?.followProxies,
|
|
705
|
+
includeSources: false,
|
|
706
|
+
}),
|
|
707
|
+
(contract) => contract.abi,
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const getSources = (
|
|
711
|
+
address: `0x${string}`,
|
|
712
|
+
options?: GetSourcesOptions,
|
|
713
|
+
): Effect.Effect<ReadonlyArray<ContractSourceFile>, BlockExplorerApiError> =>
|
|
714
|
+
Effect.flatMap(
|
|
715
|
+
getContract(address, {
|
|
716
|
+
includeSources: true,
|
|
717
|
+
followProxies: options?.followProxies,
|
|
718
|
+
}),
|
|
719
|
+
(contract) => {
|
|
720
|
+
if (!contract.sources || contract.sources.length === 0) {
|
|
721
|
+
return Effect.fail(
|
|
722
|
+
new BlockExplorerNotFoundError(address, ["sources"]),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return Effect.succeed(contract.sources);
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
return { getContract, getAbi, getSources };
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Create a BlockExplorerApiService layer from explicit configuration.
|
|
735
|
+
* Requires ChainService. ProviderService is required when followProxies is used.
|
|
736
|
+
* @since 0.0.1
|
|
737
|
+
*/
|
|
738
|
+
export function BlockExplorerApi(
|
|
739
|
+
config: BlockExplorerApiConfig,
|
|
740
|
+
): Layer.Layer<BlockExplorerApiService, BlockExplorerConfigError, ChainService | ProviderService> {
|
|
741
|
+
return Layer.effect(BlockExplorerApiService, makeBlockExplorerApi(config));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Create a BlockExplorerApiService layer from environment variables.
|
|
746
|
+
* Requires ChainService. ProviderService is required when followProxies is used.
|
|
747
|
+
* @since 0.0.1
|
|
748
|
+
*/
|
|
749
|
+
BlockExplorerApi.fromEnv = function fromEnv(): Layer.Layer<
|
|
750
|
+
BlockExplorerApiService,
|
|
751
|
+
BlockExplorerConfigError,
|
|
752
|
+
ChainService | ProviderService
|
|
753
|
+
> {
|
|
754
|
+
return Layer.effect(
|
|
755
|
+
BlockExplorerApiService,
|
|
756
|
+
Effect.gen(function* () {
|
|
757
|
+
const etherscanKey =
|
|
758
|
+
typeof process !== "undefined"
|
|
759
|
+
? process.env.ETHERSCAN_API_KEY
|
|
760
|
+
: undefined;
|
|
761
|
+
const blockscoutKey =
|
|
762
|
+
typeof process !== "undefined"
|
|
763
|
+
? process.env.BLOCKSCOUT_API_KEY
|
|
764
|
+
: undefined;
|
|
765
|
+
const sourcesEnv =
|
|
766
|
+
typeof process !== "undefined"
|
|
767
|
+
? process.env.VOLTAIRE_EXPLORER_SOURCES
|
|
768
|
+
: undefined;
|
|
769
|
+
|
|
770
|
+
const sourceOrder: ExplorerSourceId[] = sourcesEnv
|
|
771
|
+
? (sourcesEnv.split(",").map((s) => s.trim()) as ExplorerSourceId[])
|
|
772
|
+
: (["sourcify", "etherscanV2", "blockscout"] as const).slice();
|
|
773
|
+
|
|
774
|
+
const config: BlockExplorerApiConfig = {
|
|
775
|
+
sources: {
|
|
776
|
+
sourcify: { enabled: true },
|
|
777
|
+
etherscanV2: {
|
|
778
|
+
enabled: true,
|
|
779
|
+
apiKey: etherscanKey ? Redacted.make(etherscanKey) : undefined,
|
|
780
|
+
},
|
|
781
|
+
blockscout: {
|
|
782
|
+
enabled: true,
|
|
783
|
+
apiKey: blockscoutKey ? Redacted.make(blockscoutKey) : undefined,
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
sourceOrder,
|
|
787
|
+
cache: {
|
|
788
|
+
enabled: true,
|
|
789
|
+
ttlMillis: 300_000,
|
|
790
|
+
capacity: 100,
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
return yield* makeBlockExplorerApi(config);
|
|
795
|
+
}),
|
|
796
|
+
);
|
|
797
|
+
};
|