@onrail-xyz/evm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,491 @@
1
+ import { type Address, checksumAddress } from "viem";
2
+ import type { Identity, RoUint8Array, RoTuple,
3
+ RoNeTuple, RoPair, HeadTail, Opts, OptionalArg } from "@onrail-xyz/utils";
4
+ import { utf8, hex, bignum, assertDistinct, argOf } from "@onrail-xyz/utils";
5
+ import type { Conversion, Layout, Struct, DeriveType, Item,
6
+ NumSizeToPrimitive, WithCustom, UnwrapSingleton } from "@onrail-xyz/binary-layout";
7
+ import { serialize, deserialize,
8
+ calcStaticSize, paddingItem, unwrapSingleton,
9
+ isItem, uintItem, switchItem, numSizeToPrimitive,
10
+ timestampItem, withCustom, hasFixed, customOf, boolItem } from "@onrail-xyz/binary-layout";
11
+ import type { KindWithAtomic } from "@onrail-xyz/amount";
12
+ import { type AmountOr, amountItem, hashItem } from "@onrail-xyz/common";
13
+ import { keccak256 } from "./hashing.js";
14
+
15
+ // ---- Padding ----
16
+
17
+ export const wordSize = 32;
18
+ export const addressSize = 20;
19
+
20
+ //sub-word layouts are left-padded onto one slot - the ABI's alignment for every static type but
21
+ // bytesN, which `bytesNItem` spells at word width, right-padded; whole-word ones (a signature's
22
+ // three words) are already slot-aligned and pass with empty padding
23
+ const slotPadding = (layout: Layout) => {
24
+ const size = calcStaticSize(layout);
25
+ if (size === null || (size > wordSize && size % wordSize !== 0))
26
+ throw new Error("layout must have a static size of at most one word or of whole words");
27
+
28
+ return paddingItem(size > wordSize ? 0 : wordSize - size);
29
+ };
30
+
31
+ //the ABI sign-extends a negative value across its slot, which a fixed padding cannot do, so a
32
+ // signed value takes the word's own width on the wire and narrows back to the primitive its
33
+ // declared width surfaces. The conversion rejects a value the declared width cannot hold,
34
+ // coming or going: the wider wire item would pass it silently either way
35
+ type SignedSlot = Readonly<{ binary: "int"; size: typeof wordSize }>;
36
+ export type SignedSlotItem<I extends Item> = WithCustom<SignedSlot, DeriveType<I>>;
37
+
38
+ const signedSlotLayout = { binary: "int", size: wordSize } as const satisfies SignedSlot;
39
+
40
+ const signedSlotItem = <const I extends Item>(item: I): SignedSlotItem<I> => {
41
+ const size = calcStaticSize(item)!;
42
+ const bound = 1n << BigInt(size * 8 - 1);
43
+ const checkBound = (wide: bigint) => {
44
+ if (wide < -bound || wide >= bound)
45
+ throw new Error(`${wide} does not fit a signed ${size * 8}-bit slot`);
46
+
47
+ return wide;
48
+ };
49
+ const narrow = (word: bigint) => numSizeToPrimitive(checkBound(word), size) as DeriveType<I>;
50
+ const widen = (value: unknown) => checkBound(BigInt(value as number | bigint));
51
+
52
+ //the item's own conversion speaks its declared width, so the widening wraps it on both sides
53
+ // rather than composing through `withCustom`, which only appends
54
+ const own = customOf(item);
55
+ return withCustom(signedSlotLayout, own === undefined
56
+ ? { to: narrow, from: widen }
57
+ : { to: (word: bigint) => own.to(narrow(word)) as DeriveType<I>,
58
+ from: (value: DeriveType<I>) => widen(own.from(value)) },
59
+ ) as SignedSlotItem<I>;
60
+ };
61
+
62
+ //annotated: inferring this return would make declaration emit inline unwrapSingleton's fully
63
+ // evaluated type machinery (recursion unrolled, alias identity - and with it instantiation
64
+ // caching - lost), which downstream checkers then re-evaluate per instantiation. The
65
+ // annotation is a conditional over the item's signedness, which neither branch's value can
66
+ // satisfy while I is open, hence the cast in paddedItem
67
+ type SlotPadding = ReturnType<typeof paddingItem>;
68
+ type PaddedStruct<I extends Item> = { readonly _padding: SlotPadding, readonly item: I };
69
+ type ZeroPaddedItem<I extends Item> = UnwrapSingleton<PaddedStruct<I>>;
70
+
71
+ type PaddedItem<I extends Item> =
72
+ I extends { binary: "int" } ? SignedSlotItem<I> : ZeroPaddedItem<I>;
73
+
74
+ //a slot surfaces exactly the item's value, so an item that surfaces none - one of fixed value,
75
+ // `as` included, since `as` carries `fixed` by construction - has no slot form: it would leave
76
+ // the padded struct with nothing to unwrap to
77
+ const paddedItem = <const I extends Item>(item: I): PaddedItem<I> => {
78
+ if (calcStaticSize(item) === null || hasFixed(item))
79
+ throw new Error("a slot item must be statically sized and surface a value");
80
+
81
+ return (item.binary === "int"
82
+ ? signedSlotItem(item)
83
+ : unwrapSingleton({ _padding: slotPadding(item), item })
84
+ ) as PaddedItem<I>;
85
+ };
86
+
87
+ //a generated wire-only field leads with an underscore - `_padding`, `_selector` - which keeps
88
+ // it out of the namespace a spec's own fields are spelled in
89
+ const paddedStruct = <const S extends Struct>(layout: S) =>
90
+ ({ _padding: slotPadding(layout), ...layout } as const);
91
+
92
+ export type PaddedSlotLayout<T extends Struct | Item> =
93
+ T extends Item
94
+ ? PaddedItem<T>
95
+ : T extends Struct
96
+ ? ReturnType<typeof paddedStruct<T>>
97
+ : never;
98
+
99
+ export const paddedSlotLayout =
100
+ <const T extends Struct | Item>(layoutOrItem: T): PaddedSlotLayout<T> =>
101
+ (isItem(layoutOrItem)
102
+ ? paddedItem(layoutOrItem)
103
+ : paddedStruct(layoutOrItem)
104
+ ) as PaddedSlotLayout<T>;
105
+
106
+ export type PaddedFields<S extends Struct> = { readonly [K in keyof S]: PaddedSlotLayout<S[K]> };
107
+
108
+ //every field on a slot of its own: the ABI's static parameter list, EIP-712's encodeData
109
+ export const paddedFields = <const S extends Struct>(layout: S): PaddedFields<S> =>
110
+ Object.fromEntries(
111
+ Object.entries(layout).map(([name, field]) => [name, paddedSlotLayout(field)]),
112
+ ) as PaddedFields<S>;
113
+
114
+ // ---- Value items ----
115
+
116
+ export const uint256Item = { binary: "uint", size: wordSize } as const;
117
+
118
+ //an amount on a word; narrower amounts are `amountItem(size, kind)` as anywhere else
119
+ export const evmAmountItem =
120
+ <const K extends KindWithAtomic | undefined = undefined>(...kind: OptionalArg<K>) =>
121
+ amountItem(wordSize, ...kind);
122
+
123
+ //a maxed out deadline is the "never expires" idiom (EIP-2612, EIP-3009), and anything else out
124
+ // that far is not an intended instant either, so the whole high end resolves to the same sentinel
125
+ export const deadlineItem = timestampItem({ size: wordSize, saturateAs: "infinity" });
126
+ export type Deadline = DeriveType<typeof deadlineItem>;
127
+
128
+ export const evmTimestampItem = timestampItem({ size: wordSize });
129
+
130
+ //annotated, not `satisfies`: only an annotation gives the value the `Conversion` reference as
131
+ // its type, and that reference is what survives into every item embedding it.
132
+ // The surfaced spelling is EIP-55, which is what viem's own decoders return: a lowercase one
133
+ // would make the same word decode to two strings that compare unequal
134
+ export const addressConversion: Conversion<RoUint8Array, Address> = {
135
+ to: (encoded: RoUint8Array) => checksumAddress(hex.encode(encoded, true)),
136
+ from: (addr: Address ) => hex.decode(addr),
137
+ };
138
+
139
+ const _addressItem = {
140
+ binary: "bytes",
141
+ size: addressSize,
142
+ custom: addressConversion,
143
+ } as const;
144
+ export interface AddressItem extends Identity<typeof _addressItem> {}
145
+ export const addressItem: AddressItem = _addressItem;
146
+
147
+ //an address and a bool on their ABI words
148
+ export const abiAddressItem = paddedSlotLayout(addressItem);
149
+ export const abiBoolItem = paddedSlotLayout(boolItem());
150
+
151
+ //the ABI's `bytesN`, the one static type it pads on the right: spelled at word width, where a
152
+ // bare bytes item on a slot (an address, say) is left-padded like everything else
153
+ const rightPaddedBytesLayout = <N extends number>(size: N) => ({
154
+ value: { binary: "bytes", size },
155
+ _padding: paddingItem(wordSize - size),
156
+ } as const);
157
+ export type BytesNItem<N extends number> =
158
+ UnwrapSingleton<ReturnType<typeof rightPaddedBytesLayout<N>>>;
159
+
160
+ export const bytesNItem = <N extends number>(size: N): BytesNItem<N> => {
161
+ if (!Number.isInteger(size) || size < 1 || size > wordSize)
162
+ throw new Error(`bytes${size} is not an ABI type`);
163
+
164
+ return unwrapSingleton(rightPaddedBytesLayout(size));
165
+ };
166
+
167
+ // ---- Signatures ----
168
+
169
+ export const signatureSize = 65;
170
+ export const signatureItem = { binary: "bytes", size: signatureSize } as const;
171
+
172
+ const vItem = { binary: "uint", size: 1 } as const;
173
+ //v is the recovery id in its legacy spelling, 27 + yParity
174
+ const vBase = 27;
175
+ const yParityBit = 0x80;
176
+
177
+ export const signatureLayout = { r: hashItem, s: hashItem, v: vItem } as const;
178
+ export const abiSignatureLayout = { v: paddedSlotLayout(vItem), r: hashItem, s: hashItem } as const;
179
+
180
+ export const abiSignatureItem = withCustom(abiSignatureLayout, {
181
+ to: (words): RoUint8Array => serialize (signatureLayout, words),
182
+ from: (signature: RoUint8Array) => deserialize(signatureLayout, signature),
183
+ });
184
+
185
+ export const compactSignatureItem = withCustom({ r: hashItem, yParityAndS: hashItem }, {
186
+ to: ({ r, yParityAndS }): RoUint8Array => {
187
+ const s = Uint8Array.from(yParityAndS);
188
+ s[0] = yParityAndS[0]! & ~yParityBit;
189
+ const yParity = yParityAndS[0]! >= yParityBit ? 1 : 0;
190
+ return serialize(signatureLayout, { r, s, v: vBase + yParity });
191
+ },
192
+ from: (signature: RoUint8Array) => {
193
+ const { r, s, v } = deserialize(signatureLayout, signature);
194
+ //the form has one bit for the parity: v must be that bit's legacy spelling, and s must
195
+ // leave the bit free, as a canonical (low) s does
196
+ if (v !== vBase && v !== vBase + 1)
197
+ throw new Error(`v must be ${vBase} or ${vBase + 1}, got ${v}`);
198
+
199
+ if (s[0]! >= yParityBit)
200
+ throw new Error("s has its top bit set (not a low s), so it has no compact form");
201
+
202
+ const yParityAndS = Uint8Array.from(s);
203
+ yParityAndS[0] = s[0]! | (v - vBase) * yParityBit;
204
+ return { r, yParityAndS };
205
+ },
206
+ });
207
+
208
+ // ---- Function selectors ----
209
+
210
+ const funcSigHashPrefix = (funcSig: string, size: number) =>
211
+ keccak256(utf8.encode(funcSig)).subarray(0, size);
212
+
213
+ export const selectorLength = 4;
214
+ export const selectorOf = (funcSig: string) => funcSigHashPrefix(funcSig, selectorLength);
215
+
216
+ export const selectorItem = (funcSig: string) =>
217
+ ({ binary: "bytes", fixed: selectorOf(funcSig) } as const);
218
+
219
+ export const selectorLayout =
220
+ (funcSig: string) =>
221
+ <const L extends Struct>(layout: L) =>
222
+ ({ _selector: selectorItem(funcSig), ...layout } as const);
223
+
224
+ // ---- A single dynamic value ----
225
+
226
+ //poor man's abi.encode/decode(bytes): handles a single dynamic value - a trailing argument, or a
227
+ // lone return - whose payload therefore follows the whole head. `offset` is that head's size,
228
+ // its own word included - hence the default, a lone argument - and is the argument's index times
229
+ // the word size only where every preceding argument occupies exactly one slot
230
+ const lengthSize = 4;
231
+ export type AbiEncoded<L extends Layout | undefined> =
232
+ L extends Layout ? DeriveType<L> : RoUint8Array;
233
+
234
+ //the payload stays raw bytes in the layout, so its length is at hand to check the padding
235
+ // against; the conversion runs the content layout, if any
236
+ const abiEncodedLayout = (offset: number) => ({
237
+ offset: { ...uint256Item, fixed: BigInt(offset) },
238
+ lengthPadding: { binary: "bytes", fixed: new Uint8Array(wordSize - lengthSize) },
239
+ item: { binary: "bytes", size: { binary: "uint", size: lengthSize } },
240
+ postPadding: { binary: "bytes" },
241
+ } as const);
242
+
243
+ const paddingSizeOf = (dataSize: number) => (wordSize - dataSize % wordSize) % wordSize;
244
+
245
+ //annotated so declaration emit prints the alias rather than WithCustom's evaluated form
246
+ export type AbiEncodedBytesItem<L extends Layout | undefined> =
247
+ WithCustom<ReturnType<typeof abiEncodedLayout>, AbiEncoded<L>>;
248
+
249
+ type AbiEncodedOpts = Opts<{ layout: Layout, offset: number }>;
250
+ //the content layout, read off the whole record: one that may be undefined - or absent, where the
251
+ // record's own type declares it optional - types as either payload
252
+ type ContentOf<O extends AbiEncodedOpts> = "layout" extends keyof O ? O["layout"] : undefined;
253
+
254
+ export const abiEncodedBytesItem = <const O extends AbiEncodedOpts = {}>(
255
+ ...[opts]: OptionalArg<O>
256
+ ): AbiEncodedBytesItem<ContentOf<O>> => {
257
+ type L = ContentOf<O>;
258
+ const layout = abiEncodedLayout(opts?.offset ?? wordSize);
259
+ const content = opts?.layout;
260
+
261
+ return withCustom(layout, {
262
+ to: (wrapped) => {
263
+ const { item, postPadding } = wrapped;
264
+ if (postPadding.length !== paddingSizeOf(item.length) || postPadding.some(b => b !== 0))
265
+ throw new Error(`abi-encoded bytes: expected ${paddingSizeOf(item.length)} padding bytes`);
266
+
267
+ return (content !== undefined ? deserialize(content, item) : item) as AbiEncoded<L>;
268
+ },
269
+ from: (value: AbiEncoded<L>) => {
270
+ const item = content !== undefined ? serialize(content, value) : value as RoUint8Array;
271
+ return { item, postPadding: new Uint8Array(paddingSizeOf(item.length)) };
272
+ },
273
+ });
274
+ };
275
+
276
+ // ---- Parameters ----
277
+
278
+ //`__valueType` is a phantom, never set, carrying the item's surfaced type, which the item's
279
+ // DeriveType does not reduce to while an amount's kind is still open - the kinded params state
280
+ // it as AmountOrAtomic<K>. A string key rather than a symbol brand: a consumer's spread or Omit
281
+ // of a param expands the alias, and an expanded private symbol cannot be named in their
282
+ // declarations.
283
+ //spelled member by member rather than as Readonly<{...}>: an alias whose body is another alias
284
+ // reference emits under the inner name, expanded
285
+ export type AbiParam<
286
+ N extends string = string,
287
+ T extends string = string,
288
+ I extends Item = Item,
289
+ V = unknown,
290
+ > = {
291
+ readonly name: N;
292
+ readonly type: T;
293
+ readonly item: I;
294
+ readonly __valueType?: V;
295
+ };
296
+
297
+ //the surfaced type is the item's, or stated up front where `DeriveType<I>` does not reduce - an
298
+ // amount at an open kind. The switch is on a boolean rather than on V itself: a check type
299
+ // that is always a literal resolves eagerly, where a check on V would stay deferred whenever V
300
+ // is open (a stated AmountOrAtomic<K>), and a deferred conditional in parameter position
301
+ // admits nothing
302
+ type ValueOf<I extends Item, Stated extends boolean, V> = Stated extends true ? V : DeriveType<I>;
303
+
304
+ type ParamFactory<Stated extends boolean, V> =
305
+ <N extends string, T extends string, const I extends Item>(name: N, type: T, item: I) =>
306
+ AbiParam<N, T, I, ValueOf<I, Stated, V>>;
307
+
308
+ //the zero-argument overload exists because a type argument cannot be given without giving them
309
+ // all: `abiParam<V>()` takes V alone and leaves the rest to inference
310
+ type AbiParamFn = ParamFactory<false, "ignored"> & { <V>(): ParamFactory<true, V> };
311
+
312
+ export const abiParam: AbiParamFn = ((...args: [string, string, Item] | []) =>
313
+ args.length === 0
314
+ ? (name: string, type: string, item: Item) => ({ name, type, item })
315
+ : { name: args[0], type: args[1], item: args[2] }
316
+ ) as AbiParamFn;
317
+
318
+ export type AbiParamValue<A extends AbiParam> = A extends { __valueType?: infer V } ? V : never;
319
+
320
+ type ParamOfFactory<Stated extends boolean, V> =
321
+ <T extends string, const I extends Item>(type: T, item: I) =>
322
+ <N extends string>(name: N) => AbiParam<N, T, I, ValueOf<I, Stated, V>>;
323
+
324
+ //the same door one level up: `paramOf<V>()(type, item)` fixes the pair with a stated type
325
+ type ParamOfFn = ParamOfFactory<false, "ignored"> & { <V>(): ParamOfFactory<true, V> };
326
+
327
+ export const paramOf: ParamOfFn = ((...args: [string, Item] | []) =>
328
+ args.length === 0
329
+ ? (type: string, item: Item) => (name: string) => ({ name, type, item })
330
+ : (name: string) => ({ name, type: args[0], item: args[1] })
331
+ ) as ParamOfFn;
332
+
333
+ export const addressParam = paramOf("address", addressItem);
334
+
335
+ //the item's width is the domain's range, independent of the ABI type the slot is spelled as: a
336
+ // uint64 amount the contract holds rides a `uint256` word, and the 8-byte item is what rejects
337
+ // a value the contract could not hold. Without a kind the value is what that width derives,
338
+ // as the item's own DeriveType would say
339
+ export const amountParam = <
340
+ N extends string,
341
+ T extends string,
342
+ const K extends KindWithAtomic | undefined = undefined,
343
+ S extends number = typeof wordSize,
344
+ >(name: N, type: T, ...args: OptionalArg<K, OptionalArg<S>>) =>
345
+ abiParam<AmountOr<K, NumSizeToPrimitive<S>>>()
346
+ (name, type, amountItem((args[1] ?? wordSize) as S, argOf(args)));
347
+
348
+ //the one dynamic parameter a function spec admits. Its offset is a constant - the parameter
349
+ // count's word, since the whole head precedes the tail - but the payload can only follow the
350
+ // head, so bundling offset and payload into one item confines it to the last position, which
351
+ // `contractFromSpec` enforces and where it builds the item, the count being known only there
352
+ export type DynamicAbiParam<N extends string, L extends Layout | undefined> =
353
+ AbiParam<N, "bytes", AbiEncodedBytesItem<L>, AbiEncoded<L>> &
354
+ { readonly dynamic: { readonly layout: L } };
355
+
356
+ //`bytes` as a function's last parameter - raw, or the serialization of `layout`. The item is a
357
+ // placeholder built at offset zero; `contractFromSpec` rebuilds it once the parameter list
358
+ // fixes the head's size, which the type does not depend on
359
+ export const trailingBytesParam = <
360
+ N extends string,
361
+ const L extends Layout | undefined = undefined,
362
+ >(name: N, ...layout: OptionalArg<L>): DynamicAbiParam<N, L> => ({
363
+ ...abiParam(name, "bytes", abiEncodedBytesItem({ layout: argOf(layout) })),
364
+ dynamic: { layout: argOf(layout) },
365
+ }) as DynamicAbiParam<N, L>;
366
+
367
+ // ---- Signature variants: events and errors ----
368
+
369
+ export type SigVariant<S extends string = string, P extends Struct = Struct> = RoPair<S, P>;
370
+ export type SigVariants = RoNeTuple<SigVariant>;
371
+
372
+ //the params' names become a struct's keys, so a repeat would fold two params into one field;
373
+ // every params list passes through here, which is where it is caught
374
+ export const signatureOf = (name: string, params: RoTuple<AbiParam>): string => {
375
+ assertDistinct(...params.map(p => p.name));
376
+ return `${name}(${params.map(p => p.type).join(",")})`;
377
+ };
378
+
379
+ //the name a signature leads with - `signatureOf` read backwards, as far as the name goes
380
+ export type SigNameOf<S extends string> = S extends `${infer N}(${string}` ? N : never;
381
+
382
+ export const sigNameOf = <S extends string>(funcSig: S): SigNameOf<S> => {
383
+ const open = funcSig.indexOf("(");
384
+ if (open < 1)
385
+ throw new Error(`not a function signature: ${funcSig}`);
386
+
387
+ return funcSig.slice(0, open) as SigNameOf<S>;
388
+ };
389
+
390
+ type JoinTypes<P extends RoTuple<AbiParam>> =
391
+ P extends HeadTail<P, infer H, infer R>
392
+ ? R extends readonly [] ? H["type"] : `${H["type"]},${JoinTypes<R>}`
393
+ : "";
394
+
395
+ export type SigVariantOf<N extends string, P extends RoTuple<AbiParam>> =
396
+ SigVariant<`${N}(${JoinTypes<P>})`, { readonly [E in P[number] as E["name"]]: E["item"] }>;
397
+
398
+ //a variant from its name and params, so an event or error is spelled once
399
+ export const sigVariant = <
400
+ N extends string,
401
+ const P extends RoTuple<AbiParam>,
402
+ >(name: N, ...params: P): SigVariantOf<N, P> =>
403
+ [signatureOf(name, params), Object.fromEntries(params.map(p => [p.name, p.item]))] as any;
404
+
405
+ type SwitchRows = Parameters<typeof switchItem>[2];
406
+
407
+ type SigRows<IS extends number, V extends SigVariants> = {
408
+ readonly [K in keyof V]:
409
+ V[K] extends SigVariant<infer S, infer P>
410
+ ? readonly [NumSizeToPrimitive<IS>, SigNameOf<S>, PaddedFields<P>]
411
+ : never
412
+ };
413
+
414
+ //the infer binding resolves SigRows once: switchItem's return mentions its R both as keyof R
415
+ // and as R[K], and an unbound SigRows would be re-derived per mention
416
+ type SigRowsBound<IS extends number, V extends SigVariants> =
417
+ SigRows<IS, V> extends infer R extends SwitchRows ? R : never;
418
+
419
+ //sigSwitchItem's curried return, named so partial applications (parsing's error/event switch
420
+ // items and any downstream ones) declaration-emit as one alias reference: emitted in evaluated
421
+ // form instead, every consumer instantiation re-evaluates the whole switch typing per variant
422
+ export type SigSwitcher<IS extends number, T extends string> =
423
+ <const V extends SigVariants>(variants: V) =>
424
+ ReturnType<typeof switchItem<T, ReturnType<typeof uintItem<IS, {}>>, SigRowsBound<IS, V>, {}>>;
425
+
426
+ const sigPrefixId = <S extends number>(funcSig: string, size: S): NumSizeToPrimitive<S> => {
427
+ if (size < 1 || size > wordSize)
428
+ throw new Error(`signature prefix size must be within [1, ${wordSize}], got ${size}`);
429
+
430
+ return numSizeToPrimitive(bignum.fromBytes(funcSigHashPrefix(funcSig, size)), size);
431
+ };
432
+
433
+ //a switch over signature-derived ids, decoding the abi-encoded params of the matched variant.
434
+ // The tag surfaces the signature's own name, Solidity's spelling included - a consumer wanting
435
+ // its discriminants to read uniformly folds the case on its own side, where the convention is:
436
+ // custom errors off revert data (idSize 4 - the selector is all the wire carries), events off
437
+ // topic0 concatenated with the remaining topics and the data - so an event's struct is in
438
+ // wire order, indexed params first, which `sigVariant`'s param order is only when they lead
439
+ // the declaration. topic0 carries the whole hash, so events take idSize 32; a shorter prefix
440
+ // would only trade away collision headroom
441
+ export const sigSwitchItem = <
442
+ IS extends number,
443
+ T extends string,
444
+ >(idSize: IS, tag: T): SigSwitcher<IS, T> =>
445
+ <const V extends SigVariants>(variants: V) =>
446
+ switchItem(tag, uintItem(idSize), (variants as SigVariants).map(([funcSig, params]) => [
447
+ sigPrefixId(funcSig, idSize),
448
+ sigNameOf(funcSig),
449
+ paddedFields(params),
450
+ ] as const) as unknown as SigRowsBound<IS, V>);
451
+
452
+ // ---- Storage slots ----
453
+
454
+ //value-type keys only - string/bytes keys are hashed unpadded. The key's JS type says which
455
+ // value type it is and so how Solidity pads it to the word: an integer (a negative one in two's
456
+ // complement, i.e. sign-extended) and a bool right-aligned, an address right-aligned, a bytesN
457
+ // left-aligned - a full word is itself
458
+ export type MappingKey = bigint | number | boolean | Address | RoUint8Array;
459
+
460
+ export const mappingSlot = (key: MappingKey, declareSlot: bigint): bigint => {
461
+ const buf = new Uint8Array(2 * wordSize);
462
+ if (typeof key === "bigint" || typeof key === "number") {
463
+ const int = BigInt(key);
464
+ if (int < -(1n << 255n) || int >= (1n << 256n))
465
+ throw new Error(`mapping key ${int} does not fit a word`);
466
+
467
+ buf.set(bignum.toBytes(int < 0n ? int + (1n << 256n) : int, wordSize));
468
+ }
469
+ else if (typeof key === "boolean")
470
+ buf[wordSize - 1] = key ? 1 : 0;
471
+ else if (typeof key === "string") {
472
+ //viem's Address is any `0x${string}`, so a hex-spelled bytesN would otherwise pass for one
473
+ const address = hex.decode(key);
474
+ if (address.length !== addressSize)
475
+ throw new Error(`a string mapping key is an address, got ${address.length} bytes - ` +
476
+ `pass a bytesN key as a Uint8Array`);
477
+
478
+ buf.set(address, wordSize - addressSize);
479
+ }
480
+ else {
481
+ if (key.length > wordSize)
482
+ throw new Error("mapping key must not exceed one word");
483
+
484
+ buf.set(key, 0);
485
+ }
486
+ buf.set(bignum.toBytes(declareSlot, wordSize), wordSize);
487
+ return bignum.fromBytes(keccak256(buf));
488
+ };
489
+
490
+ export const keccakSlot = (slot: bigint): bigint =>
491
+ bignum.fromBytes(keccak256(bignum.toBytes(slot, wordSize)));
package/src/parsing.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Hex } from "viem";
2
+ import type { RoArray, RoUint8Array } from "@onrail-xyz/utils";
3
+ import { bytes, hex, succeeds } from "@onrail-xyz/utils";
4
+ import type { DeriveType, SwitchItem } from "@onrail-xyz/binary-layout";
5
+ import { deserialize, findVariantByRawId } from "@onrail-xyz/binary-layout";
6
+ import { type SigVariants, selectorLength, sigSwitchItem, wordSize } from "./layouting.js";
7
+
8
+ const requireKnownSig = (item: SwitchItem, prefix: RoUint8Array, what: string): void => {
9
+ if (!succeeds(() => findVariantByRawId(item, deserialize(item.id, prefix))))
10
+ throw new Error(`unrecognized ${what}: ${hex.encode(prefix, true)}`);
11
+ };
12
+
13
+ //required minimum for both viem's Log and RpcLog
14
+ export type ViemLog = Readonly<{ topics: RoArray<Hex>; data: Hex }>;
15
+
16
+ const errorSwitchItem = sigSwitchItem(selectorLength, "error");
17
+ const eventSwitchItem = sigSwitchItem(wordSize, "event");
18
+
19
+ //deliberate explicit result types to avoid issues arising from massive expansion by inference
20
+ export type ParsedError<V extends SigVariants> =
21
+ DeriveType<ReturnType<typeof errorSwitchItem<V>>>;
22
+ export type ParsedEvent<V extends SigVariants> =
23
+ DeriveType<ReturnType<typeof eventSwitchItem<V>>>;
24
+
25
+ export const buildParseError = <const V extends SigVariants>(errorDefs: V) => {
26
+ const item = errorSwitchItem(errorDefs);
27
+ return (revertData: RoUint8Array): ParsedError<V> => {
28
+ requireKnownSig(item, revertData.subarray(0, selectorLength), "error selector");
29
+ return deserialize(item, revertData);
30
+ };
31
+ };
32
+
33
+ export const buildParseEvent = <const V extends SigVariants>(eventDefs: V) => {
34
+ const item = eventSwitchItem(eventDefs);
35
+ return (log: ViemLog): ParsedEvent<V> => {
36
+ if (log.topics.length === 0)
37
+ throw new Error("anonymous event: no topic0 to switch on");
38
+
39
+ const encoded = bytes.concat(...log.topics.map(t => hex.decode(t)), hex.decode(log.data));
40
+ requireKnownSig(item, encoded.subarray(0, wordSize), "event topic0");
41
+ return deserialize(item, encoded);
42
+ };
43
+ };
package/src/permit.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { Address } from "viem";
2
+ import type { OptionalArg } from "@onrail-xyz/utils";
3
+ import type { KindWithAtomic } from "@onrail-xyz/amount";
4
+ import { type AmountOrAtomic, toAtomicIfAmount, hashItem } from "@onrail-xyz/common";
5
+ import { type Deadline, uint256Item, deadlineItem,
6
+ abiSignatureItem, abiParam, addressParam, amountParam } from "./layouting.js";
7
+ import { type ContractMethods, abiFunction, contractFromSpec } from "./client.js";
8
+ import type { Eip712Domain, Eip712Data, Eip712Message } from "./eip712.js";
9
+ import { eip712DomainType } from "./eip712.js";
10
+
11
+ const owner = addressParam("owner" );
12
+ const spender = addressParam("spender");
13
+ const deadline = abiParam("deadline", "uint256", deadlineItem );
14
+ const signature = abiParam("signature", "uint8,bytes32,bytes32", abiSignatureItem);
15
+
16
+ const permitSpec = <const K extends KindWithAtomic | undefined = undefined>(
17
+ ...kind: OptionalArg<K>
18
+ ) => {
19
+ const value = amountParam("value", "uint256", ...kind);
20
+ return [
21
+ abiFunction("DOMAIN_SEPARATOR", [], () => ({}), hashItem ),
22
+ abiFunction("nonces", [owner], owner => ({ owner }), uint256Item),
23
+ abiFunction("permit", [owner, spender, value, deadline, signature],
24
+ (owner, spender, value, deadline, signature) =>
25
+ ({ owner, spender, value, deadline, signature })),
26
+ ] as const;
27
+ };
28
+
29
+ export const permit = <const K extends KindWithAtomic | undefined = undefined>(
30
+ contract: Address,
31
+ ...kind: OptionalArg<K>
32
+ ): ContractMethods<ReturnType<typeof permitSpec<K>>> =>
33
+ contractFromSpec(contract, permitSpec(...kind));
34
+
35
+ const permitType = [
36
+ { name: "owner", type: "address" },
37
+ { name: "spender", type: "address" },
38
+ { name: "value", type: "uint256" },
39
+ { name: "nonce", type: "uint256" },
40
+ { name: "deadline", type: "uint256" },
41
+ ] as const;
42
+
43
+ export type Eip2612Message = Eip712Message<typeof permitType>;
44
+ export type Eip2612Data = Eip712Data<Eip2612Message>;
45
+
46
+ //compose an EIP-2612 permit message; `toViemTypedData` bridges it to viem's signing calls
47
+ export const composePermitMsg = (
48
+ owner: Address,
49
+ spender: Address,
50
+ value: AmountOrAtomic,
51
+ domain: Eip712Domain,
52
+ nonce: number | bigint,
53
+ deadline: Deadline = "infinity",
54
+ ) => {
55
+ const atomic = toAtomicIfAmount(value);
56
+ if (atomic < 0n)
57
+ throw new Error("Value must not be negative");
58
+
59
+ const unixDeadline = deadlineItem.custom.from(deadline);
60
+ const bigNonce = typeof nonce === "number" ? BigInt(nonce) : nonce;
61
+
62
+ return {
63
+ types: { EIP712Domain: eip712DomainType(domain), Permit: permitType },
64
+ primaryType: "Permit",
65
+ domain,
66
+ message: { owner, spender, value: atomic, nonce: bigNonce, deadline: unixDeadline },
67
+ } as const satisfies Eip2612Data;
68
+ };