@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/src/eip712.ts ADDED
@@ -0,0 +1,215 @@
1
+ import type { Address, Hex } from "viem";
2
+ import type { RoUint8Array, RoArray, StrRecord, Simplify, DeepRo } from "@onrail-xyz/utils";
3
+ import { bytes, hex, utf8 } from "@onrail-xyz/utils";
4
+ import type { Layout, Item, DeriveType } from "@onrail-xyz/binary-layout";
5
+ import { serialize } from "@onrail-xyz/binary-layout";
6
+ import { hashItem } from "@onrail-xyz/common";
7
+ import { keccak256 } from "./hashing.js";
8
+ import type { BytesNItem } from "./layouting.js";
9
+ import { wordSize, abiAddressItem, abiBoolItem, uint256Item, bytesNItem } from "./layouting.js";
10
+
11
+ // ---- EIP-712 Types ----
12
+
13
+ export type Eip712Field = { readonly name: string; readonly type: string };
14
+
15
+ //the value a field of the given Solidity type takes in a composed message; struct-typed fields
16
+ // would need the whole `types` record to resolve and are not covered. A type known only as
17
+ // `string` takes any of them, so the message of an unknown field list is a record of values
18
+ type Eip712Value<T extends string> =
19
+ string extends T
20
+ ? Address | boolean | bigint | RoUint8Array | string
21
+ : T extends "address"
22
+ ? Address
23
+ : T extends "bool"
24
+ ? boolean
25
+ : T extends `uint${number}` | `int${number}`
26
+ ? bigint
27
+ : T extends `bytes${number}` | "bytes"
28
+ ? RoUint8Array
29
+ : T extends "string"
30
+ ? string
31
+ : never;
32
+
33
+ //the message a struct's field list describes, so a struct is spelled once; the bare spelling
34
+ // is the message of some field list, which every derived message is assignable to
35
+ export type Eip712Message<F extends RoArray<Eip712Field> = RoArray<Eip712Field>> =
36
+ { readonly [E in F[number] as E["name"]]: Eip712Value<E["type"]> };
37
+
38
+ const domainSeparatorFields = [
39
+ { name: "name", type: "string" },
40
+ { name: "version", type: "string" },
41
+ { name: "chainId", type: "uint256" },
42
+ { name: "verifyingContract", type: "address" },
43
+ { name: "salt", type: "bytes32" },
44
+ ] as const;
45
+ type DomainSeparatorField = typeof domainSeparatorFields[number];
46
+ type DomainSeparatorFieldName = DomainSeparatorField["name"];
47
+
48
+ //every field is optional: a domain uses whichever subset it declares
49
+ export type Eip712Domain = Partial<Eip712Message<typeof domainSeparatorFields>>;
50
+
51
+ export type Eip712Data<Message = StrRecord> = {
52
+ readonly types: StrRecord<RoArray<Eip712Field>>;
53
+ readonly primaryType: string;
54
+ readonly domain: Eip712Domain;
55
+ readonly message: DeepRo<Message>;
56
+ };
57
+
58
+ export type ViemTypedData<T> =
59
+ T extends RoUint8Array
60
+ ? Hex
61
+ : T extends object
62
+ ? { readonly [K in keyof T]: ViemTypedData<T[K]> }
63
+ : T;
64
+
65
+ //viem spells bytes values as hex where this package speaks bytes: every bytes leaf of domain and
66
+ // message is converted so that consumers hand the result straight to signTypedData/hashTypedData
67
+ export const toViemTypedData = <T>(value: T): ViemTypedData<T> => (
68
+ value instanceof Uint8Array
69
+ ? hex.encode(value, true)
70
+ : Array.isArray(value)
71
+ ? value.map(toViemTypedData)
72
+ : typeof value === "object" && value !== null
73
+ ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toViemTypedData(v)]))
74
+ : value
75
+ ) as ViemTypedData<T>;
76
+
77
+ // ---- encodeData ----
78
+ // A struct's encodeData puts each atomic field on a word, in field order, so a layout spelled
79
+ // from the field list serializes a message to exactly those bytes and reads one back - which
80
+ // is how a message is derived from data that already has a word-per-field layout. A dynamic
81
+ // (`bytes`, `string`), array or struct-typed field takes a word too, but holding a hash this
82
+ // layout does not compute, so such a field is rejected. Values ride at word width: a `uint8`
83
+ // field's range is its composer's to keep.
84
+
85
+ const int256Item = { binary: "int", size: wordSize } as const;
86
+
87
+ type WordItem<T extends string> =
88
+ T extends "bool"
89
+ ? typeof abiBoolItem
90
+ : T extends "address"
91
+ ? typeof abiAddressItem
92
+ : T extends `uint${number}`
93
+ ? typeof uint256Item
94
+ : T extends `int${number}`
95
+ ? typeof int256Item
96
+ : T extends `bytes${infer N extends number}`
97
+ ? BytesNItem<N>
98
+ : never;
99
+
100
+ const wordItem = (type: string): Item => {
101
+ if (type === "bool")
102
+ return abiBoolItem;
103
+ if (type === "address")
104
+ return abiAddressItem;
105
+ if (/^uint\d+$/.test(type))
106
+ return uint256Item;
107
+ if (/^int\d+$/.test(type))
108
+ return int256Item;
109
+
110
+ const bytesN = /^bytes(\d+)$/.exec(type);
111
+ if (bytesN !== null)
112
+ return bytesNItem(Number(bytesN[1]));
113
+
114
+ throw new Error(`EIP-712 type '${type}' has no encodeData word - only atomic types are encoded`);
115
+ };
116
+
117
+ export type Eip712EncodeDataLayout<F extends RoArray<Eip712Field>> =
118
+ { readonly [E in F[number] as E["name"]]: WordItem<E["type"]> };
119
+
120
+ export const eip712EncodeDataLayout =
121
+ <const F extends RoArray<Eip712Field>>(fields: F): Eip712EncodeDataLayout<F> =>
122
+ Object.fromEntries(fields.map(f => [f.name, wordItem(f.type)])) as Eip712EncodeDataLayout<F>;
123
+
124
+ // ---- Domain Separator ----
125
+
126
+ //the domain's type as viem derives it - the fields present, in the standard's order - so a message
127
+ // composed with it hashes identically on both sides
128
+ export const eip712DomainType = (domain: Eip712Domain) =>
129
+ domainSeparatorFields.filter(f => domain[f.name] !== undefined);
130
+
131
+ const associatedLayoutItem = {
132
+ name: hashItem,
133
+ version: hashItem,
134
+ chainId: { binary: "uint", size: wordSize },
135
+ verifyingContract: abiAddressItem,
136
+ salt: hashItem,
137
+ } as const satisfies Record<DomainSeparatorFieldName, Layout>;
138
+
139
+ const hashString = (s: string) => keccak256(utf8.encode(s));
140
+
141
+ const typeString = (fields: RoArray<DomainSeparatorFieldName>) =>
142
+ "EIP712Domain("
143
+ + domainSeparatorFields //enforces correct order (iterating over fields would not)
144
+ .filter(dsf => fields.includes(dsf.name))
145
+ .map(dsf => `${dsf.type} ${dsf.name}`)
146
+ .join(",")
147
+ + ")";
148
+
149
+ const typeHash = (fields: RoArray<DomainSeparatorFieldName>) => hashString(typeString(fields));
150
+
151
+ const typeHashItem = (fields: RoArray<DomainSeparatorFieldName>) =>
152
+ ({ binary: "bytes", fixed: typeHash(fields) } as const);
153
+
154
+ //record key order is wire order, so keys are inserted in domainSeparatorFields order; the
155
+ // type is orderless either way (record types carry no key order)
156
+ type SeparatorLayout<F extends DomainSeparatorFieldName> = Simplify<
157
+ { readonly typeHash: ReturnType<typeof typeHashItem> } &
158
+ { readonly [K in F]: typeof associatedLayoutItem[K] }
159
+ >;
160
+
161
+ const separatorLayout =
162
+ <const F extends RoArray<DomainSeparatorFieldName>>(fields: F): SeparatorLayout<F[number]> => ({
163
+ typeHash: typeHashItem(fields),
164
+ ...Object.fromEntries(
165
+ domainSeparatorFields //enforces correct order (iterating over fields would not)
166
+ .filter(dsf => fields.includes(dsf.name))
167
+ .map(dsf => [dsf.name, associatedLayoutItem[dsf.name]]),
168
+ ),
169
+ } as any);
170
+
171
+ const matchesHash =
172
+ <const L extends Layout>(layout: L, candidate: DeriveType<L>, expected: RoUint8Array) =>
173
+ bytes.equals(keccak256(serialize(layout, candidate)), expected);
174
+
175
+ // ---- Domain Resolution ----
176
+
177
+ // Reconstructs the domain of tokens that only expose DOMAIN_SEPARATOR() by brute-forcing the
178
+ // version field against the on-chain hash. Covers the { name, chainId, verifyingContract } domain
179
+ // with an optional version, i.e. everything that follows EIP-2612's reference implementation.
180
+
181
+ const typicalDomainFields = ["name", "chainId", "verifyingContract"] as const;
182
+ const unversionedLayout = separatorLayout(typicalDomainFields);
183
+ const versionedLayout = separatorLayout([...typicalDomainFields, "version"]);
184
+
185
+ const guessDomainSeparatorVersion = (
186
+ known: DeriveType<typeof unversionedLayout>,
187
+ domainSeparator: RoUint8Array,
188
+ guesses: RoArray<string>,
189
+ ) => {
190
+ for (const version of guesses)
191
+ if (matchesHash(versionedLayout, { ...known, version: hashString(version) }, domainSeparator))
192
+ return version;
193
+
194
+ throw new Error("Could not determine domain separator version");
195
+ };
196
+
197
+ //reconstructs the EIP-712 domain from on-chain data, guesses the version if needed
198
+ export const guessEip712Domain = (
199
+ name: string,
200
+ verifyingContract: Address,
201
+ chainId: bigint,
202
+ domainSeparator: RoUint8Array,
203
+ versionGuesses: RoArray<string> = ["1", "2", "0"],
204
+ ) => {
205
+ const known = { name: hashString(name), chainId, verifyingContract } as const;
206
+ return {
207
+ name,
208
+ ...(matchesHash(unversionedLayout, known, domainSeparator)
209
+ ? {}
210
+ : { version: guessDomainSeparatorVersion(known, domainSeparator, versionGuesses) }
211
+ ),
212
+ chainId,
213
+ verifyingContract,
214
+ } as const;
215
+ };
package/src/erc20.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { Address } from "viem";
2
+ import type { OptionalArg } from "@onrail-xyz/utils";
3
+ import { utf8Conversion, withCustom } from "@onrail-xyz/binary-layout";
4
+ import type { KindWithAtomic } from "@onrail-xyz/amount";
5
+ import { paddedSlotLayout, evmAmountItem,
6
+ abiEncodedBytesItem, sigVariant, addressParam, amountParam } from "./layouting.js";
7
+ import type { ContractMethods } from "./client.js";
8
+ import { abiFunction, contractFromSpec } from "./client.js";
9
+
10
+ const abiStringItem = withCustom(abiEncodedBytesItem(), utf8Conversion);
11
+ const decimalsItem = paddedSlotLayout({ binary: "uint", size: 1 } as const);
12
+
13
+ const from = addressParam("from" );
14
+ const to = addressParam("to" );
15
+ const owner = addressParam("owner" );
16
+ const spender = addressParam("spender");
17
+
18
+ const valueParam = <const K extends KindWithAtomic | undefined = undefined>(
19
+ ...kind: OptionalArg<K>
20
+ ) =>
21
+ amountParam("value", "uint256", ...kind);
22
+
23
+ const erc20Spec = <const K extends KindWithAtomic | undefined = undefined>(
24
+ ...kind: OptionalArg<K>
25
+ ) => {
26
+ const value = valueParam(...kind);
27
+ const amountItem = evmAmountItem(...kind);
28
+ const abiF = abiFunction;
29
+ return [
30
+ abiF("name", [], () => ({}), abiStringItem),
31
+ abiF("symbol", [], () => ({}), abiStringItem),
32
+ abiF("decimals", [], () => ({}), decimalsItem),
33
+ abiF("totalSupply", [], () => ({}), amountItem),
34
+ abiF("balanceOf", [owner], owner => ({ owner }), amountItem),
35
+ abiF("allowance", [owner, spender], (owner, spender) => ({ owner, spender }), amountItem),
36
+ abiF("approve", [spender, value], (spender, value) => ({ spender, value }) ),
37
+ abiF("transfer", [to, value], (to, value) => ({ to, value }) ),
38
+ abiF("transferFrom", [from, to, value], (from, to, value) => ({ from, to, value }) ),
39
+ ] as const;
40
+ };
41
+
42
+ export const erc20 = <const K extends KindWithAtomic | undefined = undefined>(
43
+ contract: Address,
44
+ ...kind: OptionalArg<K>
45
+ ): ContractMethods<ReturnType<typeof erc20Spec<K>>> =>
46
+ contractFromSpec(contract, erc20Spec(...kind));
47
+
48
+ //`Transfer` and `Approval` as `buildParseEvent` variants; `allowanceAdjusters` emits the latter
49
+ export const erc20Events = <const K extends KindWithAtomic | undefined = undefined>(
50
+ ...kind: OptionalArg<K>
51
+ ) => {
52
+ const value = valueParam(...kind);
53
+ return [
54
+ sigVariant("Transfer", from, to, value),
55
+ sigVariant("Approval", owner, spender, value),
56
+ ] as const;
57
+ };
58
+
59
+ const allowanceAdjustersSpec = <const K extends KindWithAtomic | undefined = undefined>(
60
+ ...kind: OptionalArg<K>
61
+ ) => {
62
+ const addedValue = amountParam("addedValue", "uint256", ...kind);
63
+ const subtractedValue = amountParam("subtractedValue", "uint256", ...kind);
64
+ return [
65
+ abiFunction("increaseAllowance", [spender, addedValue],
66
+ (spender, addedValue) => ({ spender, addedValue })),
67
+ abiFunction("decreaseAllowance", [spender, subtractedValue],
68
+ (spender, subtractedValue) => ({ spender, subtractedValue })),
69
+ ] as const;
70
+ };
71
+
72
+ export const allowanceAdjusters = <const K extends KindWithAtomic | undefined = undefined>(
73
+ contract: Address,
74
+ ...kind: OptionalArg<K>
75
+ ): ContractMethods<ReturnType<typeof allowanceAdjustersSpec<K>>> =>
76
+ contractFromSpec(contract, allowanceAdjustersSpec(...kind));
package/src/erc3009.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type { Address } from "viem";
2
+ import type { RoUint8Array, 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, deadlineItem, evmTimestampItem, abiBoolItem,
6
+ abiSignatureItem, sigVariant, abiParam, amountParam, addressParam } 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 from = addressParam("from" );
12
+ const to = addressParam("to" );
13
+ const authorizer = addressParam("authorizer");
14
+ //the standard's nonce is a caller-chosen bytes32, unrelated to EIP-2612's sequential uint256
15
+ const nonce = abiParam("nonce", "bytes32", hashItem );
16
+ const validAfter = abiParam("validAfter", "uint256", evmTimestampItem);
17
+ const validBefore = abiParam("validBefore", "uint256", deadlineItem );
18
+ const signature = abiParam("signature", "uint8,bytes32,bytes32", abiSignatureItem);
19
+
20
+ const erc3009Spec = <const K extends KindWithAtomic | undefined = undefined>(
21
+ ...kind: OptionalArg<K>
22
+ ) => {
23
+ const value = amountParam("value", "uint256", ...kind);
24
+
25
+ //one parameter list under two names: the receiving variant binds the submitter to be `to`
26
+ const withAuthorization = <N extends string>(name: N) =>
27
+ abiFunction(name, [from, to, value, validAfter, validBefore, nonce, signature],
28
+ (from, to, value, validAfter, validBefore, nonce, signature) =>
29
+ ({ from, to, value, validAfter, validBefore, nonce, signature }));
30
+
31
+ return [
32
+ abiFunction("authorizationState", [authorizer, nonce],
33
+ (authorizer, nonce) => ({ authorizer, nonce }), abiBoolItem),
34
+ withAuthorization("transferWithAuthorization"),
35
+ withAuthorization("receiveWithAuthorization"),
36
+ abiFunction("cancelAuthorization", [authorizer, nonce, signature],
37
+ (authorizer, nonce, signature) => ({ authorizer, nonce, signature })),
38
+ ] as const;
39
+ };
40
+
41
+ export const erc3009 = <const K extends KindWithAtomic | undefined = undefined>(
42
+ contract: Address,
43
+ ...kind: OptionalArg<K>
44
+ ): ContractMethods<ReturnType<typeof erc3009Spec<K>>> =>
45
+ contractFromSpec(contract, erc3009Spec(...kind));
46
+
47
+ //the standard's events as `buildParseEvent` variants; its reverts are left to implementations
48
+ export const erc3009Events = [
49
+ sigVariant("AuthorizationUsed", authorizer, nonce),
50
+ sigVariant("AuthorizationCanceled", authorizer, nonce),
51
+ ] as const;
52
+
53
+ // ---- EIP-3009 Authorization Messages ----
54
+
55
+ const authorizationType = [
56
+ { name: "from", type: "address" },
57
+ { name: "to", type: "address" },
58
+ { name: "value", type: "uint256" },
59
+ { name: "validAfter", type: "uint256" },
60
+ { name: "validBefore", type: "uint256" },
61
+ { name: "nonce", type: "bytes32" },
62
+ ] as const;
63
+
64
+ const cancelAuthorizationType = [
65
+ { name: "authorizer", type: "address" },
66
+ { name: "nonce", type: "bytes32" },
67
+ ] as const;
68
+
69
+ export type Eip3009AuthorizationMessage = Eip712Message<typeof authorizationType>;
70
+ export type Eip3009CancelMessage = Eip712Message<typeof cancelAuthorizationType>;
71
+
72
+ export const randomAuthorizationNonce = (): Uint8Array =>
73
+ crypto.getRandomValues(new Uint8Array(32));
74
+
75
+ //one struct under two names: the receiving variant additionally binds the submitter to be `to`
76
+ const composeAuthorization = (
77
+ primaryType: "TransferWithAuthorization" | "ReceiveWithAuthorization",
78
+ ) => (
79
+ from: Address,
80
+ to: Address,
81
+ value: AmountOrAtomic,
82
+ domain: Eip712Domain,
83
+ nonce: RoUint8Array,
84
+ validBefore: Deadline = "infinity",
85
+ validAfter: Date = new Date(0),
86
+ ) => {
87
+ const atomic = toAtomicIfAmount(value);
88
+ if (atomic < 0n)
89
+ throw new Error("Value must not be negative");
90
+
91
+ return {
92
+ types: { EIP712Domain: eip712DomainType(domain), [primaryType]: authorizationType },
93
+ primaryType,
94
+ domain,
95
+ message: { from,
96
+ to,
97
+ value: atomic,
98
+ validAfter: evmTimestampItem.custom.from(validAfter),
99
+ validBefore: deadlineItem.custom.from(validBefore),
100
+ nonce,
101
+ },
102
+ } as const satisfies Eip712Data<Eip3009AuthorizationMessage>;
103
+ };
104
+
105
+ //compose an EIP-3009 authorization; `toViemTypedData` bridges it to viem's signing calls
106
+ export const composeTransferWithAuthorizationMsg =
107
+ composeAuthorization("TransferWithAuthorization");
108
+ export const composeReceiveWithAuthorizationMsg =
109
+ composeAuthorization("ReceiveWithAuthorization");
110
+
111
+ export const composeCancelAuthorizationMsg = (
112
+ authorizer: Address,
113
+ domain: Eip712Domain,
114
+ nonce: RoUint8Array,
115
+ ) => ({
116
+ types: { EIP712Domain: eip712DomainType(domain),
117
+ CancelAuthorization: cancelAuthorizationType,
118
+ },
119
+ primaryType: "CancelAuthorization",
120
+ domain,
121
+ message: { authorizer, nonce },
122
+ } as const satisfies Eip712Data<Eip3009CancelMessage>);
package/src/hashing.ts ADDED
@@ -0,0 +1 @@
1
+ export { keccak_256 as keccak256, sha3_256 } from "@noble/hashes/sha3.js";
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./client.js";
2
+ export * from "./eip712.js";
3
+ export * from "./erc20.js";
4
+ export * from "./erc3009.js";
5
+ export * from "./hashing.js";
6
+ export * from "./layouting.js";
7
+ export * from "./parsing.js";
8
+ export * from "./permit.js";
9
+ export * from "./query.js";