@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.
- package/LICENSE +201 -0
- package/README.md +301 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +79 -0
- package/dist/eip712.d.ts +72 -0
- package/dist/eip712.d.ts.map +1 -0
- package/dist/eip712.js +96 -0
- package/dist/erc20.d.ts +113 -0
- package/dist/erc20.d.ts.map +1 -0
- package/dist/erc20.js +44 -0
- package/dist/erc3009.d.ts +416 -0
- package/dist/erc3009.d.ts.map +1 -0
- package/dist/erc3009.js +72 -0
- package/dist/hashing.d.ts +2 -0
- package/dist/hashing.d.ts.map +1 -0
- package/dist/hashing.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/layouting.d.ts +264 -0
- package/dist/layouting.d.ts.map +1 -0
- package/dist/layouting.js +238 -0
- package/dist/parsing.d.ts +16 -0
- package/dist/parsing.d.ts.map +1 -0
- package/dist/parsing.js +26 -0
- package/dist/permit.d.ts +139 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +38 -0
- package/dist/query.d.ts +54 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +105 -0
- package/package.json +70 -0
- package/src/client.ts +196 -0
- package/src/eip712.ts +215 -0
- package/src/erc20.ts +76 -0
- package/src/erc3009.ts +122 -0
- package/src/hashing.ts +1 -0
- package/src/index.ts +9 -0
- package/src/layouting.ts +491 -0
- package/src/parsing.ts +43 -0
- package/src/permit.ts +68 -0
- package/src/query.ts +257 -0
package/src/layouting.ts
ADDED
|
@@ -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
|
+
};
|