@feelyourprotocol/util 8141.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +297 -0
  3. package/dist/cjs/account.d.ts +165 -0
  4. package/dist/cjs/account.d.ts.map +1 -0
  5. package/dist/cjs/account.js +530 -0
  6. package/dist/cjs/account.js.map +1 -0
  7. package/dist/cjs/address.d.ts +67 -0
  8. package/dist/cjs/address.d.ts.map +1 -0
  9. package/dist/cjs/address.js +136 -0
  10. package/dist/cjs/address.js.map +1 -0
  11. package/dist/cjs/authorization.d.ts +41 -0
  12. package/dist/cjs/authorization.d.ts.map +1 -0
  13. package/dist/cjs/authorization.js +135 -0
  14. package/dist/cjs/authorization.js.map +1 -0
  15. package/dist/cjs/bal.d.ts +129 -0
  16. package/dist/cjs/bal.d.ts.map +1 -0
  17. package/dist/cjs/bal.js +529 -0
  18. package/dist/cjs/bal.js.map +1 -0
  19. package/dist/cjs/binaryTree.d.ts +148 -0
  20. package/dist/cjs/binaryTree.d.ts.map +1 -0
  21. package/dist/cjs/binaryTree.js +240 -0
  22. package/dist/cjs/binaryTree.js.map +1 -0
  23. package/dist/cjs/blobs.d.ts +76 -0
  24. package/dist/cjs/blobs.d.ts.map +1 -0
  25. package/dist/cjs/blobs.js +175 -0
  26. package/dist/cjs/blobs.js.map +1 -0
  27. package/dist/cjs/bytes.d.ts +291 -0
  28. package/dist/cjs/bytes.d.ts.map +1 -0
  29. package/dist/cjs/bytes.js +606 -0
  30. package/dist/cjs/bytes.js.map +1 -0
  31. package/dist/cjs/constants.d.ts +91 -0
  32. package/dist/cjs/constants.d.ts.map +1 -0
  33. package/dist/cjs/constants.js +97 -0
  34. package/dist/cjs/constants.js.map +1 -0
  35. package/dist/cjs/db.d.ts +65 -0
  36. package/dist/cjs/db.d.ts.map +1 -0
  37. package/dist/cjs/db.js +14 -0
  38. package/dist/cjs/db.js.map +1 -0
  39. package/dist/cjs/env.d.ts +9 -0
  40. package/dist/cjs/env.d.ts.map +1 -0
  41. package/dist/cjs/env.js +13 -0
  42. package/dist/cjs/env.js.map +1 -0
  43. package/dist/cjs/errors.d.ts +3 -0
  44. package/dist/cjs/errors.d.ts.map +1 -0
  45. package/dist/cjs/errors.js +19 -0
  46. package/dist/cjs/errors.js.map +1 -0
  47. package/dist/cjs/helpers.d.ts +21 -0
  48. package/dist/cjs/helpers.d.ts.map +1 -0
  49. package/dist/cjs/helpers.js +50 -0
  50. package/dist/cjs/helpers.js.map +1 -0
  51. package/dist/cjs/index.d.ts +67 -0
  52. package/dist/cjs/index.d.ts.map +1 -0
  53. package/dist/cjs/index.js +93 -0
  54. package/dist/cjs/index.js.map +1 -0
  55. package/dist/cjs/internal.d.ts +72 -0
  56. package/dist/cjs/internal.d.ts.map +1 -0
  57. package/dist/cjs/internal.js +182 -0
  58. package/dist/cjs/internal.js.map +1 -0
  59. package/dist/cjs/kzg.d.ts +14 -0
  60. package/dist/cjs/kzg.d.ts.map +1 -0
  61. package/dist/cjs/kzg.js +3 -0
  62. package/dist/cjs/kzg.js.map +1 -0
  63. package/dist/cjs/lock.d.ts +15 -0
  64. package/dist/cjs/lock.d.ts.map +1 -0
  65. package/dist/cjs/lock.js +45 -0
  66. package/dist/cjs/lock.js.map +1 -0
  67. package/dist/cjs/mapDB.d.ts +17 -0
  68. package/dist/cjs/mapDB.d.ts.map +1 -0
  69. package/dist/cjs/mapDB.js +46 -0
  70. package/dist/cjs/mapDB.js.map +1 -0
  71. package/dist/cjs/package.json +3 -0
  72. package/dist/cjs/provider.d.ts +46 -0
  73. package/dist/cjs/provider.d.ts.map +1 -0
  74. package/dist/cjs/provider.js +84 -0
  75. package/dist/cjs/provider.js.map +1 -0
  76. package/dist/cjs/request.d.ts +20 -0
  77. package/dist/cjs/request.d.ts.map +1 -0
  78. package/dist/cjs/request.js +35 -0
  79. package/dist/cjs/request.js.map +1 -0
  80. package/dist/cjs/signature.d.ts +47 -0
  81. package/dist/cjs/signature.d.ts.map +1 -0
  82. package/dist/cjs/signature.js +147 -0
  83. package/dist/cjs/signature.js.map +1 -0
  84. package/dist/cjs/tasks.d.ts +32 -0
  85. package/dist/cjs/tasks.d.ts.map +1 -0
  86. package/dist/cjs/tasks.js +51 -0
  87. package/dist/cjs/tasks.js.map +1 -0
  88. package/dist/cjs/types.d.ts +64 -0
  89. package/dist/cjs/types.d.ts.map +1 -0
  90. package/dist/cjs/types.js +78 -0
  91. package/dist/cjs/types.js.map +1 -0
  92. package/dist/cjs/units.d.ts +22 -0
  93. package/dist/cjs/units.d.ts.map +1 -0
  94. package/dist/cjs/units.js +51 -0
  95. package/dist/cjs/units.js.map +1 -0
  96. package/dist/cjs/withdrawal.d.ts +72 -0
  97. package/dist/cjs/withdrawal.d.ts.map +1 -0
  98. package/dist/cjs/withdrawal.js +93 -0
  99. package/dist/cjs/withdrawal.js.map +1 -0
  100. package/dist/esm/account.d.ts +165 -0
  101. package/dist/esm/account.d.ts.map +1 -0
  102. package/dist/esm/account.js +505 -0
  103. package/dist/esm/account.js.map +1 -0
  104. package/dist/esm/address.d.ts +67 -0
  105. package/dist/esm/address.d.ts.map +1 -0
  106. package/dist/esm/address.js +125 -0
  107. package/dist/esm/address.js.map +1 -0
  108. package/dist/esm/authorization.d.ts +41 -0
  109. package/dist/esm/authorization.d.ts.map +1 -0
  110. package/dist/esm/authorization.js +126 -0
  111. package/dist/esm/authorization.js.map +1 -0
  112. package/dist/esm/bal.d.ts +129 -0
  113. package/dist/esm/bal.d.ts.map +1 -0
  114. package/dist/esm/bal.js +522 -0
  115. package/dist/esm/bal.js.map +1 -0
  116. package/dist/esm/binaryTree.d.ts +148 -0
  117. package/dist/esm/binaryTree.d.ts.map +1 -0
  118. package/dist/esm/binaryTree.js +226 -0
  119. package/dist/esm/binaryTree.js.map +1 -0
  120. package/dist/esm/blobs.d.ts +76 -0
  121. package/dist/esm/blobs.d.ts.map +1 -0
  122. package/dist/esm/blobs.js +163 -0
  123. package/dist/esm/blobs.js.map +1 -0
  124. package/dist/esm/bytes.d.ts +291 -0
  125. package/dist/esm/bytes.d.ts.map +1 -0
  126. package/dist/esm/bytes.js +562 -0
  127. package/dist/esm/bytes.js.map +1 -0
  128. package/dist/esm/constants.d.ts +91 -0
  129. package/dist/esm/constants.d.ts.map +1 -0
  130. package/dist/esm/constants.js +94 -0
  131. package/dist/esm/constants.js.map +1 -0
  132. package/dist/esm/db.d.ts +65 -0
  133. package/dist/esm/db.d.ts.map +1 -0
  134. package/dist/esm/db.js +11 -0
  135. package/dist/esm/db.js.map +1 -0
  136. package/dist/esm/env.d.ts +9 -0
  137. package/dist/esm/env.d.ts.map +1 -0
  138. package/dist/esm/env.js +9 -0
  139. package/dist/esm/env.js.map +1 -0
  140. package/dist/esm/errors.d.ts +3 -0
  141. package/dist/esm/errors.d.ts.map +1 -0
  142. package/dist/esm/errors.js +14 -0
  143. package/dist/esm/errors.js.map +1 -0
  144. package/dist/esm/helpers.d.ts +21 -0
  145. package/dist/esm/helpers.d.ts.map +1 -0
  146. package/dist/esm/helpers.js +43 -0
  147. package/dist/esm/helpers.js.map +1 -0
  148. package/dist/esm/index.d.ts +67 -0
  149. package/dist/esm/index.d.ts.map +1 -0
  150. package/dist/esm/index.js +67 -0
  151. package/dist/esm/index.js.map +1 -0
  152. package/dist/esm/internal.d.ts +72 -0
  153. package/dist/esm/internal.d.ts.map +1 -0
  154. package/dist/esm/internal.js +170 -0
  155. package/dist/esm/internal.js.map +1 -0
  156. package/dist/esm/kzg.d.ts +14 -0
  157. package/dist/esm/kzg.d.ts.map +1 -0
  158. package/dist/esm/kzg.js +2 -0
  159. package/dist/esm/kzg.js.map +1 -0
  160. package/dist/esm/lock.d.ts +15 -0
  161. package/dist/esm/lock.d.ts.map +1 -0
  162. package/dist/esm/lock.js +41 -0
  163. package/dist/esm/lock.js.map +1 -0
  164. package/dist/esm/mapDB.d.ts +17 -0
  165. package/dist/esm/mapDB.d.ts.map +1 -0
  166. package/dist/esm/mapDB.js +42 -0
  167. package/dist/esm/mapDB.js.map +1 -0
  168. package/dist/esm/package.json +3 -0
  169. package/dist/esm/provider.d.ts +46 -0
  170. package/dist/esm/provider.d.ts.map +1 -0
  171. package/dist/esm/provider.js +79 -0
  172. package/dist/esm/provider.js.map +1 -0
  173. package/dist/esm/request.d.ts +20 -0
  174. package/dist/esm/request.d.ts.map +1 -0
  175. package/dist/esm/request.js +30 -0
  176. package/dist/esm/request.js.map +1 -0
  177. package/dist/esm/signature.d.ts +47 -0
  178. package/dist/esm/signature.d.ts.map +1 -0
  179. package/dist/esm/signature.js +137 -0
  180. package/dist/esm/signature.js.map +1 -0
  181. package/dist/esm/tasks.d.ts +32 -0
  182. package/dist/esm/tasks.d.ts.map +1 -0
  183. package/dist/esm/tasks.js +47 -0
  184. package/dist/esm/tasks.js.map +1 -0
  185. package/dist/esm/types.d.ts +64 -0
  186. package/dist/esm/types.d.ts.map +1 -0
  187. package/dist/esm/types.js +71 -0
  188. package/dist/esm/types.js.map +1 -0
  189. package/dist/esm/units.d.ts +22 -0
  190. package/dist/esm/units.d.ts.map +1 -0
  191. package/dist/esm/units.js +46 -0
  192. package/dist/esm/units.js.map +1 -0
  193. package/dist/esm/withdrawal.d.ts +72 -0
  194. package/dist/esm/withdrawal.d.ts.map +1 -0
  195. package/dist/esm/withdrawal.js +86 -0
  196. package/dist/esm/withdrawal.js.map +1 -0
  197. package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
  198. package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
  199. package/package.json +116 -0
  200. package/src/account.ts +630 -0
  201. package/src/address.ts +158 -0
  202. package/src/authorization.ts +180 -0
  203. package/src/bal.ts +761 -0
  204. package/src/binaryTree.ts +353 -0
  205. package/src/blobs.ts +209 -0
  206. package/src/bytes.ts +659 -0
  207. package/src/constants.ts +125 -0
  208. package/src/db.ts +86 -0
  209. package/src/env.ts +9 -0
  210. package/src/errors.ts +28 -0
  211. package/src/helpers.ts +46 -0
  212. package/src/index.ts +88 -0
  213. package/src/internal.ts +212 -0
  214. package/src/kzg.ts +24 -0
  215. package/src/lock.ts +42 -0
  216. package/src/mapDB.ts +57 -0
  217. package/src/provider.ts +109 -0
  218. package/src/request.ts +48 -0
  219. package/src/signature.ts +202 -0
  220. package/src/tasks.ts +59 -0
  221. package/src/types.ts +177 -0
  222. package/src/units.ts +56 -0
  223. package/src/withdrawal.ts +133 -0
