-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtx-validator.ts
251 lines (233 loc) · 7.52 KB
/
tx-validator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import { Address, RawTxMap, add0x } from './index.js';
import { parseDecimal } from './formatters.js';
export type Unit = 'eth' | 'wei' | 'gwei';
type SNB = string | number | bigint;
export type HumanizedTx = {
from?: string;
to: string;
value: SNB;
maxFeePerGas: SNB;
maxPriorityFeePerGas: SNB;
nonce: SNB;
data?: string;
gasLimit?: SNB;
amountUnit?: Unit;
maxFeePerGasUnit?: Unit;
maxPriorityFeePerGasUnit?: Unit;
chainId?: number;
};
const GWEI_PRECISION = 9;
const ETHER_PRECISION = 18;
const GWEI = 10n ** BigInt(GWEI_PRECISION);
const ETHER = 10n ** BigInt(ETHER_PRECISION);
// const MICROETH = 10n ** 12n;
const MAX_AMOUNT = ETHER * 100000000n; // 100m ether
const MAX_GAS_PRICE = Number(GWEI * 10000n); // 10,000 gwei. Arbitrage HFT bots can use more.
// etherscan.io/chart/gasprice
const MIN_GAS_LIMIT = 21000;
const MAX_GAS_LIMIT = 20000000; // 20m wei. It's dynamic; a block limit in 2021 is 12m.
const MAX_NONCE = 10000000; // 10M
const MAX_DATA_SIZE = 10000000;
function minmax(val: bigint, min: bigint, max: bigint, err?: string): true | string;
function minmax(val: number, min: number, max: number, err?: string): true | string;
function minmax(
val: number | bigint,
min: number | bigint,
max: number | bigint,
err?: string
): true | string {
if (!err) err = `>= ${min} and <= ${max}`;
if (Number.isNaN(val) || val < min || val > max) throw new Error(`Must be ${err}`);
return true;
}
function ensureNot16x(val: SNB, isBig = false) {
if (typeof val === 'string' && val.startsWith('0x')) {
return isBig ? BigInt(val) : Number.parseInt(val, 16);
}
return val;
}
const checks = {
nonce(num: number) {
return minmax(num, 0, MAX_NONCE);
},
maxFeePerGas(num: number) {
return minmax(num, 1, MAX_GAS_PRICE, '>= 1 wei and < 10000 gwei');
},
maxPriorityFeePerGas(num: number) {
return minmax(num, 0, MAX_GAS_PRICE, '>= 1 wei and < 10000 gwei');
},
gasLimit(num: number) {
return minmax(num, MIN_GAS_LIMIT, MAX_GAS_LIMIT);
},
to(addr: string) {
if (addr.length !== 40 && addr.length !== 42)
throw new Error('Address length must be 40 or 42 symbols');
addr = add0x(addr);
if (!/^0x[0-9a-f]+$/i.test(addr)) throw new Error('Address must be hex');
if (!Address.verifyChecksum(addr)) throw new Error('Address checksum does not match');
return true;
},
value(num: bigint) {
return minmax(num, 0n, MAX_AMOUNT, '>= 0 and < 100M eth');
},
data(val?: string) {
if (typeof val === 'string' && val.length > MAX_DATA_SIZE) throw new Error('Data is too big');
return true;
},
chainId(num?: number) {
if (!num) return true;
return minmax(num, 1, 2 ** 32 - 1, '>= 1 and <= 2**32-1');
},
};
function parseHex(val: string) {
if (val === '0x') val = '0x00';
return Number.parseInt(val, 16);
}
export function parseUnit(val: SNB, unit: Unit) {
const str = ensureNot16x(val, true).toString();
if (unit === 'wei') return BigInt(str);
let precision: number;
if (unit === 'gwei') precision = GWEI_PRECISION;
else if (unit === 'eth') precision = ETHER_PRECISION;
else throw new Error(`Wrong unit name: ${unit}`);
return parseDecimal(str, precision);
}
// Raw transaction to humanized
const r2h = {
nonce: parseHex,
maxFeePerGas: parseHex,
gasLimit: parseHex,
to: (val: string): string => Address.checksum(val),
value: (val: string): bigint => BigInt(val),
data: (val: string): string => val,
chainId: (val: string): number => (val ? parseHex(val) : 1),
};
// Humanized to raw.
const h2r = {
nonce(val: SNB): number {
return Number.parseInt(ensureNot16x(val).toString());
},
maxFeePerGas(val: SNB, opts?: Partial<HumanizedTx>): bigint {
return parseUnit(val, (opts && opts.maxFeePerGasUnit) || 'gwei');
},
maxPriorityFeePerGas(val: SNB, opts?: Partial<HumanizedTx>): bigint {
return parseUnit(val, (opts && opts.maxPriorityFeePerGasUnit) || 'gwei');
},
gasLimit(val: SNB): number {
return Number.parseInt(ensureNot16x(val).toString()) || MIN_GAS_LIMIT;
},
to(val: string, opts?: Partial<HumanizedTx>): string {
if (opts && opts.from && opts.from === val) throw new Error('Must differ from sender address');
return val;
},
value(val: SNB, opts?: Partial<HumanizedTx>): bigint {
return parseUnit(val, (opts && opts.amountUnit) || 'eth');
},
data(val?: string): string {
return val || '';
},
chainId(val: string) {
return Number.parseInt(val) || 1;
},
};
type h2rf = keyof typeof h2r;
function hasOwnProperty<X extends {}, Y extends PropertyKey>(
obj: X,
prop: Y
): obj is X & Record<Y, unknown> {
return obj.hasOwnProperty(prop);
}
function numberToHexUnpadded(num: number | bigint): string {
let hex = num.toString(16);
hex = hex.length & 1 ? `0${hex}` : hex;
return hex;
}
function dataToString(snb: SNB) {
if (snb == null) return '';
if (typeof snb === 'string') return snb;
if (typeof snb === 'number' || typeof snb === 'bigint') return numberToHexUnpadded(snb);
throw new Error('Invalid type');
}
class TransactionFieldError extends Error {
constructor(message: string, readonly errors: Record<string, string>) {
super(message + '. ' + JSON.stringify(errors));
}
}
const requiredFields = ['maxFeePerGas', 'maxPriorityFeePerGas', 'to', 'value', 'nonce'];
const optionFields = {
value: ['amountUnit'],
to: ['from'],
maxFeePerGas: ['maxFeePerGasUnit'],
maxPriorityFeePerGas: ['maxPriorityFeePerGasUnit'],
chainId: [],
};
const allOptionFields = Object.values(optionFields).flat();
export function createTxMapFromFields(fields: HumanizedTx): RawTxMap {
// prettier-ignore
const normalized = {} as RawTxMap;
const errors: Record<string, string> = {};
requiredFields.forEach((f) => {
if (fields[f as h2rf] == null) errors[f] = 'Cannot be empty';
});
Object.keys(fields).forEach((f) => {
if (allOptionFields.includes(f)) return;
const field = f as h2rf;
const opts: Record<string, SNB> = {};
if (hasOwnProperty(optionFields, field)) {
const list = optionFields[field] as (keyof HumanizedTx)[];
for (const optionalField of list) {
const ofVal = fields[optionalField];
if (ofVal != null) opts[optionalField] = ofVal;
}
}
const val = fields[field];
try {
const normVal = h2r[field](val as any, opts);
// @ts-ignore
checks[field](normVal);
normalized[field] = dataToString(normVal);
} catch (error: any) {
errors[field] = error.messages || error.message;
}
});
if (Object.keys(errors).length) throw new TransactionFieldError('Invalid transaction', errors);
Object.keys(normalized).forEach((f) => {
const field = f as keyof RawTxMap;
if (field === 'accessList') return;
normalized[field] = add0x(normalized[field]!);
});
const raw: RawTxMap = Object.assign(
{
nonce: '0x',
to: '0x',
value: '0x',
gasLimit: '0x5208',
maxFeePerGas: '0x',
data: '0x',
v: '0x',
r: '0x',
s: '0x',
chainId: 1,
},
normalized
);
return raw;
}
export function validateField(field: h2rf, val: SNB, opts?: Partial<HumanizedTx>) {
const normVal = h2r[field](val as any, opts);
// @ts-ignore
checks[field](normVal);
return dataToString(normVal);
}
export function validateFields(raw: RawTxMap) {
Object.keys(raw).forEach((f) => {
const field = f as keyof RawTxMap;
if (field === 'accessList') return;
const fn = r2h[field as keyof typeof r2h];
if (typeof fn === 'function') {
const value = raw[field];
const normVal = fn(value || '');
checks[field as keyof typeof checks](normVal as never);
}
});
}