1use alloy_primitives::{Address, Bytes, B256, U256, keccak256, ChainId};
6use alloy_rlp::Encodable;
7
8pub const TX_TYPE_USD_MULTI_TOKEN: u8 = 0x7a;
10
11pub const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x7b;
13
14#[derive(Debug, Clone, Default)]
16pub struct AccessListItem {
17 pub address: Address,
18 pub storage_keys: Vec<B256>,
19}
20
21#[derive(Debug, Clone)]
23pub struct TxBuilder {
24 pub chain_id: u64,
25 pub nonce: u64,
26 pub gas_limit: u64,
27 pub to: Option<Address>,
29 pub value: U256,
30 pub data: Bytes,
31 pub access_list: Vec<AccessListItem>, pub max_priority_fee_per_gas: u128,
33 pub max_fee_per_gas: u128,
34 pub fee_token: Address,
35 pub fee_payer: Option<Address>,
36 pub max_fee_per_gas_usd: Option<u128>,
37 pub fee_payer_signature: Option<Bytes>,
38}
39
40impl Default for TxBuilder {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl TxBuilder {
47 pub fn new() -> Self {
48 Self {
49 chain_id: 0,
50 nonce: 0,
51 gas_limit: 21000,
52 to: Some(Address::ZERO),
53 value: U256::ZERO,
54 data: Bytes::new(),
55 access_list: Vec::new(), max_priority_fee_per_gas: 0,
57 max_fee_per_gas: 0,
58 fee_token: Address::ZERO,
59 fee_payer: None, max_fee_per_gas_usd: Some(40_000_000_000), fee_payer_signature: None,
62 }
63 }
64
65 pub fn chain_id(mut self, chain_id: u64) -> Self {
66 self.chain_id = chain_id;
67 self
68 }
69
70 pub fn nonce(mut self, nonce: u64) -> Self {
71 self.nonce = nonce;
72 self
73 }
74
75 pub fn gas_limit(mut self, gas_limit: u64) -> Self {
76 self.gas_limit = gas_limit;
77 self
78 }
79
80 pub fn to(mut self, to: Address) -> Self {
81 self.to = Some(to);
82 self
83 }
84
85 pub fn to_address(mut self, address: Address) -> Self {
87 self.to = Some(address);
88 self
89 }
90
91 pub fn create(mut self) -> Self {
93 self.to = None;
94 self
95 }
96
97 pub fn value(mut self, value: U256) -> Self {
98 self.value = value;
99 self
100 }
101
102 pub fn data(mut self, data: Bytes) -> Self {
103 self.data = data;
104 self
105 }
106
107 pub fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self {
108 self.max_priority_fee_per_gas = max_priority_fee_per_gas;
109 self
110 }
111
112 pub fn max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self {
113 self.max_fee_per_gas = max_fee_per_gas;
114 self
115 }
116
117 pub fn fee_token(mut self, fee_token: Address) -> Self {
118 self.fee_token = fee_token;
119 self
120 }
121
122 pub fn fee_payer(mut self, fee_payer: Option<Address>) -> Self {
123 self.fee_payer = fee_payer;
124 self
125 }
126
127 pub fn max_fee_per_gas_usd(mut self, max_fee_per_gas_usd: u128) -> Self {
128 self.max_fee_per_gas_usd = Some(max_fee_per_gas_usd);
129 self
130 }
131
132 pub fn fee_payer_signature(mut self, signature: Bytes) -> Self {
133 self.fee_payer_signature = Some(signature);
134 self
135 }
136
137 pub fn erc20_transfer(mut self, token: Address, to: Address, amount: U256) -> Self {
139 let mut data = vec![0xa9, 0x05, 0x9c, 0xbb];
141 let mut recipient = [0u8; 32];
143 recipient[12..].copy_from_slice(to.as_slice());
144 data.extend_from_slice(&recipient);
145 let mut amount_padded = [0u8; 32];
147 let amount_bytes: [u8; 32] = amount.to_be_bytes();
148 amount_padded.copy_from_slice(&amount_bytes);
149 data.extend_from_slice(&amount_padded);
150
151 self.to = Some(token);
152 self.data = Bytes::from(data);
153 self
154 }
155
156 pub fn build(&self) -> TxFields {
158 TxFields {
159 chain_id: self.chain_id,
160 nonce: self.nonce,
161 gas_limit: self.gas_limit,
162 to: self.to,
163 value: self.value,
164 data: self.data.clone(),
165 access_list: self.access_list.clone(), max_priority_fee_per_gas: self.max_priority_fee_per_gas,
167 max_fee_per_gas: self.max_fee_per_gas,
168 fee_token: self.fee_token,
169 fee_payer: self.fee_payer.unwrap_or(Address::ZERO),
172 max_fee_per_gas_usd: self.max_fee_per_gas_usd.unwrap_or(40_000_000_000),
173 fee_payer_signature: self.fee_payer_signature.clone(),
174 }
175 }
176
177 pub fn signature_hash(&self) -> B256 {
181 let fields = self.build();
182
183 let payload_len = fields.payload_len_for_signing();
185
186 let mut full_buf = Vec::new();
188 full_buf.push(TX_TYPE_USD_MULTI_TOKEN);
189 alloy_rlp::Header { list: true, payload_length: payload_len }.encode(&mut full_buf);
190
191 fields.encode_for_signing(&mut full_buf);
193
194 let hash = keccak256(&full_buf);
195
196 hash
197 }
198
199 pub fn sign(&self, signer: &impl Signer) -> Result<SignedTx, Box<dyn std::error::Error>> {
201 let sender = signer.address();
202
203 let fee_payer = self.fee_payer.unwrap_or(sender);
207
208 let hash = {
210 let mut resolved = self.clone();
211 resolved.fee_payer = Some(fee_payer);
212 resolved.signature_hash()
213 };
214 let signature = signer.sign_hash(&hash)?;
215
216 let fee_payer_signature = if self.fee_payer_signature.is_none() {
220 if fee_payer == sender {
221 Some(signature.to_bytes())
223 } else {
224 return Err("fee_payer != sender requires external fee_payer_signature".into());
226 }
227 } else {
228 self.fee_payer_signature.clone()
229 };
230
231 let fields = TxFields {
233 chain_id: self.chain_id,
234 nonce: self.nonce,
235 gas_limit: self.gas_limit,
236 to: self.to,
237 value: self.value.clone(),
238 data: self.data.clone(),
239 access_list: self.access_list.clone(), max_priority_fee_per_gas: self.max_priority_fee_per_gas,
241 max_fee_per_gas: self.max_fee_per_gas,
242 fee_token: self.fee_token,
243 fee_payer,
244 max_fee_per_gas_usd: self.max_fee_per_gas_usd.unwrap_or(40_000_000_000),
245 fee_payer_signature,
246 };
247
248 let y_parity = signature.y_parity();
249
250 let raw_tx = fields.encode_signed(y_parity, signature.r(), signature.s());
253
254 let tx_hash = keccak256(&raw_tx);
255
256 Ok(SignedTx {
257 raw_transaction: format!("0x{}", hex::encode(&raw_tx)),
258 transaction_hash: tx_hash,
259 chain_id: fields.chain_id,
260 nonce: fields.nonce,
261 gas_limit: fields.gas_limit,
262 to: fields.to,
263 value: fields.value,
264 data: fields.data,
265 max_priority_fee_per_gas: fields.max_priority_fee_per_gas,
266 max_fee_per_gas: fields.max_fee_per_gas,
267 fee_token: fields.fee_token,
268 fee_payer: fields.fee_payer,
269 max_fee_per_gas_usd_attodollars: fields.max_fee_per_gas_usd,
270 fee_payer_signature: fields.fee_payer_signature,
271 v: y_parity,
272 r: signature.r(),
273 s: signature.s(),
274 })
275 }
276}
277
278pub trait Signer {
280 fn sign_hash(&self, hash: &B256) -> Result<Signature, Box<dyn std::error::Error>>;
281 fn address(&self) -> Address;
282}
283
284#[derive(Debug, Clone)]
286pub struct Signature {
287 pub r: B256,
288 pub s: B256,
289 pub v: u8,
290}
291
292impl Signature {
293 pub fn new(r: B256, s: B256, v: u8) -> Self {
294 Self { r, s, v }
295 }
296
297 pub fn y_parity(&self) -> u8 {
298 self.v
299 }
300
301 pub fn r(&self) -> B256 {
302 self.r
303 }
304
305 pub fn s(&self) -> B256 {
306 self.s
307 }
308
309 pub fn to_bytes(&self) -> Bytes {
312 let mut buf = Vec::with_capacity(65);
313 buf.extend_from_slice(self.r.as_slice()); buf.extend_from_slice(self.s.as_slice()); let v_legacy = 27 + self.v; buf.push(v_legacy); Bytes::from(buf)
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct SignedTx {
324 pub raw_transaction: String,
325 pub transaction_hash: B256,
326 pub chain_id: u64,
327 pub nonce: u64,
328 pub gas_limit: u64,
329 pub to: Option<Address>,
330 pub value: U256,
331 pub data: Bytes,
332 pub max_priority_fee_per_gas: u128,
333 pub max_fee_per_gas: u128,
334 pub fee_token: Address,
335 pub fee_payer: Address,
336 pub max_fee_per_gas_usd_attodollars: u128,
337 pub fee_payer_signature: Option<Bytes>,
338 pub v: u8,
339 pub r: B256,
340 pub s: B256,
341}
342
343#[derive(Debug, Clone)]
345pub struct TxFields {
346 pub chain_id: u64,
347 pub nonce: u64,
348 pub gas_limit: u64,
349 pub to: Option<Address>,
351 pub value: U256,
352 pub data: Bytes,
353 pub access_list: Vec<AccessListItem>, pub max_priority_fee_per_gas: u128,
355 pub max_fee_per_gas: u128,
356 pub fee_token: Address,
357 pub fee_payer: Address,
358 pub max_fee_per_gas_usd: u128,
359 pub fee_payer_signature: Option<Bytes>,
360}
361
362impl TxFields {
363 fn encode_access_list(access_list: &[AccessListItem], out: &mut Vec<u8>) {
365 if access_list.is_empty() {
367 alloy_rlp::Header { list: true, payload_length: 0 }.encode(out);
369 } else {
370 let payload_len = access_list.iter().map(|item| {
371 item.address.length() + item.storage_keys.iter().map(|k| k.length()).sum::<usize>()
372 }).sum::<usize>() + access_list.len(); alloy_rlp::Header { list: true, payload_length: payload_len }.encode(out);
375
376 for item in access_list {
377 let inner_len = item.address.length() +
379 item.storage_keys.iter().map(|k| k.length()).sum::<usize>() +
380 1; alloy_rlp::Header { list: true, payload_length: inner_len }.encode(out);
382 item.address.encode(out);
383 if item.storage_keys.is_empty() {
385 alloy_rlp::Header { list: true, payload_length: 0 }.encode(out);
386 } else {
387 let keys_len: usize = item.storage_keys.iter().map(|k| k.length()).sum();
388 alloy_rlp::Header { list: true, payload_length: keys_len }.encode(out);
389 for key in &item.storage_keys {
390 key.encode(out);
391 }
392 }
393 }
394 }
395 }
396
397 pub fn encode_for_signing(&self, out: &mut Vec<u8>) {
400 ChainId::from(self.chain_id).encode(out);
404 self.nonce.encode(out);
405 self.max_priority_fee_per_gas.encode(out);
406 self.max_fee_per_gas.encode(out);
407 self.gas_limit.encode(out);
408 Self::encode_to(&self.to, out);
410 self.value.encode(out);
411 self.data.encode(out);
412 Self::encode_access_list(&self.access_list, out);
414 self.fee_token.encode(out);
415 self.fee_payer.encode(out);
416 self.max_fee_per_gas_usd.encode(out);
417 }
418
419 fn encode_to(to: &Option<Address>, out: &mut Vec<u8>) {
421 match to {
422 Some(addr) => addr.encode(out),
423 None => out.push(0x80), }
425 }
426
427 fn to_length(to: &Option<Address>) -> usize {
429 match to {
430 Some(addr) => addr.length(),
431 None => 1, }
433 }
434
435 fn access_list_len(access_list: &[AccessListItem]) -> usize {
436 if access_list.is_empty() {
437 return 1; }
439 let keys_len: usize = access_list.iter()
440 .map(|item| item.address.length() + item.storage_keys.iter().map(|k| k.length()).sum::<usize>() + 1)
441 .sum();
442 1 + keys_len
444 }
445
446 fn payload_len_for_signing(&self) -> usize {
447 self.chain_id.length() +
448 self.nonce.length() +
449 self.max_priority_fee_per_gas.length() +
450 self.max_fee_per_gas.length() +
451 self.gas_limit.length() +
452 Self::to_length(&self.to) +
453 self.value.length() +
454 self.data.length() +
455 Self::access_list_len(&self.access_list) +
456 self.fee_token.length() +
457 self.fee_payer.length() +
458 self.max_fee_per_gas_usd.length()
459 }
460
461 pub fn encode_signed(&self, y_parity: u8, r: B256, s: B256) -> Vec<u8> {
464 let mut buf = Vec::new();
465
466 buf.push(TX_TYPE_USD_MULTI_TOKEN);
468
469 let payload_len = self.payload_len_for_signed(&r, &s);
471 alloy_rlp::Header { list: true, payload_length: payload_len }.encode(&mut buf);
472
473 self.chain_id.encode(&mut buf);
474 self.nonce.encode(&mut buf);
475 self.max_priority_fee_per_gas.encode(&mut buf);
476 self.max_fee_per_gas.encode(&mut buf);
477 self.gas_limit.encode(&mut buf);
478 Self::encode_to(&self.to, &mut buf);
479 self.value.encode(&mut buf);
480 self.data.encode(&mut buf);
481 Self::encode_access_list(&self.access_list, &mut buf);
483 self.fee_token.encode(&mut buf);
484 self.fee_payer.encode(&mut buf);
485 self.max_fee_per_gas_usd.encode(&mut buf);
486
487 if let Some(ref sig) = self.fee_payer_signature {
489 sig.encode(&mut buf);
490 } else {
491 Bytes::new().encode(&mut buf);
492 }
493
494 y_parity.encode(&mut buf);
496 U256::from_be_bytes(r.0).encode(&mut buf);
497 U256::from_be_bytes(s.0).encode(&mut buf);
498
499 buf
500 }
501
502 fn payload_len_for_signed(&self, r: &B256, s: &B256) -> usize {
503 let fee_payer_sig_len = self.fee_payer_signature.as_ref()
504 .map(|sig| sig.length())
505 .unwrap_or_else(|| Bytes::new().length());
506
507 self.payload_len_for_signing() +
508 fee_payer_sig_len +
509 1 + U256::from_be_bytes(r.0).length() +
511 U256::from_be_bytes(s.0).length()
512 }
513}
514
515pub fn fee_payer_signature_hash(
523 chain_id: u64,
524 nonce: u64,
525 gas_limit: u64,
526 fee_token: Address,
527 fee_payer: Address,
528 max_fee_per_gas_usd: u128,
529 sender: Address,
530) -> B256 {
531 use alloy_rlp::{Encodable, Header};
532
533 let payload_len = chain_id.length()
535 + nonce.length()
536 + gas_limit.length()
537 + fee_token.length()
538 + fee_payer.length()
539 + max_fee_per_gas_usd.length()
540 + sender.length();
541
542 let mut buf = Vec::with_capacity(1 + Header { list: true, payload_length: payload_len }.length() + payload_len);
544
545 buf.push(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
547
548 Header { list: true, payload_length: payload_len }.encode(&mut buf);
550
551 chain_id.encode(&mut buf);
553 nonce.encode(&mut buf);
554 gas_limit.encode(&mut buf);
555 fee_token.encode(&mut buf);
556 fee_payer.encode(&mut buf);
557 max_fee_per_gas_usd.encode(&mut buf);
558 sender.encode(&mut buf);
559
560 let hash = keccak256(&buf);
561
562 hash
563}
564
565pub fn create_transaction() -> TxBuilder {
567 TxBuilder::new()
568}