package/src/bal.ts ADDED
@@ -0,0 +1,761 @@
1
+ import { RLP } from '@feelyourprotocol/rlp'
2
+ import { keccak_256 } from '@noble/hashes/sha3.js'
3
+ import {
4
+ bigIntToBytes,
5
+ bigIntToHex,
6
+ bytesToHex,
7
+ bytesToInt,
8
+ hexToBigInt,
9
+ hexToBytes,
10
+ } from './bytes.ts'
11
+ import { SYSTEM_ADDRESS } from './constants.ts'
12
+ import { padToEven } from './internal.ts'
13
+ import type { PrefixedHexString } from './types.ts'
14
+
15
+ // Base types which can be used for JSON, internal representation and raw format.
16
+ type BALAddressHex = PrefixedHexString // bytes20
17
+ type BALStorageKeyBytes = Uint8Array // uint256
18
+ type BALStorageKeyHex = PrefixedHexString // uint256
19
+ type BALStorageValueBytes = Uint8Array // uint256
20
+ type BALStorageValueHex = PrefixedHexString // uint256 as hex
21
+ type BALAccessIndexNumber = number // uint16
22
+ type BALAccessIndexHex = PrefixedHexString // uint16 as hex d
23
+ type BALBalanceBigInt = bigint // uint256 as bigint
24
+ type BALBalanceHex = PrefixedHexString // uint256 as hex
25
+ type BALNonceBigInt = bigint // uint64 as bigint
26
+ type BALNonceHex = PrefixedHexString // uint64 as hex
27
+ type BALByteCodeBytes = Uint8Array // bytes
28
+ type BALByteCodeHex = PrefixedHexString // bytes as hex
29
+
30
+ // Change types which can be used for internal representation and raw format.
31
+ type BALRawStorageChange = [BALAccessIndexNumber, BALStorageValueBytes]
32
+ type BALRawBalanceChange = [BALAccessIndexNumber, BALBalanceHex]
33
+ type BALRawNonceChange = [BALAccessIndexNumber, BALNonceHex]
34
+ type BALRawCodeChange = [BALAccessIndexNumber, BALByteCodeBytes]
35
+ type BALRawSlotChanges = [BALStorageKeyHex, BALRawStorageChange[]]
36
+
37
+ // Core data format for the raw format.
38
+ type BALRawAccountChanges = [
39
+ BALAddressHex,
40
+ BALRawSlotChanges[],
41
+ BALStorageKeyHex[],
42
+ BALRawBalanceChange[],
43
+ BALRawNonceChange[],
44
+ BALRawCodeChange[],
45
+ ]
46
+ type BALRawBlockAccessList = BALRawAccountChanges[]
47
+
48
+ // Internal representation of the access list.
49
+ export type Accesses = Record<
50
+ BALAddressHex,
51
+ {
52
+ nonceChanges: Map<BALAccessIndexNumber, BALNonceHex>
53
+ balanceChanges: Map<BALAccessIndexNumber, BALBalanceHex>
54
+ codeChanges: BALRawCodeChange[]
55
+ storageChanges: Record<BALStorageKeyHex, BALRawStorageChange[]>
56
+ storageReads: Set<BALStorageKeyHex>
57
+ }
58
+ >
59
+
60
+ // JSON representation types (all numeric values as hex strings for JSON serialization)
61
+ // JSON change types
62
+ interface BALJSONBalanceChange {
63
+ blockAccessIndex: BALAccessIndexHex
64
+ postBalance: BALBalanceHex
65
+ }
66
+
67
+ interface BALJSONNonceChange {
68
+ blockAccessIndex: BALAccessIndexHex
69
+ postNonce: BALNonceHex
70
+ }
71
+
72
+ interface BALJSONCodeChange {
73
+ blockAccessIndex: BALAccessIndexHex
74
+ newCode: BALByteCodeHex
75
+ }
76
+
77
+ interface BALJSONStorageChange {
78
+ blockAccessIndex: BALAccessIndexHex
79
+ postValue: BALStorageValueHex
80
+ }
81
+
82
+ interface BALJSONSlotChanges {
83
+ slot: BALStorageKeyHex
84
+ slotChanges: BALJSONStorageChange[]
85
+ }
86
+
87
+ // JSON representation of account changes
88
+ interface BALJSONAccountChanges {
89
+ address: BALAddressHex
90
+ balanceChanges: BALJSONBalanceChange[]
91
+ nonceChanges: BALJSONNonceChange[]
92
+ codeChanges: BALJSONCodeChange[]
93
+ storageChanges: BALJSONSlotChanges[]
94
+ storageReads: BALStorageKeyHex[]
95
+ }
96
+
97
+ // Top level JSON type
98
+ export type BALJSONBlockAccessList = BALJSONAccountChanges[]
99
+
100
+ // Re-export JSON types for external use
101
+ export type {
102
+ BALJSONAccountChanges,
103
+ BALJSONStorageChange,
104
+ BALJSONSlotChanges,
105
+ BALJSONBalanceChange,
106
+ BALJSONNonceChange,
107
+ BALJSONCodeChange,
108
+ }
109
+
110
+ /**
111
+ * Structural helper class for block level access lists
112
+ *
113
+ * EXPERIMENTAL: DO NOT USE IN PRODUCTION!
114
+ */
115
+ export class BlockLevelAccessList {
116
+ public accesses: Accesses
117
+ public blockAccessIndex: number
118
+ private checkpoints: { accesses: Accesses; blockAccessIndex: number }[] = []
119
+ // Track original (pre-transaction) balances for net-zero detection
120
+ private originalBalances: Map<BALAddressHex, bigint> = new Map()
121
+ // Track original code at the start of each blockAccessIndex for each address
122
+ // Key format: `${address}-${blockAccessIndex}`
123
+ private originalCodesAtIndex: Map<string, Uint8Array> = new Map()
124
+ constructor(accesses: Accesses = {}) {
125
+ this.accesses = accesses
126
+ this.blockAccessIndex = 0
127
+ }
128
+
129
+ /**
130
+ * Serializes the block level access list to RLP.
131
+ *
132
+ * @returns the RLP encoded block level access list
133
+ */
134
+ public serialize(): Uint8Array {
135
+ return RLP.encode(this.raw())
136
+ }
137
+
138
+ /**
139
+ * This hash is used in the block header
140
+ *
141
+ * @returns the hash of the serialized block level access list
142
+ */
143
+ public hash(): Uint8Array {
144
+ return keccak_256(this.serialize())
145
+ }
146
+
147
+ public checkpoint(): void {
148
+ this.checkpoints.push({
149
+ accesses: this.cloneAccesses(this.accesses),
150
+ blockAccessIndex: this.blockAccessIndex,
151
+ })
152
+ }
153
+
154
+ public commit(): void {
155
+ if (this.checkpoints.length > 0) {
156
+ this.checkpoints.pop()
157
+ }
158
+ }
159
+
160
+ public revert(): void {
161
+ const snapshot = this.checkpoints.pop()
162
+ if (!snapshot) {
163
+ return
164
+ }
165
+ const current = this.accesses
166
+ this.accesses = snapshot.accesses
167
+ this.blockAccessIndex = snapshot.blockAccessIndex
168
+
169
+ // Preserve address touches and storage reads across reverts.
170
+ // EIP-7928: When storage writes are reverted, the slot keys MUST still
171
+ // appear in storageReads since the slots were accessed (SSTORE reads
172
+ // the current value for gas calculation).
173
+ for (const [address, access] of Object.entries(current)) {
174
+ if (this.accesses[address as BALAddressHex] === undefined) {
175
+ // Collect both explicit reads and slots that were written (but will be reverted)
176
+ const allReads = new Set(access.storageReads)
177
+ for (const slot of Object.keys(access.storageChanges)) {
178
+ allReads.add(slot as BALStorageKeyHex)
179
+ }
180
+ this.accesses[address as BALAddressHex] = {
181
+ nonceChanges: new Map(),
182
+ balanceChanges: new Map(),
183
+ codeChanges: [],
184
+ storageChanges: {},
185
+ storageReads: allReads,
186
+ }
187
+ continue
188
+ }
189
+ const target = this.accesses[address as BALAddressHex]
190
+ // Preserve explicit storageReads
191
+ for (const slot of access.storageReads) {
192
+ target.storageReads.add(slot)
193
+ }
194
+ // EIP-7928: Convert reverted storageChanges to storageReads
195
+ for (const slot of Object.keys(access.storageChanges)) {
196
+ // Only add to reads if not already in the target's storageChanges
197
+ // (a successful write subsumes a read)
198
+ if (target.storageChanges[slot as BALStorageKeyHex] === undefined) {
199
+ target.storageReads.add(slot as BALStorageKeyHex)
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ private cloneAccesses(accesses: Accesses): Accesses {
206
+ const cloned: Accesses = {}
207
+ for (const [address, access] of Object.entries(accesses)) {
208
+ const storageChanges: Record<BALStorageKeyHex, BALRawStorageChange[]> = {}
209
+ for (const [slot, changes] of Object.entries(access.storageChanges)) {
210
+ storageChanges[slot as BALStorageKeyHex] = changes.map(
211
+ ([index, value]) => [index, value] as BALRawStorageChange,
212
+ )
213
+ }
214
+ cloned[address as BALAddressHex] = {
215
+ nonceChanges: new Map(access.nonceChanges),
216
+ balanceChanges: new Map(access.balanceChanges),
217
+ codeChanges: access.codeChanges.map(([index, code]) => [index, code] as BALRawCodeChange),
218
+ storageChanges,
219
+ storageReads: new Set(access.storageReads),
220
+ }
221
+ }
222
+ return cloned
223
+ }
224
+
225
+ /**
226
+ * Returns the raw block level access list with values
227
+ * correctly sorted.
228
+ *
229
+ * @returns the raw block level access list
230
+ */
231
+ public raw(): BALRawBlockAccessList {
232
+ const bal: BALRawBlockAccessList = []
233
+
234
+ for (const address of Object.keys(this.accesses)
235
+ .sort()
236
+ .filter((address) =>
237
+ shouldIncludeAddress(address as BALAddressHex, this.accesses[address as BALAddressHex]),
238
+ )) {
239
+ const data = this.accesses[address as BALAddressHex]
240
+
241
+ // Format storage changes: [slot, [[index, value], ...]]
242
+ // Normalize slot keys for canonical RLP encoding (0 -> empty bytes)
243
+ const storageChanges = (
244
+ Object.entries(data.storageChanges) as [BALStorageKeyHex, BALRawStorageChange[]][]
245
+ )
246
+ .sort((a, b) => compareLexicographicHexOrBytes(a[0], b[0]))
247
+ .map(([slot, changes]) => [
248
+ normalizeHexForRLP(slot),
249
+ changes
250
+ .sort((a, b) => a[0] - b[0])
251
+ .map(
252
+ ([index, value]) =>
253
+ [index, normalizeBytesForRLPQuantity(value)] as BALRawStorageChange,
254
+ ),
255
+ ])
256
+
257
+ // Normalize storage reads for canonical RLP encoding (0 -> empty bytes)
258
+ const storageReads = Array.from(data.storageReads)
259
+ .map(normalizeHexForRLP)
260
+ .sort((a, b) => compareLexicographicHexOrBytes(a, b))
261
+
262
+ const balanceChanges = Array.from(data.balanceChanges.entries())
263
+ .sort(([a], [b]) => a - b)
264
+ .map(
265
+ ([index, balance]) => [index, normalizeQuantityHexForRLP(balance)] as BALRawBalanceChange,
266
+ )
267
+ const nonceChanges = Array.from(data.nonceChanges.entries())
268
+ .sort(([a], [b]) => a - b)
269
+ .map(([index, nonce]) => [index, normalizeQuantityHexForRLP(nonce)] as BALRawNonceChange)
270
+ const codeChanges = [...data.codeChanges].sort(([a], [b]) => a - b)
271
+ bal.push([
272
+ address as BALAddressHex,
273
+ storageChanges,
274
+ storageReads,
275
+ balanceChanges,
276
+ nonceChanges,
277
+ codeChanges,
278
+ ] as BALRawAccountChanges)
279
+ }
280
+
281
+ return bal
282
+ }
283
+
284
+ public addAddress(address: BALAddressHex): void {
285
+ if (this.accesses[address] !== undefined) {
286
+ return
287
+ }
288
+ this.accesses[address] = {
289
+ storageChanges: {},
290
+ storageReads: new Set(),
291
+ balanceChanges: new Map(),
292
+ nonceChanges: new Map(),
293
+ codeChanges: [],
294
+ }
295
+ }
296
+
297
+ public addStorageWrite(
298
+ address: BALAddressHex,
299
+ storageKey: BALStorageKeyBytes,
300
+ value: BALStorageValueBytes,
301
+ blockAccessIndex: BALAccessIndexNumber,
302
+ originalValue?: BALStorageValueBytes,
303
+ ): void {
304
+ const strippedKey = normalizeStorageKeyHex(bytesToHex(stripLeadingZeros(storageKey)))
305
+ const strippedValue = stripLeadingZeros(value)
306
+ const strippedOriginal = originalValue ? stripLeadingZeros(originalValue) : undefined
307
+ const isZeroWrite = strippedValue.length === 0
308
+
309
+ // EIP-7928: Check if this is a no-op write (value equals pre-transaction value)
310
+ // No-op writes should be recorded as reads, not changes.
311
+ // Note: Both empty arrays (zero values) compare equal via bytesToHex
312
+ let isNoOp = false
313
+ if (strippedOriginal !== undefined) {
314
+ // We have original value - compare properly
315
+ isNoOp = bytesToHex(strippedValue) === bytesToHex(strippedOriginal)
316
+ } else if (isZeroWrite) {
317
+ // No original value provided and writing zero - likely a no-op for system contracts
318
+ // reading empty slots. Treat as read for safety.
319
+ isNoOp = true
320
+ }
321
+
322
+ // Only no-op writes (writing same value as original) are treated as reads
323
+ // EIP-7928: Zeroing a slot (pre-value exists, post-value is zero) IS a write
324
+ if (isNoOp) {
325
+ // EIP-7928: If a slot is written back to its original value (net-zero change),
326
+ // it should appear in storageReads, not storageChanges.
327
+ // This handles nested calls where intermediate frames write different values
328
+ // but the final value equals the original.
329
+ if (this.accesses[address] !== undefined) {
330
+ // Remove any existing storageChanges for this slot since final == original
331
+ delete this.accesses[address].storageChanges[strippedKey]
332
+ }
333
+ this.addStorageRead(address, storageKey)
334
+ return
335
+ }
336
+ if (this.accesses[address] === undefined) {
337
+ this.addAddress(address)
338
+ }
339
+ if (this.accesses[address].storageChanges[strippedKey] === undefined) {
340
+ this.accesses[address].storageChanges[strippedKey] = []
341
+ }
342
+ // For zero values, strippedValue is empty - this is correct for RLP encoding
343
+ this.accesses[address].storageChanges[strippedKey].push([blockAccessIndex, strippedValue])
344
+ // Per EIP-7928: A successful storage write subsumes any prior read of the same slot.
345
+ // Remove the slot from storageReads since it's now in storageChanges.
346
+ this.accesses[address].storageReads.delete(strippedKey)
347
+ }
348
+
349
+ public addStorageRead(address: BALAddressHex, storageKey: BALStorageKeyBytes): void {
350
+ if (this.accesses[address] === undefined) {
351
+ this.addAddress(address)
352
+ }
353
+ const strippedKey = normalizeStorageKeyHex(bytesToHex(stripLeadingZeros(storageKey)))
354
+ // Per EIP-7928: Don't add to storageReads if the slot was already written.
355
+ // A write subsumes any reads of the same slot.
356
+ if (this.accesses[address].storageChanges[strippedKey] === undefined) {
357
+ this.accesses[address].storageReads.add(strippedKey)
358
+ }
359
+ }
360
+
361
+ public addBalanceChange(
362
+ address: BALAddressHex,
363
+ balance: BALBalanceBigInt,
364
+ blockAccessIndex: BALAccessIndexNumber,
365
+ originalBalance?: BALBalanceBigInt,
366
+ ): void {
367
+ if (this.accesses[address] === undefined) {
368
+ this.addAddress(address)
369
+ }
370
+ // EIP-7928: Track the original (pre-transaction) balance for net-zero detection
371
+ // Only set if not already tracked (first call wins)
372
+ if (originalBalance !== undefined && !this.originalBalances.has(address)) {
373
+ this.originalBalances.set(address, originalBalance)
374
+ }
375
+ this.accesses[address].balanceChanges.set(
376
+ blockAccessIndex,
377
+ padToEvenHex(bytesToHex(stripLeadingZeros(bigIntToBytes(balance)))),
378
+ )
379
+ }
380
+
381
+ /**
382
+ * EIP-7928: Remove balance changes for addresses where final balance equals first balance.
383
+ * Call this at the end of each transaction to clean up net-zero balance changes.
384
+ */
385
+ public cleanupNetZeroBalanceChanges(): void {
386
+ for (const [address, originalBalance] of this.originalBalances.entries()) {
387
+ const access = this.accesses[address]
388
+ if (access === undefined || access.balanceChanges.size === 0) {
389
+ continue
390
+ }
391
+ // Get the final balance (last entry in the balanceChanges map)
392
+ const entries = Array.from(access.balanceChanges.values())
393
+ const finalBalanceHex = entries[entries.length - 1]
394
+ const finalBalance =
395
+ finalBalanceHex === '0x' ? BigInt(0) : BigInt(`0x${finalBalanceHex.replace(/^0x/, '')}`)
396
+
397
+ // EIP-7928: If final balance == original balance, remove all balanceChanges
398
+ // for the current blockAccessIndex only, but keep prior tx entries.
399
+ if (finalBalance === originalBalance) {
400
+ access.balanceChanges.delete(this.blockAccessIndex)
401
+ }
402
+ }
403
+ // Clear the tracking map for the next transaction
404
+ this.originalBalances.clear()
405
+ }
406
+
407
+ public addNonceChange(
408
+ address: BALAddressHex,
409
+ nonce: BALNonceBigInt,
410
+ blockAccessIndex: BALAccessIndexNumber,
411
+ ): void {
412
+ if (this.accesses[address] === undefined) {
413
+ this.addAddress(address)
414
+ }
415
+ this.accesses[address].nonceChanges.set(blockAccessIndex, padToEvenHex(bigIntToHex(nonce)))
416
+ }
417
+
418
+ public addCodeChange(
419
+ address: BALAddressHex,
420
+ code: BALByteCodeBytes,
421
+ blockAccessIndex: BALAccessIndexNumber,
422
+ originalCode?: BALByteCodeBytes,
423
+ ): void {
424
+ if (this.accesses[address] === undefined) {
425
+ this.addAddress(address)
426
+ }
427
+ const codeChanges = this.accesses[address].codeChanges
428
+
429
+ // Track the original code at the start of this blockAccessIndex
430
+ const trackingKey = `${address}-${blockAccessIndex}`
431
+ if (!this.originalCodesAtIndex.has(trackingKey) && originalCode !== undefined) {
432
+ this.originalCodesAtIndex.set(trackingKey, originalCode)
433
+ }
434
+
435
+ // Get the original code at the start of this blockAccessIndex
436
+ const originalCodeAtIndex = this.originalCodesAtIndex.get(trackingKey)
437
+
438
+ // Check if there's already a code change at this blockAccessIndex
439
+ const existingIndex = codeChanges.findIndex(([idx]) => idx === blockAccessIndex)
440
+ if (existingIndex !== -1) {
441
+ // Check if the new code equals the original code at start of this blockAccessIndex
442
+ // If so, remove the entry (net-zero change within this blockAccessIndex)
443
+ if (
444
+ originalCodeAtIndex !== undefined &&
445
+ bytesToHex(code) === bytesToHex(originalCodeAtIndex)
446
+ ) {
447
+ codeChanges.splice(existingIndex, 1)
448
+ } else {
449
+ // Update the existing entry with the new code
450
+ codeChanges[existingIndex] = [blockAccessIndex, code]
451
+ }
452
+ } else {
453
+ // Add new entry, but only if code is actually different from originalCode
454
+ if (originalCode !== undefined && bytesToHex(code) === bytesToHex(originalCode)) {
455
+ // No actual change, don't record
456
+ return
457
+ }
458
+ codeChanges.push([blockAccessIndex, code])
459
+ }
460
+ }
461
+
462
+ /**
463
+ * EIP-7928: For selfdestructed accounts, drop all state changes while
464
+ * preserving read footprints. Any storageChanges are converted to storageReads.
465
+ *
466
+ * Per EIP-7928: "if the account had a positive balance pre-transaction,
467
+ * the balance change to zero MUST be recorded."
468
+ */
469
+ /**
470
+ * Converts the internal representation to the JSON format (BALJSONBlockAccessList).
471
+ * Inverse of createBlockLevelAccessListFromJSON().
472
+ */
473
+ public toJSON(): BALJSONBlockAccessList {
474
+ const result: BALJSONBlockAccessList = []
475
+
476
+ for (const [address, access] of Object.entries(this.accesses)
477
+ .sort(([a], [b]) => a.localeCompare(b))
478
+ .filter(([address, access]) => shouldIncludeAddress(address as BALAddressHex, access))) {
479
+ const storageChanges: BALJSONSlotChanges[] = (
480
+ Object.entries(access.storageChanges) as [BALStorageKeyHex, BALRawStorageChange[]][]
481
+ )
482
+ .sort(([a], [b]) => a.localeCompare(b))
483
+ .map(([slot, changes]) => ({
484
+ slot,
485
+ slotChanges: changes
486
+ .sort((a, b) => a[0] - b[0])
487
+ .map(([index, value]) => ({
488
+ blockAccessIndex: indexToHex(index),
489
+ postValue: padToEvenHex(bytesToHex(value)),
490
+ })),
491
+ }))
492
+
493
+ const storageReads: BALStorageKeyHex[] = Array.from(access.storageReads).sort((a, b) =>
494
+ Number(
495
+ (a === '0x' ? 0n : hexToBigInt(a as `0x${string}`)) -
496
+ (b === '0x' ? 0n : hexToBigInt(b as `0x${string}`)),
497
+ ),
498
+ )
499
+
500
+ const balanceChanges: BALJSONBalanceChange[] = Array.from(access.balanceChanges.entries())
501
+ .sort(([a], [b]) => a - b)
502
+ .map(([index, balance]) => ({
503
+ blockAccessIndex: indexToHex(index),
504
+ postBalance: balance,
505
+ }))
506
+
507
+ const nonceChanges: BALJSONNonceChange[] = Array.from(access.nonceChanges.entries())
508
+ .sort(([a], [b]) => a - b)
509
+ .map(([index, nonce]) => ({
510
+ blockAccessIndex: indexToHex(index),
511
+ postNonce: nonce,
512
+ }))
513
+
514
+ const codeChanges: BALJSONCodeChange[] = access.codeChanges.map(([index, code]) => ({
515
+ blockAccessIndex: indexToHex(index),
516
+ newCode: bytesToHex(code),
517
+ }))
518
+
519
+ result.push({
520
+ address: address as BALAddressHex,
521
+ nonceChanges,
522
+ balanceChanges,
523
+ codeChanges,
524
+ storageChanges,
525
+ storageReads,
526
+ })
527
+ }
528
+
529
+ return result
530
+ }
531
+
532
+ public cleanupSelfdestructed(addresses: Array<BALAddressHex>): void {
533
+ for (const address of addresses) {
534
+ const access = this.accesses[address]
535
+ if (access === undefined) {
536
+ continue
537
+ }
538
+
539
+ // Convert any storageChanges into storageReads
540
+ for (const slot of Object.keys(access.storageChanges)) {
541
+ access.storageReads.add(slot as BALStorageKeyHex)
542
+ }
543
+
544
+ access.storageChanges = {}
545
+ access.nonceChanges.clear()
546
+ access.codeChanges = []
547
+
548
+ // EIP-7928: If the account had a positive pre-transaction balance,
549
+ // the balance change to zero MUST be recorded.
550
+ // The balance change to 0 is already added during SELFDESTRUCT execution.
551
+ // We only clear balance changes if pre-transaction balance was 0 (no actual change).
552
+ const originalBalance = this.originalBalances.get(address)
553
+ if (originalBalance === undefined || originalBalance === BigInt(0)) {
554
+ // Pre-transaction balance was 0 or unknown - clear balance changes
555
+ // (0 -> 0 is no change, so nothing to record)
556
+ access.balanceChanges.clear()
557
+ }
558
+ // If originalBalance > 0, keep the balance changes (which should show balance = 0)
559
+ }
560
+ }
561
+ }
562
+
563
+ function compareLexicographicHexOrBytes(
564
+ a: PrefixedHexString | Uint8Array,
565
+ b: PrefixedHexString | Uint8Array,
566
+ ): number {
567
+ const aBytes = a instanceof Uint8Array ? a : hexToBytes(a)
568
+ const bBytes = b instanceof Uint8Array ? b : hexToBytes(b)
569
+ const minLength = Math.min(aBytes.length, bBytes.length)
570
+ for (let i = 0; i < minLength; i++) {
571
+ if (aBytes[i] < bBytes[i]) return -1
572
+ if (aBytes[i] > bBytes[i]) return 1
573
+ }
574
+ if (aBytes.length < bBytes.length) return -1
575
+ if (aBytes.length > bBytes.length) return 1
576
+ return 0
577
+ }
578
+
579
+ export function createBlockLevelAccessList(): BlockLevelAccessList {
580
+ return new BlockLevelAccessList()
581
+ }
582
+
583
+ export function createBlockLevelAccessListFromJSON(
584
+ json: BALJSONBlockAccessList,
585
+ ): BlockLevelAccessList {
586
+ const bal = new BlockLevelAccessList()
587
+
588
+ for (const account of json) {
589
+ bal.addAddress(account.address)
590
+ const access = bal.accesses[account.address]
591
+
592
+ for (const slotChange of account.storageChanges) {
593
+ const normalizedSlot = normalizeStorageKeyHex(slotChange.slot)
594
+ if (access.storageChanges[normalizedSlot] === undefined) {
595
+ access.storageChanges[normalizedSlot] = []
596
+ }
597
+ for (const change of slotChange.slotChanges) {
598
+ access.storageChanges[normalizedSlot].push([
599
+ parseInt(change.blockAccessIndex, 16),
600
+ hexToBytes(change.postValue),
601
+ ])
602
+ }
603
+ }
604
+
605
+ for (const slot of account.storageReads) {
606
+ access.storageReads.add(normalizeStorageKeyHex(slot))
607
+ }
608
+
609
+ for (const change of account.balanceChanges) {
610
+ access.balanceChanges.set(
611
+ parseInt(change.blockAccessIndex, 16),
612
+ padToEvenHex(change.postBalance),
613
+ )
614
+ }
615
+
616
+ for (const change of account.nonceChanges) {
617
+ access.nonceChanges.set(parseInt(change.blockAccessIndex, 16), padToEvenHex(change.postNonce))
618
+ }
619
+
620
+ for (const change of account.codeChanges) {
621
+ access.codeChanges.push([parseInt(change.blockAccessIndex, 16), hexToBytes(change.newCode)])
622
+ }
623
+ }
624
+
625
+ return bal
626
+ }
627
+
628
+ /**
629
+ * Normalizes a quantity-like hex string for canonical RLP encoding.
630
+ * Integer fields in the BAL use minimal big-endian encoding, so leading zero bytes
631
+ * are stripped and zero is encoded as empty bytes.
632
+ */
633
+ function normalizeHexForRLP(hex: PrefixedHexString): PrefixedHexString | Uint8Array {
634
+ const stripped = hex.slice(2).replace(/^0+/, '')
635
+ if (stripped === '') {
636
+ return Uint8Array.from([])
637
+ }
638
+ return `0x${padToEven(stripped)}` as PrefixedHexString
639
+ }
640
+
641
+ function normalizeBytesForRLPQuantity(bytes: Uint8Array): Uint8Array {
642
+ return stripLeadingZeros(bytes)
643
+ }
644
+
645
+ function normalizeQuantityHexForRLP(hex: PrefixedHexString): PrefixedHexString {
646
+ const stripped = hex.slice(2).replace(/^0+/, '')
647
+ if (stripped === '') {
648
+ return '0x'
649
+ }
650
+ return `0x${padToEven(stripped)}`
651
+ }
652
+
653
+ export function createBlockLevelAccessListFromRLP(rlp: Uint8Array): BlockLevelAccessList {
654
+ const decoded = RLP.decode(rlp) as Array<
655
+ [
656
+ Uint8Array, // address
657
+ Array<[Uint8Array, Array<[Uint8Array, Uint8Array]>]>, // storage changes
658
+ Uint8Array[], // storage reads
659
+ Array<[Uint8Array, Uint8Array]>, // balance changes
660
+ Array<[Uint8Array, Uint8Array]>, // nonce changes
661
+ Array<[Uint8Array, Uint8Array]>, // code changes
662
+ ]
663
+ >
664
+
665
+ const bal = new BlockLevelAccessList()
666
+
667
+ for (const account of decoded) {
668
+ const [
669
+ addressBytes,
670
+ storageChangesRaw,
671
+ storageReadsRaw,
672
+ balanceChangesRaw,
673
+ nonceChangesRaw,
674
+ codeChangesRaw,
675
+ ] = account
676
+ const address = bytesToHex(addressBytes) as BALAddressHex
677
+ bal.addAddress(address)
678
+ const access = bal.accesses[address]
679
+
680
+ for (const [slotBytes, slotChangesRaw] of storageChangesRaw) {
681
+ const slot = normalizeStorageKeyHex(bytesToHex(slotBytes))
682
+ if (access.storageChanges[slot] === undefined) {
683
+ access.storageChanges[slot] = []
684
+ }
685
+ for (const [indexBytes, valueBytes] of slotChangesRaw) {
686
+ access.storageChanges[slot].push([bytesToInt(indexBytes), valueBytes])
687
+ }
688
+ }
689
+
690
+ for (const slotBytes of storageReadsRaw) {
691
+ access.storageReads.add(normalizeStorageKeyHex(bytesToHex(slotBytes)))
692
+ }
693
+
694
+ for (const [indexBytes, balanceBytes] of balanceChangesRaw) {
695
+ access.balanceChanges.set(
696
+ bytesToInt(indexBytes),
697
+ padToEvenHex(bytesToHex(balanceBytes)) as BALBalanceHex,
698
+ )
699
+ }
700
+
701
+ for (const [indexBytes, nonceBytes] of nonceChangesRaw) {
702
+ access.nonceChanges.set(
703
+ bytesToInt(indexBytes),
704
+ padToEvenHex(bytesToHex(nonceBytes)) as BALNonceHex,
705
+ )
706
+ }
707
+
708
+ for (const [indexBytes, codeBytes] of codeChangesRaw) {
709
+ access.codeChanges.push([bytesToInt(indexBytes), codeBytes])
710
+ }
711
+ }
712
+
713
+ return bal
714
+ }
715
+
716
+ function stripLeadingZeros(bytes: Uint8Array): Uint8Array {
717
+ let first = bytes[0]
718
+ while (bytes.length > 0 && first.toString() === '0') {
719
+ bytes = bytes.slice(1)
720
+ first = bytes[0]
721
+ }
722
+ return bytes
723
+ }
724
+
725
+ function padToEvenHex(hex: PrefixedHexString): PrefixedHexString {
726
+ return `0x${padToEven(hex.slice(2))}`
727
+ }
728
+
729
+ /**
730
+ * Normalizes a storage key hex string to ensure consistent even-length representation.
731
+ * - "0x" (empty bytes) is kept as is
732
+ * - "0x0" becomes "0x00"
733
+ * - Any odd-length hex is padded to even (e.g., "0x1" → "0x01")
734
+ */
735
+ function normalizeStorageKeyHex(hex: PrefixedHexString): BALStorageKeyHex {
736
+ const stripped = hex.slice(2)
737
+ // Empty string "0x" stays as is
738
+ if (stripped === '') {
739
+ return '0x' as BALStorageKeyHex
740
+ }
741
+ // Pad to even length (handles "0x0" → "0x00", "0x1" → "0x01", etc.)
742
+ return `0x${padToEven(stripped)}` as BALStorageKeyHex
743
+ }
744
+
745
+ function shouldIncludeAddress(address: BALAddressHex, access: Accesses[BALAddressHex]): boolean {
746
+ if (address !== SYSTEM_ADDRESS) {
747
+ return true
748
+ }
749
+
750
+ return (
751
+ access.storageReads.size > 0 ||
752
+ Object.keys(access.storageChanges).length > 0 ||
753
+ access.balanceChanges.size > 0 ||
754
+ access.nonceChanges.size > 0 ||
755
+ access.codeChanges.length > 0
756
+ )
757
+ }
758
+
759
+ function indexToHex(index: BALAccessIndexNumber): BALAccessIndexHex {
760
+ return padToEvenHex(`0x${index.toString(16)}`) as BALAccessIndexHex
761
+ }