@volga-sh/evm-ghostcall 0.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/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/sdk/generated/initcode.d.ts +2 -0
- package/dist/sdk/generated/initcode.d.ts.map +1 -0
- package/dist/sdk/generated/initcode.js +4 -0
- package/dist/sdk/generated/initcode.js.map +1 -0
- package/dist/sdk/index.d.ts +254 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +226 -0
- package/dist/sdk/index.js.map +1 -0
- package/package.json +56 -0
- package/src/Ghostcall.yul +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 volga-sh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# ghostcall
|
|
2
|
+
|
|
3
|
+
`ghostcall` is a zero-deployment batching program for CREATE-style `eth_call`.
|
|
4
|
+
|
|
5
|
+
Instead of calling a deployed Multicall contract, the client sends compiled initcode plus an
|
|
6
|
+
appended payload. The EVM executes that initcode exactly as if it were deploying a contract, but
|
|
7
|
+
because the transport is `eth_call`, nothing is persisted. Whatever the initcode `RETURN`s comes
|
|
8
|
+
back as the RPC result.
|
|
9
|
+
|
|
10
|
+
The implementation lives in [`src/Ghostcall.yul`](src/Ghostcall.yul).
|
|
11
|
+
|
|
12
|
+
## Quick example
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { aggregateCalls } from "@volga-sh/evm-ghostcall";
|
|
16
|
+
import {
|
|
17
|
+
createPublicClient,
|
|
18
|
+
decodeFunctionResult,
|
|
19
|
+
encodeFunctionData,
|
|
20
|
+
http,
|
|
21
|
+
parseAbi,
|
|
22
|
+
} from "viem";
|
|
23
|
+
import { mainnet } from "viem/chains";
|
|
24
|
+
|
|
25
|
+
const erc20Abi = parseAbi([
|
|
26
|
+
"function balanceOf(address account) view returns (uint256)",
|
|
27
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const token = "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
31
|
+
const owner = "0x1111111111111111111111111111111111111111";
|
|
32
|
+
const spender = "0x2222222222222222222222222222222222222222";
|
|
33
|
+
|
|
34
|
+
const client = createPublicClient({
|
|
35
|
+
chain: mainnet,
|
|
36
|
+
transport: http(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const [balance, allowance] = await aggregateCalls(
|
|
40
|
+
client,
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
to: token,
|
|
44
|
+
data: encodeFunctionData({
|
|
45
|
+
abi: erc20Abi,
|
|
46
|
+
functionName: "balanceOf",
|
|
47
|
+
args: [owner],
|
|
48
|
+
}),
|
|
49
|
+
decodeResult: (data) =>
|
|
50
|
+
decodeFunctionResult({
|
|
51
|
+
abi: erc20Abi,
|
|
52
|
+
functionName: "balanceOf",
|
|
53
|
+
data,
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
to: token,
|
|
58
|
+
data: encodeFunctionData({
|
|
59
|
+
abi: erc20Abi,
|
|
60
|
+
functionName: "allowance",
|
|
61
|
+
args: [owner, spender],
|
|
62
|
+
}),
|
|
63
|
+
decodeResult: (data) =>
|
|
64
|
+
decodeFunctionResult({
|
|
65
|
+
abi: erc20Abi,
|
|
66
|
+
functionName: "allowance",
|
|
67
|
+
data,
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
{ results: "decoded" },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
console.log({
|
|
75
|
+
balance,
|
|
76
|
+
allowance,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Why this works
|
|
81
|
+
|
|
82
|
+
- `eth_call` without a `to` field executes the supplied `data` as CREATE initcode.
|
|
83
|
+
- Initcode can read caller-appended bytes from its own code using `CODECOPY`.
|
|
84
|
+
- Initcode can perform ordinary external calls, pack the returned bytes into memory, and `RETURN` them.
|
|
85
|
+
- Returned bytes are still subject to CREATE limits because the client treats them as would-be
|
|
86
|
+
runtime bytecode.
|
|
87
|
+
|
|
88
|
+
## Development stack
|
|
89
|
+
|
|
90
|
+
The repository now uses a minimal TypeScript-based test stack:
|
|
91
|
+
|
|
92
|
+
- Foundry for contract compilation and `anvil`
|
|
93
|
+
- Node's built-in [`node:test`](https://nodejs.org/api/test.html) runner
|
|
94
|
+
- Node's built-in TypeScript stripping for test execution
|
|
95
|
+
- [`ox`](https://www.npmjs.com/package/ox) for JSON-RPC, ABI, hex, and byte utilities
|
|
96
|
+
- [`@safe-global/mock-contract`](https://www.npmjs.com/package/@safe-global/mock-contract) for configurable mock-call behavior
|
|
97
|
+
|
|
98
|
+
That keeps the dependency footprint small while giving us a stable place to grow ABI-heavy tests.
|
|
99
|
+
|
|
100
|
+
## TypeScript SDK
|
|
101
|
+
|
|
102
|
+
Install the SDK from npm:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm install @volga-sh/evm-ghostcall
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The repository also includes a minimal internal-first TypeScript SDK in
|
|
109
|
+
[`src/sdk/index.ts`](src/sdk/index.ts).
|
|
110
|
+
|
|
111
|
+
It intentionally exposes only the small protocol surface:
|
|
112
|
+
|
|
113
|
+
- `encodeCalls(calls)` bundles the canonical Ghostcall initcode and returns the full CREATE-style `eth_call` data blob.
|
|
114
|
+
- `decodeResults(data)` parses the packed Ghostcall response format into `{ success, returnData }` entries.
|
|
115
|
+
- `aggregateCalls(provider, calls, options?)` sends the CREATE-style `eth_call` through an EIP-1193 `request` provider, decodes the packed response, and optionally runs each call's `decodeResult` callback.
|
|
116
|
+
|
|
117
|
+
`encodeCalls` fails fast if any subcall exceeds the `uint16` calldata limit or if the full
|
|
118
|
+
encoded CREATE payload would exceed the EVM initcode size ceiling.
|
|
119
|
+
|
|
120
|
+
`aggregateCalls` treats `allowFailure` as an SDK-side policy. Failed subcalls reject by default,
|
|
121
|
+
matching Multicall3-style strict batches, while calls marked `allowFailure: true` are returned as
|
|
122
|
+
ordinary `{ success: false, returnData }` entries.
|
|
123
|
+
|
|
124
|
+
The SDK has no ABI helpers and no runtime artifact reads. To ABI-decode successful entries, pass
|
|
125
|
+
`decodeResult` callbacks that call the ABI library already used by the application. By default,
|
|
126
|
+
`aggregateCalls` returns result entries. Pass `{ results: "decoded" }` to return decoded values
|
|
127
|
+
directly.
|
|
128
|
+
|
|
129
|
+
## Current scope
|
|
130
|
+
|
|
131
|
+
This implementation is intentionally focused on the smallest SDK-first variant:
|
|
132
|
+
|
|
133
|
+
- zero-value `CALL` for subcalls
|
|
134
|
+
- packed binary input instead of ABI encoding
|
|
135
|
+
- packed binary output instead of ABI encoding
|
|
136
|
+
- always-return result entries for every subcall
|
|
137
|
+
- SDK-enforced strict failure policy instead of engine-enforced batch reverts
|
|
138
|
+
|
|
139
|
+
That keeps the initcode small, auditable, and easy to extend.
|
|
140
|
+
|
|
141
|
+
## Why not a naive Solidity constructor
|
|
142
|
+
|
|
143
|
+
A straightforward deployless design is to write a Solidity constructor that:
|
|
144
|
+
|
|
145
|
+
- accepts an ABI-encoded array of calls,
|
|
146
|
+
- executes them in the constructor, and
|
|
147
|
+
- rewrites constructor memory so the returned bytes look like a normal ABI-encoded multicall result.
|
|
148
|
+
|
|
149
|
+
That approach works, but this project intentionally uses a lower-level Yul program instead.
|
|
150
|
+
|
|
151
|
+
Advantages of the current design:
|
|
152
|
+
|
|
153
|
+
- smaller base program, because it avoids Solidity's constructor scaffolding and generic ABI decoding,
|
|
154
|
+
- a tighter wire format, because both requests and responses use a compact custom binary layout instead of full ABI encoding,
|
|
155
|
+
- less compiler coupling, because the batching logic does not depend on Solidity memory-layout assumptions inside constructor-generated code.
|
|
156
|
+
|
|
157
|
+
In practice, this means less initcode to ship on every request, fewer bytes on the wire, and a design that is easier to reason about at the EVM level.
|
|
158
|
+
|
|
159
|
+
## Input format
|
|
160
|
+
|
|
161
|
+
The caller sends:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
<compiled ghostcall initcode><payload>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Payload layout:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
N bytes repeated call entries
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Each call entry:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
2 bytes calldata length (big-endian uint16)
|
|
177
|
+
20 bytes target
|
|
178
|
+
N bytes calldata
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Notes:
|
|
182
|
+
|
|
183
|
+
- Payload bytes are not normal calldata. They are appended after the compiled initcode and read via
|
|
184
|
+
`CODECOPY`.
|
|
185
|
+
- The length comes first on purpose. Ghostcall copies the 22-byte fixed header into scratch memory
|
|
186
|
+
at offset `0x0a`, so one `mload(0x00)` exposes the length in the high 2 non-zero bytes and the
|
|
187
|
+
target address in the low 20 bytes used by `CALL`.
|
|
188
|
+
- An empty payload is valid and returns an empty result blob.
|
|
189
|
+
- Per-call calldata is limited to `65535` bytes because the format uses `uint16`.
|
|
190
|
+
- The whole CREATE payload is still limited by the network/client initcode size ceiling.
|
|
191
|
+
|
|
192
|
+
## Output format
|
|
193
|
+
|
|
194
|
+
The program returns:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
N bytes repeated result entries
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Each result entry:
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
2 bytes packed header
|
|
204
|
+
bit 15 = success flag
|
|
205
|
+
bits 0-14 = returndata length (big-endian uint15)
|
|
206
|
+
N bytes returndata
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Subcall failures are returned inline as ordinary result entries with `success = 0`.
|
|
210
|
+
|
|
211
|
+
The engine only reverts for malformed payloads or per-entry return-size violations, and those
|
|
212
|
+
top-level reverts are intentionally empty. The SDK is expected to validate payloads up front and
|
|
213
|
+
impose any higher-level "fail the whole batch" policy for callers that want it.
|
|
214
|
+
|
|
215
|
+
The packed result header can represent up to `32767` bytes of returndata per entry. On
|
|
216
|
+
Ethereum, EIP-170's returned-code limit is usually the stricter bound: CREATE-style execution
|
|
217
|
+
limits the whole response to `24,576` bytes, including the 2-byte header on each entry.
|
|
218
|
+
|
|
219
|
+
## Limits
|
|
220
|
+
|
|
221
|
+
The aggregate response is returned through CREATE-style execution, so clients still treat it as
|
|
222
|
+
would-be runtime code. Ghostcall does not impose its own aggregate response cap; the effective
|
|
223
|
+
ceiling comes from the chain, client, RPC provider, gas setting, and request-size policy.
|
|
224
|
+
|
|
225
|
+
Common reference points:
|
|
226
|
+
|
|
227
|
+
- Ethereum's EIP-170 returned-code limit is `24,576` bytes.
|
|
228
|
+
- Ethereum's EIP-3860 initcode limit is `49,152` bytes.
|
|
229
|
+
- Other chains may set different values. For example, Monad documents larger contract-code and
|
|
230
|
+
initcode limits.
|
|
231
|
+
|
|
232
|
+
Measure the endpoint you plan to use instead of assuming a consensus value. Provider-side request
|
|
233
|
+
limits can be lower than the chain limit.
|
|
234
|
+
|
|
235
|
+
## Benchmark limits
|
|
236
|
+
|
|
237
|
+
The repository includes a TypeScript benchmark for rough endpoint-specific measurements:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npm run benchmark:limits -- --rpc-url "$RPC_URL" --mode raw
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`raw` mode probes accepted CREATE initcode bytes and returned runtime-code bytes. `balances` mode
|
|
244
|
+
uses a realistic ERC-20 balance workload:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npm run benchmark:limits -- \
|
|
248
|
+
--rpc-url "$RPC_URL" \
|
|
249
|
+
--mode balances \
|
|
250
|
+
--token "$TOKEN_ADDRESS" \
|
|
251
|
+
--owner "$OWNER_ADDRESS"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For balance benchmarking, pass token addresses that implement `balanceOf(address)` on the selected
|
|
255
|
+
chain. The script repeats those token and owner inputs, builds ghostcall batches with the public
|
|
256
|
+
SDK encoder, and searches for the largest successful call count. The balance search is capped by
|
|
257
|
+
both `--max-calls` and `--max-initcode-bytes`.
|
|
258
|
+
|
|
259
|
+
Useful options:
|
|
260
|
+
|
|
261
|
+
- `--mode raw|balances|all`, default `all`
|
|
262
|
+
- `--token` and `--owner`, repeatable or comma-separated
|
|
263
|
+
- `--block`, `--from`, `--gas`, and `--timeout-ms`
|
|
264
|
+
- `--max-calls`, `--max-initcode-bytes`, and `--max-runtime-bytes`
|
|
265
|
+
- `--json` for machine-readable output
|
|
266
|
+
|
|
267
|
+
## Install
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
npm install
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Build contracts
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm run build:contracts
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The compiled artifacts are emitted into the standard Foundry artifact tree under `out/`.
|
|
280
|
+
That build step also refreshes the generated SDK initcode file at
|
|
281
|
+
[`src/sdk/generated/initcode.ts`](src/sdk/generated/initcode.ts).
|
|
282
|
+
|
|
283
|
+
## Test
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm test
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The test suite:
|
|
290
|
+
|
|
291
|
+
- compiles the contracts with Foundry,
|
|
292
|
+
- starts an ephemeral `anvil` instance automatically,
|
|
293
|
+
- deploys and configures `MockContract` from Foundry artifacts,
|
|
294
|
+
- encodes function calldata with `ox`,
|
|
295
|
+
- executes a CREATE-style `eth_call` against Ghostcall,
|
|
296
|
+
- dogfoods the provider-facing SDK aggregation helper,
|
|
297
|
+
- decodes both function return data and revert data with `ox`,
|
|
298
|
+
- verifies configurable success paths, calldata-vs-method precedence, inline failure entries, the empty-batch case, the CREATE request-size boundary, the CREATE return-size boundary, and top-level malformed-payload handling.
|
|
299
|
+
|
|
300
|
+
For static TypeScript checking:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npm run typecheck
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Design notes
|
|
307
|
+
|
|
308
|
+
The implementation chooses Yul over raw bytecode because it keeps the control flow legible while
|
|
309
|
+
still mapping one-to-one onto the EVM concepts that matter here:
|
|
310
|
+
|
|
311
|
+
- `dataoffset(...)` anchors the appended payload boundary
|
|
312
|
+
- `codecopy` streams headers and calldata directly from the appended payload
|
|
313
|
+
- the len-first header plus a `0x0a` scratch offset lets one `mload(0x00)` yield both calldata
|
|
314
|
+
length and the `CALL` address word without extra masking
|
|
315
|
+
- `call` executes each subcall with zero value
|
|
316
|
+
- `returndatacopy` packs the aggregate response into a compact binary format
|
|
317
|
+
- `return` hands the batch result back to RPC
|
|
318
|
+
|
|
319
|
+
That gives you a maintainable base version first, with a straightforward path to hand-optimizing
|
|
320
|
+
hot spots later if initcode size becomes the bottleneck.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const ghostcallInitcode: "0x6020606c5b38810360135750601f19016020f35b90601682600a395f51918260a01c9060168282010193388511606757825f92838093601696600289019788940184395af1913d91617fff83116067575f839182600296600f1b1760f01b84523e0101906004565b5f80fdfe";
|
|
2
|
+
//# sourceMappingURL=initcode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initcode.d.ts","sourceRoot":"","sources":["../../../src/sdk/generated/initcode.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,EAC7B,4NAAqO,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// This file is generated by scripts/generate-sdk-initcode.mjs.
|
|
2
|
+
// Run npm run build:contracts after changing src/Ghostcall.yul.
|
|
3
|
+
export const ghostcallInitcode = "0x6020606c5b38810360135750601f19016020f35b90601682600a395f51918260a01c9060168282010193388511606757825f92838093601696600289019788940184395af1913d91617fff83116067575f839182600296600f1b1760f01b84523e0101906004565b5f80fdfe";
|
|
4
|
+
//# sourceMappingURL=initcode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initcode.js","sourceRoot":"","sources":["../../../src/sdk/generated/initcode.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,gEAAgE;AAEhE,MAAM,CAAC,MAAM,iBAAiB,GAC7B,4NAAqO,CAAC"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex-encoded binary data prefixed with `0x`.
|
|
3
|
+
*
|
|
4
|
+
* Ghostcall request and response data is represented as raw hex strings. The
|
|
5
|
+
* SDK does not accept byte arrays or ABI fragments.
|
|
6
|
+
*/
|
|
7
|
+
type Hex = `0x${string}`;
|
|
8
|
+
/**
|
|
9
|
+
* One Ghostcall subcall entry.
|
|
10
|
+
*/
|
|
11
|
+
type GhostcallCall = {
|
|
12
|
+
/**
|
|
13
|
+
* Target contract address to invoke.
|
|
14
|
+
*/
|
|
15
|
+
to: Hex;
|
|
16
|
+
/**
|
|
17
|
+
* Hex-encoded call data to forward to {@link GhostcallCall.to}.
|
|
18
|
+
*
|
|
19
|
+
* The encoded payload is limited to `65535` bytes because Ghostcall stores each
|
|
20
|
+
* calldata length as a big-endian `uint16`.
|
|
21
|
+
*/
|
|
22
|
+
data: Hex;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* One Ghostcall aggregate subcall entry.
|
|
26
|
+
*
|
|
27
|
+
* The wire format does not include failure-policy bits. `allowFailure` is an SDK
|
|
28
|
+
* policy applied after Ghostcall returns the packed result entries.
|
|
29
|
+
*/
|
|
30
|
+
type GhostcallAggregateCall = GhostcallCall & {
|
|
31
|
+
/**
|
|
32
|
+
* Allows this subcall to return a failed result entry.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to `false`, matching Multicall3's strict `aggregate3` behavior when
|
|
35
|
+
* a call does not explicitly opt into failure.
|
|
36
|
+
*/
|
|
37
|
+
allowFailure?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Optional decoder for this call's successful return data.
|
|
40
|
+
*
|
|
41
|
+
* This is intentionally a caller-provided function so the SDK stays independent
|
|
42
|
+
* from ABI libraries while still letting callers plug in helpers such as
|
|
43
|
+
* `decodeFunctionResult` from viem or ox.
|
|
44
|
+
*/
|
|
45
|
+
decodeResult?: GhostcallResultDecoder<unknown>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* One Ghostcall aggregate subcall entry for decoded-results mode.
|
|
49
|
+
*/
|
|
50
|
+
type GhostcallDecodedAggregateCall<TResult = unknown> = GhostcallCall & {
|
|
51
|
+
/**
|
|
52
|
+
* Decodes this call's successful return data.
|
|
53
|
+
*/
|
|
54
|
+
decodeResult: GhostcallResultDecoder<TResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Decoded-results mode is strict and does not return failed entries.
|
|
57
|
+
*/
|
|
58
|
+
allowFailure?: false;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* One decoded Ghostcall result entry.
|
|
62
|
+
*/
|
|
63
|
+
type GhostcallResult = {
|
|
64
|
+
/**
|
|
65
|
+
* Indicates whether the underlying EVM `CALL` returned successfully.
|
|
66
|
+
*
|
|
67
|
+
* A `false` value means the target call reverted or otherwise failed, but the
|
|
68
|
+
* Ghostcall batch itself still completed successfully.
|
|
69
|
+
*/
|
|
70
|
+
success: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Raw return data produced by the target call.
|
|
73
|
+
*
|
|
74
|
+
* For failed calls this contains revert data, if any. The SDK leaves higher-level
|
|
75
|
+
* ABI decoding and failure policy to the caller.
|
|
76
|
+
*/
|
|
77
|
+
returnData: Hex;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Function used by {@link aggregateCalls} to turn raw successful return data into
|
|
81
|
+
* a caller-chosen value.
|
|
82
|
+
*/
|
|
83
|
+
type GhostcallResultDecoder<TResult> = (returnData: Hex, entry: GhostcallResult, index: number) => TResult;
|
|
84
|
+
/**
|
|
85
|
+
* One decoded aggregate result entry when a call provides `decodeResult`.
|
|
86
|
+
*/
|
|
87
|
+
type GhostcallDecodedResult<TResult> = {
|
|
88
|
+
success: true;
|
|
89
|
+
returnData: Hex;
|
|
90
|
+
decodedResult: TResult;
|
|
91
|
+
};
|
|
92
|
+
type GhostcallAggregateResult<TCall> = TCall extends {
|
|
93
|
+
decodeResult: GhostcallResultDecoder<infer TResult>;
|
|
94
|
+
} ? TCall extends {
|
|
95
|
+
allowFailure: true;
|
|
96
|
+
} ? GhostcallDecodedResult<TResult> | GhostcallResult : GhostcallDecodedResult<TResult> : GhostcallResult;
|
|
97
|
+
type GhostcallAggregateResults<TCalls extends readonly GhostcallAggregateCall[]> = {
|
|
98
|
+
-readonly [Index in keyof TCalls]: GhostcallAggregateResult<TCalls[Index]>;
|
|
99
|
+
};
|
|
100
|
+
type GhostcallDecodedResults<TCalls extends readonly GhostcallDecodedAggregateCall[]> = {
|
|
101
|
+
-readonly [Index in keyof TCalls]: TCalls[Index] extends {
|
|
102
|
+
decodeResult: GhostcallResultDecoder<infer TResult>;
|
|
103
|
+
} ? TResult : never;
|
|
104
|
+
};
|
|
105
|
+
type GhostcallAggregateOptions = {
|
|
106
|
+
/**
|
|
107
|
+
* Result shape returned by {@link aggregateCalls}.
|
|
108
|
+
*
|
|
109
|
+
* Defaults to `entries`, returning Ghostcall result entries. Set to `decoded`
|
|
110
|
+
* to return each call's decoded value directly.
|
|
111
|
+
*/
|
|
112
|
+
results?: "entries";
|
|
113
|
+
};
|
|
114
|
+
type GhostcallDecodedAggregateOptions = {
|
|
115
|
+
/**
|
|
116
|
+
* Return each call's decoded value directly.
|
|
117
|
+
*/
|
|
118
|
+
results: "decoded";
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Minimal EIP-1193 provider shape used by the SDK.
|
|
122
|
+
*/
|
|
123
|
+
type EIP1193ProviderWithRequestFn = {
|
|
124
|
+
request(args: {
|
|
125
|
+
method: string;
|
|
126
|
+
params?: unknown;
|
|
127
|
+
}): Promise<unknown>;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Encodes a list of contract calls into the full CREATE-style `eth_call` payload
|
|
131
|
+
* expected by Ghostcall.
|
|
132
|
+
*
|
|
133
|
+
* The returned hex string already includes the bundled Ghostcall initcode followed
|
|
134
|
+
* by the compact binary payload for each subcall, so callers can pass it directly
|
|
135
|
+
* as the `data` field of an `eth_call` request without supplying a `to` address.
|
|
136
|
+
* Each encoded subcall entry uses the compact layout `[len(2)][target(20)][data]`.
|
|
137
|
+
*
|
|
138
|
+
* @param calls - Ordered list of subcalls to execute. Each entry becomes one
|
|
139
|
+
* Ghostcall payload segment in the same order it appears here.
|
|
140
|
+
*
|
|
141
|
+
* @returns Full CREATE payload consisting of the bundled Ghostcall initcode plus
|
|
142
|
+
* the encoded call list.
|
|
143
|
+
*
|
|
144
|
+
* @throws {TypeError} If any call address or calldata value is not valid hex.
|
|
145
|
+
* @throws {RangeError} If any call data exceeds the protocol `uint16` length limit
|
|
146
|
+
* or if the full encoded CREATE payload would exceed the
|
|
147
|
+
* EVM initcode size limit.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const data = encodeCalls([
|
|
151
|
+
* {
|
|
152
|
+
* to: "0x1111111111111111111111111111111111111111",
|
|
153
|
+
* data: "0x70a08231000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
154
|
+
* },
|
|
155
|
+
* {
|
|
156
|
+
* to: "0x2222222222222222222222222222222222222222",
|
|
157
|
+
* data: "0x18160ddd",
|
|
158
|
+
* },
|
|
159
|
+
* ]);
|
|
160
|
+
*
|
|
161
|
+
* // Later:
|
|
162
|
+
* // provider.request({ method: "eth_call", params: [{ data }, "latest"] })
|
|
163
|
+
*/
|
|
164
|
+
declare function encodeCalls(calls: readonly GhostcallCall[]): Hex;
|
|
165
|
+
/**
|
|
166
|
+
* Sends a Ghostcall batch with a CREATE-style `eth_call` and decodes the result.
|
|
167
|
+
*
|
|
168
|
+
* This is the provider-facing counterpart to {@link encodeCalls} and
|
|
169
|
+
* {@link decodeResults}. It sends the bundled Ghostcall initcode as the `data`
|
|
170
|
+
* field of `eth_call` without a `to` address, then returns decoded result entries
|
|
171
|
+
* in the same order as the input calls.
|
|
172
|
+
*
|
|
173
|
+
* By default, any failed subcall makes this method reject. Set
|
|
174
|
+
* `allowFailure: true` on a call to receive that failed entry in the returned
|
|
175
|
+
* results instead. Set `decodeResult` on a call to transform successful raw
|
|
176
|
+
* return data, for example with `decodeFunctionResult` from an ABI library.
|
|
177
|
+
* Pass `{ results: "decoded" }` as the third argument to receive only the
|
|
178
|
+
* decoded values.
|
|
179
|
+
*
|
|
180
|
+
* @param provider - EIP-1193-compatible provider with a `request` method.
|
|
181
|
+
* @param calls - Ordered list of subcalls to execute.
|
|
182
|
+
* @param options - Optional result-shape controls.
|
|
183
|
+
*
|
|
184
|
+
* @returns Ordered decoded Ghostcall result entries.
|
|
185
|
+
*
|
|
186
|
+
* @throws {TypeError} If inputs are not valid Ghostcall call entries or if the
|
|
187
|
+
* provider returns a non-hex `eth_call` result.
|
|
188
|
+
* @throws {RangeError} If the encoded CREATE payload exceeds protocol or EVM
|
|
189
|
+
* size limits.
|
|
190
|
+
* @throws {Error} If a subcall fails without `allowFailure: true`, or if the
|
|
191
|
+
* response entry count does not match the request entry count.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* const results = await aggregateCalls(provider, [
|
|
195
|
+
* {
|
|
196
|
+
* to: "0x1111111111111111111111111111111111111111",
|
|
197
|
+
* data: "0x70a08231000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
198
|
+
* decodeResult: (returnData) => decodeFunctionResult({
|
|
199
|
+
* abi: erc20Abi,
|
|
200
|
+
* functionName: "balanceOf",
|
|
201
|
+
* data: returnData,
|
|
202
|
+
* }),
|
|
203
|
+
* },
|
|
204
|
+
* {
|
|
205
|
+
* to: "0x2222222222222222222222222222222222222222",
|
|
206
|
+
* data: "0x18160ddd",
|
|
207
|
+
* allowFailure: true,
|
|
208
|
+
* },
|
|
209
|
+
* ]);
|
|
210
|
+
*
|
|
211
|
+
* const [balance] = await aggregateCalls(provider, [
|
|
212
|
+
* {
|
|
213
|
+
* to: "0x1111111111111111111111111111111111111111",
|
|
214
|
+
* data: "0x70a08231000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
215
|
+
* decodeResult: (returnData) => decodeFunctionResult({
|
|
216
|
+
* abi: erc20Abi,
|
|
217
|
+
* functionName: "balanceOf",
|
|
218
|
+
* data: returnData,
|
|
219
|
+
* }),
|
|
220
|
+
* },
|
|
221
|
+
* ], { results: "decoded" });
|
|
222
|
+
*/
|
|
223
|
+
declare function aggregateCalls<const TCalls extends readonly GhostcallAggregateCall[]>(provider: EIP1193ProviderWithRequestFn, calls: TCalls, options?: GhostcallAggregateOptions): Promise<GhostcallAggregateResults<TCalls>>;
|
|
224
|
+
declare function aggregateCalls<const TCalls extends readonly GhostcallDecodedAggregateCall[]>(provider: EIP1193ProviderWithRequestFn, calls: TCalls, options: GhostcallDecodedAggregateOptions): Promise<GhostcallDecodedResults<TCalls>>;
|
|
225
|
+
/**
|
|
226
|
+
* Decodes the packed result blob returned by Ghostcall.
|
|
227
|
+
*
|
|
228
|
+
* Each decoded entry corresponds to exactly one subcall in the original batch and
|
|
229
|
+
* preserves the original ordering. The SDK intentionally returns raw result bytes
|
|
230
|
+
* rather than ABI-decoding them so higher-level callers can apply their own
|
|
231
|
+
* decoding and failure policy.
|
|
232
|
+
*
|
|
233
|
+
* @param data - Raw bytes returned by Ghostcall, typically the direct result of a
|
|
234
|
+
* CREATE-style `eth_call`.
|
|
235
|
+
*
|
|
236
|
+
* @returns Ordered list of decoded Ghostcall result entries. Returns an empty
|
|
237
|
+
* array for `0x`.
|
|
238
|
+
*
|
|
239
|
+
* @throws {TypeError} If the provided data is not valid hex, if a result header is
|
|
240
|
+
* truncated, or if an entry body is shorter than advertised.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* const results = decodeResults("0x8002cafe0004deadbeef");
|
|
244
|
+
*
|
|
245
|
+
* console.log(results);
|
|
246
|
+
* // [
|
|
247
|
+
* // { success: true, returnData: "0xcafe" },
|
|
248
|
+
* // { success: false, returnData: "0xdeadbeef" }
|
|
249
|
+
* // ]
|
|
250
|
+
*/
|
|
251
|
+
declare function decodeResults(data: Hex): GhostcallResult[];
|
|
252
|
+
export type { EIP1193ProviderWithRequestFn, GhostcallAggregateCall, GhostcallAggregateOptions, GhostcallAggregateResult, GhostcallCall, GhostcallDecodedAggregateCall, GhostcallDecodedAggregateOptions, GhostcallDecodedResult, GhostcallDecodedResults, GhostcallResult, GhostcallResultDecoder, Hex, };
|
|
253
|
+
export { aggregateCalls, decodeResults, encodeCalls };
|
|
254
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ghostcallInitcode } from "./generated/initcode.js";
|
|
2
|
+
const addressHexLength = 40;
|
|
3
|
+
const encodedHeaderHexLength = 4;
|
|
4
|
+
const maxCalldataSize = 0xffff;
|
|
5
|
+
const encodedCallHeaderSize = 0x16;
|
|
6
|
+
const maxCreateInitcodeSize = 0xc000;
|
|
7
|
+
const successFlagMask = 0x8000;
|
|
8
|
+
const returnDataLengthMask = 0x7fff;
|
|
9
|
+
const bundledInitcodeSize = byteLength(ghostcallInitcode);
|
|
10
|
+
/**
|
|
11
|
+
* Encodes a list of contract calls into the full CREATE-style `eth_call` payload
|
|
12
|
+
* expected by Ghostcall.
|
|
13
|
+
*
|
|
14
|
+
* The returned hex string already includes the bundled Ghostcall initcode followed
|
|
15
|
+
* by the compact binary payload for each subcall, so callers can pass it directly
|
|
16
|
+
* as the `data` field of an `eth_call` request without supplying a `to` address.
|
|
17
|
+
* Each encoded subcall entry uses the compact layout `[len(2)][target(20)][data]`.
|
|
18
|
+
*
|
|
19
|
+
* @param calls - Ordered list of subcalls to execute. Each entry becomes one
|
|
20
|
+
* Ghostcall payload segment in the same order it appears here.
|
|
21
|
+
*
|
|
22
|
+
* @returns Full CREATE payload consisting of the bundled Ghostcall initcode plus
|
|
23
|
+
* the encoded call list.
|
|
24
|
+
*
|
|
25
|
+
* @throws {TypeError} If any call address or calldata value is not valid hex.
|
|
26
|
+
* @throws {RangeError} If any call data exceeds the protocol `uint16` length limit
|
|
27
|
+
* or if the full encoded CREATE payload would exceed the
|
|
28
|
+
* EVM initcode size limit.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const data = encodeCalls([
|
|
32
|
+
* {
|
|
33
|
+
* to: "0x1111111111111111111111111111111111111111",
|
|
34
|
+
* data: "0x70a08231000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
35
|
+
* },
|
|
36
|
+
* {
|
|
37
|
+
* to: "0x2222222222222222222222222222222222222222",
|
|
38
|
+
* data: "0x18160ddd",
|
|
39
|
+
* },
|
|
40
|
+
* ]);
|
|
41
|
+
*
|
|
42
|
+
* // Later:
|
|
43
|
+
* // provider.request({ method: "eth_call", params: [{ data }, "latest"] })
|
|
44
|
+
*/
|
|
45
|
+
function encodeCalls(calls) {
|
|
46
|
+
const encodedParts = [ghostcallInitcode.slice(2)];
|
|
47
|
+
let totalEncodedSize = bundledInitcodeSize;
|
|
48
|
+
for (const [index, call] of calls.entries()) {
|
|
49
|
+
assertAddress(call.to, `calls[${index}].to`);
|
|
50
|
+
const calldata = assertHex(call.data, `calls[${index}].data`);
|
|
51
|
+
const calldataSize = byteLength(calldata);
|
|
52
|
+
if (calldataSize > maxCalldataSize) {
|
|
53
|
+
throw new RangeError(`calls[${index}].data exceeds the ${maxCalldataSize}-byte calldata limit`);
|
|
54
|
+
}
|
|
55
|
+
totalEncodedSize += encodedCallHeaderSize + calldataSize;
|
|
56
|
+
if (totalEncodedSize > maxCreateInitcodeSize) {
|
|
57
|
+
throw new RangeError(`encoded Ghostcall initcode exceeds the ${maxCreateInitcodeSize}-byte CREATE initcode limit`);
|
|
58
|
+
}
|
|
59
|
+
encodedParts.push(calldataSize.toString(16).padStart(4, "0"));
|
|
60
|
+
encodedParts.push(call.to.slice(2));
|
|
61
|
+
encodedParts.push(calldata.slice(2));
|
|
62
|
+
}
|
|
63
|
+
return `0x${encodedParts.join("")}`;
|
|
64
|
+
}
|
|
65
|
+
async function aggregateCalls(provider, calls, options = {}) {
|
|
66
|
+
let decodedCalls;
|
|
67
|
+
if (options.results === "decoded") {
|
|
68
|
+
for (const [index, call] of calls.entries()) {
|
|
69
|
+
if (call.allowFailure === true) {
|
|
70
|
+
throw new TypeError(`calls[${index}].allowFailure cannot be true when results is decoded`);
|
|
71
|
+
}
|
|
72
|
+
if (call.decodeResult === undefined) {
|
|
73
|
+
throw new TypeError(`calls[${index}].decodeResult is required when results is decoded`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
decodedCalls = calls;
|
|
77
|
+
}
|
|
78
|
+
const data = encodeCalls(calls);
|
|
79
|
+
const result = await provider.request({
|
|
80
|
+
method: "eth_call",
|
|
81
|
+
params: [{ data }, "latest"],
|
|
82
|
+
});
|
|
83
|
+
const entries = decodeResults(assertHex(result, "eth_call result"));
|
|
84
|
+
if (entries.length !== calls.length) {
|
|
85
|
+
throw new Error(`Ghostcall returned ${entries.length} result entries for ${calls.length} calls`);
|
|
86
|
+
}
|
|
87
|
+
for (const [index, entry] of entries.entries()) {
|
|
88
|
+
if (!entry.success && calls[index]?.allowFailure !== true) {
|
|
89
|
+
throw new Error(`Ghostcall subcall ${index} failed`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (decodedCalls !== undefined) {
|
|
93
|
+
return entries.map((entry, index) => {
|
|
94
|
+
const call = decodedCalls[index];
|
|
95
|
+
if (call === undefined) {
|
|
96
|
+
throw new Error("Ghostcall decoded call invariant failed");
|
|
97
|
+
}
|
|
98
|
+
return call.decodeResult(entry.returnData, entry, index);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const decodedEntries = entries.map((entry, index) => {
|
|
102
|
+
const decodeResult = calls[index]?.decodeResult;
|
|
103
|
+
if (!entry.success || decodeResult === undefined) {
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...entry,
|
|
108
|
+
success: true,
|
|
109
|
+
decodedResult: decodeResult(entry.returnData, entry, index),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
return decodedEntries;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Decodes the packed result blob returned by Ghostcall.
|
|
116
|
+
*
|
|
117
|
+
* Each decoded entry corresponds to exactly one subcall in the original batch and
|
|
118
|
+
* preserves the original ordering. The SDK intentionally returns raw result bytes
|
|
119
|
+
* rather than ABI-decoding them so higher-level callers can apply their own
|
|
120
|
+
* decoding and failure policy.
|
|
121
|
+
*
|
|
122
|
+
* @param data - Raw bytes returned by Ghostcall, typically the direct result of a
|
|
123
|
+
* CREATE-style `eth_call`.
|
|
124
|
+
*
|
|
125
|
+
* @returns Ordered list of decoded Ghostcall result entries. Returns an empty
|
|
126
|
+
* array for `0x`.
|
|
127
|
+
*
|
|
128
|
+
* @throws {TypeError} If the provided data is not valid hex, if a result header is
|
|
129
|
+
* truncated, or if an entry body is shorter than advertised.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const results = decodeResults("0x8002cafe0004deadbeef");
|
|
133
|
+
*
|
|
134
|
+
* console.log(results);
|
|
135
|
+
* // [
|
|
136
|
+
* // { success: true, returnData: "0xcafe" },
|
|
137
|
+
* // { success: false, returnData: "0xdeadbeef" }
|
|
138
|
+
* // ]
|
|
139
|
+
*/
|
|
140
|
+
function decodeResults(data) {
|
|
141
|
+
const normalizedData = assertHex(data, "data");
|
|
142
|
+
if (normalizedData === "0x") {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const results = [];
|
|
146
|
+
const encodedData = normalizedData.slice(2);
|
|
147
|
+
let cursor = 0;
|
|
148
|
+
while (cursor < encodedData.length) {
|
|
149
|
+
if (cursor + encodedHeaderHexLength > encodedData.length) {
|
|
150
|
+
throw new TypeError("Truncated Ghostcall response header");
|
|
151
|
+
}
|
|
152
|
+
const header = Number.parseInt(encodedData.slice(cursor, cursor + encodedHeaderHexLength), 16);
|
|
153
|
+
const success = (header & successFlagMask) !== 0;
|
|
154
|
+
const returnDataSize = header & returnDataLengthMask;
|
|
155
|
+
const nextCursor = cursor + encodedHeaderHexLength;
|
|
156
|
+
const returnDataEnd = nextCursor + returnDataSize * 2;
|
|
157
|
+
if (returnDataEnd > encodedData.length) {
|
|
158
|
+
throw new TypeError("Truncated Ghostcall response body");
|
|
159
|
+
}
|
|
160
|
+
results.push({
|
|
161
|
+
success,
|
|
162
|
+
returnData: `0x${encodedData.slice(nextCursor, returnDataEnd)}`,
|
|
163
|
+
});
|
|
164
|
+
cursor = returnDataEnd;
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validates that a value is a canonical 20-byte hex address.
|
|
170
|
+
*
|
|
171
|
+
* @param value - Unknown input to validate.
|
|
172
|
+
* @param label - Field name used in thrown error messages.
|
|
173
|
+
*
|
|
174
|
+
* @throws {TypeError} If the value is not valid `0x`-prefixed hex or is not
|
|
175
|
+
* exactly 20 bytes long.
|
|
176
|
+
*
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
function assertAddress(value, label) {
|
|
180
|
+
const normalizedValue = assertHex(value, label);
|
|
181
|
+
if (normalizedValue.length !== addressHexLength + 2) {
|
|
182
|
+
throw new TypeError(`${label} must be a 20-byte hex string`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validates that a value is an even-length `0x`-prefixed hex string.
|
|
187
|
+
*
|
|
188
|
+
* @param value - Unknown input to validate.
|
|
189
|
+
* @param label - Field name used in thrown error messages.
|
|
190
|
+
*
|
|
191
|
+
* @returns The validated value narrowed to {@link Hex}.
|
|
192
|
+
*
|
|
193
|
+
* @throws {TypeError} If the value is not a string, lacks the `0x` prefix, has an
|
|
194
|
+
* odd number of hex characters, or contains non-hex digits.
|
|
195
|
+
*
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
function assertHex(value, label) {
|
|
199
|
+
if (typeof value !== "string") {
|
|
200
|
+
throw new TypeError(`${label} must be a hex string`);
|
|
201
|
+
}
|
|
202
|
+
if (!value.startsWith("0x")) {
|
|
203
|
+
throw new TypeError(`${label} must start with 0x`);
|
|
204
|
+
}
|
|
205
|
+
const rawValue = value.slice(2);
|
|
206
|
+
if (rawValue.length % 2 !== 0) {
|
|
207
|
+
throw new TypeError(`${label} must have an even number of hex characters`);
|
|
208
|
+
}
|
|
209
|
+
if (!/^[0-9a-fA-F]*$/.test(rawValue)) {
|
|
210
|
+
throw new TypeError(`${label} must contain only hexadecimal characters`);
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Returns the byte length of a validated hex string.
|
|
216
|
+
*
|
|
217
|
+
* @param value - Validated hex string.
|
|
218
|
+
* @returns Number of bytes represented by {@link value}.
|
|
219
|
+
*
|
|
220
|
+
* @internal
|
|
221
|
+
*/
|
|
222
|
+
function byteLength(value) {
|
|
223
|
+
return (value.length - 2) / 2;
|
|
224
|
+
}
|
|
225
|
+
export { aggregateCalls, decodeResults, encodeCalls };
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volga-sh/evm-ghostcall",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Zero-deployment batching program and TypeScript SDK for CREATE-style eth_call requests.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/volga-sh/ghostcall.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/volga-sh/ghostcall/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/volga-sh/ghostcall#readme",
|
|
15
|
+
"main": "./dist/sdk/index.js",
|
|
16
|
+
"types": "./dist/sdk/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/sdk/index.d.ts",
|
|
20
|
+
"import": "./dist/sdk/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"src/Ghostcall.yul"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"benchmark:limits": "node --disable-warning=ExperimentalWarning --experimental-strip-types scripts/benchmark-limits.ts",
|
|
33
|
+
"build:contracts": "forge build && npm run generate:sdk:initcode",
|
|
34
|
+
"build:sdk": "npm run build:contracts && tsc --project tsconfig.build.json",
|
|
35
|
+
"check": "biome check .",
|
|
36
|
+
"check:fix": "biome check --write .",
|
|
37
|
+
"check:sdk:initcode": "git diff --exit-code -- src/sdk/generated/initcode.ts",
|
|
38
|
+
"format": "biome format --write .",
|
|
39
|
+
"format:check": "biome format .",
|
|
40
|
+
"generate:sdk:initcode": "node scripts/generate-sdk-initcode.mjs",
|
|
41
|
+
"lint": "biome lint .",
|
|
42
|
+
"lint:fix": "npm run check:fix",
|
|
43
|
+
"prepack": "npm run build:sdk",
|
|
44
|
+
"test": "npm run build:contracts && node --disable-warning=ExperimentalWarning --experimental-strip-types --test test/*.test.ts",
|
|
45
|
+
"test:watch": "node --disable-warning=ExperimentalWarning --experimental-strip-types --test --watch test/*.test.ts",
|
|
46
|
+
"typecheck": "tsc --project tsconfig.json"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@biomejs/biome": "2.4.12",
|
|
51
|
+
"@safe-global/mock-contract": "4.1.0",
|
|
52
|
+
"ox": "0.9.6",
|
|
53
|
+
"@types/node": "24.3.1",
|
|
54
|
+
"typescript": "5.9.2"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
object "Ghostcall" {
|
|
2
|
+
code {
|
|
3
|
+
// Ghostcall is an "initcode program" rather than a normal deployed contract.
|
|
4
|
+
//
|
|
5
|
+
// Mental model:
|
|
6
|
+
// 1. A normal CREATE transaction executes initcode.
|
|
7
|
+
// 2. That initcode usually builds runtime bytecode and RETURNs it.
|
|
8
|
+
// 3. Ghostcall uses the same mechanism, but inside eth_call.
|
|
9
|
+
// 4. Because this is only a simulation, nothing is deployed.
|
|
10
|
+
// 5. Whatever bytes this program RETURNs become the eth_call result.
|
|
11
|
+
//
|
|
12
|
+
// In other words: Ghostcall treats CREATE initcode like a tiny one-shot program that can
|
|
13
|
+
// batch external CALLs and return their raw results.
|
|
14
|
+
//
|
|
15
|
+
// The caller sends one byte blob:
|
|
16
|
+
// <compiled ghostcall initcode><payload>
|
|
17
|
+
//
|
|
18
|
+
// The payload is appended directly after the compiled initcode. It is not normal calldata.
|
|
19
|
+
// This program reads that appended payload back out of its own code using CODECOPY.
|
|
20
|
+
//
|
|
21
|
+
// Payload layout:
|
|
22
|
+
// repeated call entries
|
|
23
|
+
//
|
|
24
|
+
// Each call entry:
|
|
25
|
+
// 2 bytes calldata length (big-endian uint16)
|
|
26
|
+
// 20 bytes target address
|
|
27
|
+
// N bytes calldata
|
|
28
|
+
//
|
|
29
|
+
// Output layout:
|
|
30
|
+
// repeated result entries
|
|
31
|
+
//
|
|
32
|
+
// Each result entry:
|
|
33
|
+
// 2 bytes packed header
|
|
34
|
+
// bit 15 = success flag from CALL
|
|
35
|
+
// bits 0-14 = returndata length (big-endian uint15)
|
|
36
|
+
// N bytes returndata
|
|
37
|
+
//
|
|
38
|
+
// The program does the same high-level loop for every entry:
|
|
39
|
+
// - read the next calldata length + target
|
|
40
|
+
// - copy that call's calldata into memory
|
|
41
|
+
// - execute CALL(target, calldata)
|
|
42
|
+
// - append (success, returndata) to the response buffer
|
|
43
|
+
// - continue until the payload is fully consumed
|
|
44
|
+
//
|
|
45
|
+
// The SDK is expected to validate most caller-facing invariants ahead of time. The checks
|
|
46
|
+
// left in this file exist only to protect parser correctness and response packing.
|
|
47
|
+
|
|
48
|
+
// dataoffset("user_payload_anchor") is the byte offset of the empty data section declared at
|
|
49
|
+
// the bottom of this file. Because that data section is placed after the code, its offset is
|
|
50
|
+
// exactly "the first byte after the compiled initcode". That makes it the start of the
|
|
51
|
+
// caller-appended payload.
|
|
52
|
+
let payloadCursor := dataoffset("user_payload_anchor")
|
|
53
|
+
|
|
54
|
+
// Memory layout used by this program:
|
|
55
|
+
// - 0x00..0x1f: scratch space for reading the current entry header
|
|
56
|
+
// - 0x20..... : output buffer that will become the eth_call return value
|
|
57
|
+
//
|
|
58
|
+
// writePtr always points to "where the next result entry should be written".
|
|
59
|
+
let writePtr := 0x20
|
|
60
|
+
|
|
61
|
+
// Infinite loop with an explicit break once all payload bytes are consumed.
|
|
62
|
+
for {} 1 {} {
|
|
63
|
+
if eq(payloadCursor, codesize()) {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Read the 22-byte fixed-size entry header into scratch memory starting at 0x0a rather
|
|
68
|
+
// than 0x00.
|
|
69
|
+
//
|
|
70
|
+
// Why 0x0a?
|
|
71
|
+
// - the header layout is [len(2)][target(20)]
|
|
72
|
+
// - placing the first header byte at memory offset 10 makes the 20-byte target end
|
|
73
|
+
// exactly at byte 31 of the 32-byte word loaded from mload(0x00)
|
|
74
|
+
// - that means one mload gives us:
|
|
75
|
+
// [10 zero bytes][2-byte len][20-byte target]
|
|
76
|
+
// - so shr(160, headerWord) yields calldata length
|
|
77
|
+
// - and headerWord itself already has the target in the low 20 bytes for CALL
|
|
78
|
+
//
|
|
79
|
+
// CODECOPY pads with zeros if it reads past the end of code. That is why we still need
|
|
80
|
+
// an explicit bounds check later: without it, a truncated entry would silently decode as
|
|
81
|
+
// zeros instead of failing.
|
|
82
|
+
codecopy(0x0a, payloadCursor, 0x16)
|
|
83
|
+
|
|
84
|
+
let headerWord := mload(0x00)
|
|
85
|
+
|
|
86
|
+
// The high 2 non-zero bytes hold the big-endian uint16 calldata length.
|
|
87
|
+
let calldataSize := shr(160, headerWord)
|
|
88
|
+
let nextCursor := add(add(payloadCursor, 0x16), calldataSize)
|
|
89
|
+
|
|
90
|
+
// Reject truncated entries. This single check covers both:
|
|
91
|
+
// - not enough bytes for the 22-byte header
|
|
92
|
+
// - not enough bytes for the calldata that the header claims exists
|
|
93
|
+
if gt(nextCursor, codesize()) {
|
|
94
|
+
revert(0x00, 0x00)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The next result entry will be written at writePtr. Its first 2 bytes are the packed
|
|
98
|
+
// header, so the calldata scratch area can safely start immediately after that header.
|
|
99
|
+
let calldataPtr := add(writePtr, 0x02)
|
|
100
|
+
|
|
101
|
+
// Copy just this call's calldata into memory so CALL can read it.
|
|
102
|
+
codecopy(calldataPtr, add(payloadCursor, 0x16), calldataSize)
|
|
103
|
+
|
|
104
|
+
// Execute the external call with:
|
|
105
|
+
// - all remaining gas
|
|
106
|
+
// - zero ETH value
|
|
107
|
+
// - calldata in memory at calldataPtr
|
|
108
|
+
// - no output buffer yet, because we do not know returndata size in advance
|
|
109
|
+
//
|
|
110
|
+
// CALL only cares about the low 20 bytes of its address argument, so headerWord can be
|
|
111
|
+
// passed directly: the target is already sitting there after the 0x0a codecopy trick.
|
|
112
|
+
let success := call(gas(), headerWord, 0, calldataPtr, calldataSize, 0, 0)
|
|
113
|
+
let returndataSize := returndatasize()
|
|
114
|
+
|
|
115
|
+
// The packed result header has 15 returndata length bits; bit 15 is the success flag.
|
|
116
|
+
// Revert rather than letting oversized returndata collide with the success bit.
|
|
117
|
+
if gt(returndataSize, 0x7fff) {
|
|
118
|
+
revert(0x00, 0x00)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Compute where the next result entry would begin after writing:
|
|
122
|
+
// 2-byte packed header + returndata bytes
|
|
123
|
+
let nextWritePtr := add(add(writePtr, 0x02), returndataSize)
|
|
124
|
+
|
|
125
|
+
// Intentionally do not enforce an aggregate response-size cap here. CREATE-style
|
|
126
|
+
// execution already treats returned bytes as would-be runtime code, so the active
|
|
127
|
+
// chain/client/RPC environment will reject oversized responses according to its own
|
|
128
|
+
// code-size policy. Keeping this uncapped lets the same Ghostcall initcode benefit from
|
|
129
|
+
// networks with larger limits, such as Monad's MIP-2:
|
|
130
|
+
// https://mips.monad.xyz/MIPS/MIP-2
|
|
131
|
+
|
|
132
|
+
// Write the packed 2-byte result header into the high 2 bytes of the 32-byte word at
|
|
133
|
+
// writePtr. The rest of that word does not matter because the return length is computed
|
|
134
|
+
// explicitly at the end.
|
|
135
|
+
mstore(writePtr, shl(240, or(shl(15, success), returndataSize)))
|
|
136
|
+
|
|
137
|
+
// Append the raw returndata bytes immediately after the 2-byte header.
|
|
138
|
+
returndatacopy(add(writePtr, 0x02), 0, returndataSize)
|
|
139
|
+
|
|
140
|
+
// Advance both cursors:
|
|
141
|
+
// - writePtr moves to the start of the next result entry
|
|
142
|
+
// - payloadCursor moves to the next input entry
|
|
143
|
+
writePtr := nextWritePtr
|
|
144
|
+
payloadCursor := nextCursor
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Return exactly the bytes that were written to the response buffer.
|
|
148
|
+
return(0x20, sub(writePtr, 0x20))
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
data "user_payload_anchor" hex""
|
|
153
|
+
}
|