@crewhaus/tool-chainread 0.7.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/README.md +396 -0
- package/dist/fixtures.d.ts +123 -0
- package/dist/fixtures.js +309 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +1302 -0
- package/dist/lib/blocks.d.ts +113 -0
- package/dist/lib/blocks.js +192 -0
- package/dist/lib/clock.d.ts +34 -0
- package/dist/lib/clock.js +55 -0
- package/dist/lib/endpoint.d.ts +91 -0
- package/dist/lib/endpoint.js +320 -0
- package/dist/lib/history.d.ts +196 -0
- package/dist/lib/history.js +663 -0
- package/dist/lib/logs.d.ts +79 -0
- package/dist/lib/logs.js +284 -0
- package/dist/lib/quantity.d.ts +69 -0
- package/dist/lib/quantity.js +120 -0
- package/dist/lib/rpc.d.ts +63 -0
- package/dist/lib/rpc.js +330 -0
- package/dist/lib/transfers.d.ts +134 -0
- package/dist/lib/transfers.js +231 -0
- package/package.json +48 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { ChainReadError, field, hexToBigint, lowerHex, optionalHexToBigint } from "./quantity";
|
|
2
|
+
/** `Transfer(address,address,uint256)` — ERC-20 and ERC-721 share it. */
|
|
3
|
+
export const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
4
|
+
/** `Approval(address,address,uint256)`. */
|
|
5
|
+
export const APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
|
|
6
|
+
/** `ApprovalForAll(address,address,bool)` — the blanket ERC-721/1155 approval. */
|
|
7
|
+
export const APPROVAL_FOR_ALL_TOPIC = "0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31";
|
|
8
|
+
/** `TransferSingle(address,address,address,uint256,uint256)` — ERC-1155. */
|
|
9
|
+
export const TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
|
|
10
|
+
/** The signatures the constants above are the Keccak of, so the test can recompute them. */
|
|
11
|
+
export const TOPIC_SIGNATURES = Object.freeze({
|
|
12
|
+
[TRANSFER_TOPIC]: "Transfer(address,address,uint256)",
|
|
13
|
+
[APPROVAL_TOPIC]: "Approval(address,address,uint256)",
|
|
14
|
+
[APPROVAL_FOR_ALL_TOPIC]: "ApprovalForAll(address,address,bool)",
|
|
15
|
+
[TRANSFER_SINGLE_TOPIC]: "TransferSingle(address,address,address,uint256,uint256)",
|
|
16
|
+
});
|
|
17
|
+
/** The low 20 bytes of a 32-byte topic word, which is how an indexed address is stored. */
|
|
18
|
+
export function topicToAddress(topic) {
|
|
19
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(topic)) {
|
|
20
|
+
throw new ChainReadError(`expected a 32-byte topic, got "${topic}"`);
|
|
21
|
+
}
|
|
22
|
+
return `0x${topic.slice(26).toLowerCase()}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The inverse: an address as the 32-byte topic word a log filter matches on.
|
|
26
|
+
*
|
|
27
|
+
* Left-padded to 32 bytes and lowercased, because `eth_getLogs` compares topics
|
|
28
|
+
* as fixed-width words and an unpadded or mixed-case one matches nothing — a
|
|
29
|
+
* filter that silently returns an empty set, which reads exactly like an
|
|
30
|
+
* address that never moved anything.
|
|
31
|
+
*/
|
|
32
|
+
export function addressToTopic(address) {
|
|
33
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
34
|
+
throw new ChainReadError(`expected a 20-byte address, got "${address}"`);
|
|
35
|
+
}
|
|
36
|
+
return `0x${"0".repeat(24)}${address.slice(2).toLowerCase()}`;
|
|
37
|
+
}
|
|
38
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
39
|
+
/**
|
|
40
|
+
* An ERC-20 transfer and an ERC-721 transfer are the same event with different
|
|
41
|
+
* indexing: the token id is indexed, the amount is not. So three topics means
|
|
42
|
+
* fungible and four means a specific NFT — the only signal there is, and
|
|
43
|
+
* reading a token id as an amount is how an NFT becomes "1e-18 of a token".
|
|
44
|
+
*/
|
|
45
|
+
export function decodeTransfer(log) {
|
|
46
|
+
const [topic0, from, to, third] = log.topics;
|
|
47
|
+
if (topic0 === TRANSFER_TOPIC && from !== undefined && to !== undefined) {
|
|
48
|
+
if (third !== undefined) {
|
|
49
|
+
return {
|
|
50
|
+
standard: "erc721",
|
|
51
|
+
token: log.address,
|
|
52
|
+
from: topicToAddress(from),
|
|
53
|
+
to: topicToAddress(to),
|
|
54
|
+
amount: "1",
|
|
55
|
+
tokenId: BigInt(third).toString(),
|
|
56
|
+
logIndex: log.logIndex,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
standard: "erc20",
|
|
61
|
+
token: log.address,
|
|
62
|
+
from: topicToAddress(from),
|
|
63
|
+
to: topicToAddress(to),
|
|
64
|
+
amount: dataWord(log.data, 0, "Transfer value").toString(),
|
|
65
|
+
logIndex: log.logIndex,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const [, operator, sender, recipient] = log.topics;
|
|
69
|
+
if (topic0 === TRANSFER_SINGLE_TOPIC &&
|
|
70
|
+
operator !== undefined &&
|
|
71
|
+
sender !== undefined &&
|
|
72
|
+
recipient !== undefined) {
|
|
73
|
+
return {
|
|
74
|
+
standard: "erc1155",
|
|
75
|
+
token: log.address,
|
|
76
|
+
from: topicToAddress(sender),
|
|
77
|
+
to: topicToAddress(recipient),
|
|
78
|
+
amount: dataWord(log.data, 1, "TransferSingle value").toString(),
|
|
79
|
+
tokenId: dataWord(log.data, 0, "TransferSingle id").toString(),
|
|
80
|
+
logIndex: log.logIndex,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export function decodeApproval(log) {
|
|
86
|
+
const [topic0, owner, spender, third] = log.topics;
|
|
87
|
+
if (owner === undefined || spender === undefined)
|
|
88
|
+
return null;
|
|
89
|
+
if (topic0 === APPROVAL_TOPIC && third === undefined) {
|
|
90
|
+
const amount = dataWord(log.data, 0, "Approval value");
|
|
91
|
+
return {
|
|
92
|
+
kind: "approval",
|
|
93
|
+
token: log.address,
|
|
94
|
+
owner: topicToAddress(owner),
|
|
95
|
+
spender: topicToAddress(spender),
|
|
96
|
+
amount: amount.toString(),
|
|
97
|
+
// The infinite-allowance pattern. Worth flagging on its own: it is the
|
|
98
|
+
// difference between "approved 100 USDC" and "approved the balance,
|
|
99
|
+
// forever".
|
|
100
|
+
unlimited: amount === MAX_UINT256,
|
|
101
|
+
logIndex: log.logIndex,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (topic0 === APPROVAL_FOR_ALL_TOPIC) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "approvalForAll",
|
|
107
|
+
token: log.address,
|
|
108
|
+
owner: topicToAddress(owner),
|
|
109
|
+
spender: topicToAddress(spender),
|
|
110
|
+
approved: dataWord(log.data, 0, "ApprovalForAll approved") !== 0n,
|
|
111
|
+
logIndex: log.logIndex,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function dataWord(data, index, what) {
|
|
117
|
+
const body = data.startsWith("0x") ? data.slice(2) : data;
|
|
118
|
+
const start = index * 64;
|
|
119
|
+
const word = body.slice(start, start + 64);
|
|
120
|
+
if (word.length < 64) {
|
|
121
|
+
throw new ChainReadError(`${what}: the log's data has ${body.length / 2} bytes, too few to hold word ${index} — this is not the event it claims to be`);
|
|
122
|
+
}
|
|
123
|
+
return BigInt(`0x${word}`);
|
|
124
|
+
}
|
|
125
|
+
export function projectTransaction(raw) {
|
|
126
|
+
const input = typeof raw["input"] === "string" ? lowerHex(raw["input"], "tx input") : "0x";
|
|
127
|
+
const to = typeof raw["to"] === "string" ? lowerHex(raw["to"], "tx to") : null;
|
|
128
|
+
const blockNumber = optionalHexToBigint(raw["blockNumber"], "tx blockNumber");
|
|
129
|
+
return {
|
|
130
|
+
hash: lowerHex(raw["hash"], "tx hash"),
|
|
131
|
+
from: lowerHex(raw["from"], "tx from"),
|
|
132
|
+
to,
|
|
133
|
+
value: hexToBigint(raw["value"], "tx value").toString(),
|
|
134
|
+
nonce: hexToBigint(raw["nonce"], "tx nonce").toString(),
|
|
135
|
+
input,
|
|
136
|
+
// Four bytes is a call; anything shorter is a plain transfer or a
|
|
137
|
+
// contract creation, and slicing it anyway would invent a selector.
|
|
138
|
+
selector: input.length >= 10 ? input.slice(0, 10) : null,
|
|
139
|
+
gas: optionalHexToBigint(raw["gas"], "tx gas")?.toString() ?? null,
|
|
140
|
+
gasPrice: optionalHexToBigint(raw["gasPrice"], "tx gasPrice")?.toString() ?? null,
|
|
141
|
+
maxFeePerGas: optionalHexToBigint(raw["maxFeePerGas"], "maxFeePerGas")?.toString() ?? null,
|
|
142
|
+
maxPriorityFeePerGas: optionalHexToBigint(raw["maxPriorityFeePerGas"], "maxPriorityFeePerGas")?.toString() ?? null,
|
|
143
|
+
type: typeof raw["type"] === "string" ? raw["type"] : null,
|
|
144
|
+
blockNumber: blockNumber === null ? null : blockNumber.toString(),
|
|
145
|
+
blockHash: typeof raw["blockHash"] === "string" ? lowerHex(raw["blockHash"], "blockHash") : null,
|
|
146
|
+
contractCreation: to === null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function projectReceipt(raw, projectLog) {
|
|
150
|
+
const status = raw["status"];
|
|
151
|
+
const logs = Array.isArray(raw["logs"]) ? raw["logs"].map(projectLog) : [];
|
|
152
|
+
const decided = status === "0x1" || status === "0x01"
|
|
153
|
+
? "success"
|
|
154
|
+
: status === "0x0" || status === "0x00"
|
|
155
|
+
? "reverted"
|
|
156
|
+
: "unknown";
|
|
157
|
+
return {
|
|
158
|
+
status: decided,
|
|
159
|
+
...(decided === "unknown"
|
|
160
|
+
? {
|
|
161
|
+
statusReason: raw["root"] === undefined
|
|
162
|
+
? `the receipt carries no status field (got ${JSON.stringify(status)})`
|
|
163
|
+
: "this is a pre-Byzantium receipt: it carries a state root instead of a status, so whether the call reverted is not recorded",
|
|
164
|
+
}
|
|
165
|
+
: {}),
|
|
166
|
+
blockNumber: hexToBigint(raw["blockNumber"], "receipt blockNumber").toString(),
|
|
167
|
+
blockHash: lowerHex(raw["blockHash"], "receipt blockHash"),
|
|
168
|
+
transactionIndex: raw["transactionIndex"] === undefined
|
|
169
|
+
? null
|
|
170
|
+
: Number(hexToBigint(raw["transactionIndex"], "receipt transactionIndex")),
|
|
171
|
+
gasUsed: hexToBigint(raw["gasUsed"], "receipt gasUsed").toString(),
|
|
172
|
+
effectiveGasPrice: optionalHexToBigint(raw["effectiveGasPrice"], "effectiveGasPrice")?.toString() ?? null,
|
|
173
|
+
contractAddress: typeof raw["contractAddress"] === "string"
|
|
174
|
+
? lowerHex(raw["contractAddress"], "contractAddress")
|
|
175
|
+
: null,
|
|
176
|
+
logs,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* `gasUsed * effectiveGasPrice` is the whole fee on L1 and is WRONG on a
|
|
181
|
+
* rollup, in two different directions.
|
|
182
|
+
*
|
|
183
|
+
* OP-stack chains charge a separate L1 data fee that appears as `l1Fee` on the
|
|
184
|
+
* receipt and is not included in `gasUsed`; leaving it out understates the cost
|
|
185
|
+
* of a Base or Optimism transaction by most of the cost. Arbitrum folds its L1
|
|
186
|
+
* component INTO `gasUsed` and reports the share as `gasUsedForL1`, so the
|
|
187
|
+
* simple product is right there and adding anything would double-count.
|
|
188
|
+
*
|
|
189
|
+
* Both are detected from fields the receipt either has or does not, rather than
|
|
190
|
+
* from a chain id table that goes stale every time a new rollup ships.
|
|
191
|
+
*/
|
|
192
|
+
export function feeBreakdown(receipt, view) {
|
|
193
|
+
const gasUsed = BigInt(view.gasUsed);
|
|
194
|
+
const price = view.effectiveGasPrice === null ? null : BigInt(view.effectiveGasPrice);
|
|
195
|
+
const execution = price === null ? null : gasUsed * price;
|
|
196
|
+
const l1Fee = optionalHexToBigint(field(receipt, "l1Fee"), "l1Fee");
|
|
197
|
+
const gasUsedForL1 = optionalHexToBigint(field(receipt, "gasUsedForL1"), "gasUsedForL1");
|
|
198
|
+
if (l1Fee !== null) {
|
|
199
|
+
return {
|
|
200
|
+
model: "op-stack",
|
|
201
|
+
gasUsed: view.gasUsed,
|
|
202
|
+
effectiveGasPrice: view.effectiveGasPrice,
|
|
203
|
+
executionFeeWei: execution?.toString() ?? null,
|
|
204
|
+
l1FeeWei: l1Fee.toString(),
|
|
205
|
+
totalFeeWei: execution === null ? null : (execution + l1Fee).toString(),
|
|
206
|
+
note: "OP-stack: the L1 data fee is charged separately from gas, so the total is execution + l1Fee",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (gasUsedForL1 !== null) {
|
|
210
|
+
return {
|
|
211
|
+
model: "arbitrum",
|
|
212
|
+
gasUsed: view.gasUsed,
|
|
213
|
+
effectiveGasPrice: view.effectiveGasPrice,
|
|
214
|
+
executionFeeWei: execution?.toString() ?? null,
|
|
215
|
+
l1FeeWei: null,
|
|
216
|
+
totalFeeWei: execution?.toString() ?? null,
|
|
217
|
+
note: `Arbitrum: gasUsed already includes the ${gasUsedForL1} gas charged for L1 data, so the total is the single product`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
model: price === null ? "unknown" : "eip1559",
|
|
222
|
+
gasUsed: view.gasUsed,
|
|
223
|
+
effectiveGasPrice: view.effectiveGasPrice,
|
|
224
|
+
executionFeeWei: execution?.toString() ?? null,
|
|
225
|
+
l1FeeWei: null,
|
|
226
|
+
totalFeeWei: execution?.toString() ?? null,
|
|
227
|
+
note: price === null
|
|
228
|
+
? "this receipt carries no effectiveGasPrice, so the fee cannot be computed from it"
|
|
229
|
+
: "gasUsed * effectiveGasPrice; no rollup fee fields were present on this receipt",
|
|
230
|
+
};
|
|
231
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-chainread",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deterministic EVM chain reads over a public RPC: blocks, transactions, receipts, logs, nonces, and an address's history as reconciler rows",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/chain-adapter-base": "0.7.0",
|
|
19
|
+
"@crewhaus/errors": "0.7.0",
|
|
20
|
+
"@crewhaus/tool-builder": "0.7.0",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.7.0",
|
|
22
|
+
"@crewhaus/tool-money": "0.7.0",
|
|
23
|
+
"@crewhaus/tool-onchain": "0.7.0",
|
|
24
|
+
"zod": "^3.23.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@crewhaus/tool-executor": "workspace:*"
|
|
28
|
+
},
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Max Meier",
|
|
32
|
+
"email": "max@crewhaus.ai",
|
|
33
|
+
"url": "https://crewhaus.ai"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
38
|
+
"directory": "packages/tool-chainread"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-chainread#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
48
|
+
}
|