foundry_primitives/transaction/
celo.rs1use alloy_consensus::{
4 SignableTransaction, Transaction, TxEip1559, Typed2718,
5 transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
6};
7use alloy_eips::{eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization};
8use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256};
9use alloy_rlp::{BufMut, Decodable, Encodable};
10use serde::{Deserialize, Serialize};
11
12pub const CIP64_TX_TYPE: u8 = 0x7b;
14
15#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct TxCip64 {
19 #[serde(flatten)]
21 pub inner: TxEip1559,
22 #[serde(default)]
24 pub fee_currency: Option<Address>,
25}
26
27impl RlpEcdsaEncodableTx for TxCip64 {
28 fn rlp_encoded_fields_length(&self) -> usize {
29 self.inner.rlp_encoded_fields_length()
30 + self.fee_currency.map_or(1, |address| address.length())
31 }
32
33 fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
34 self.inner.rlp_encode_fields(out);
35 match self.fee_currency {
36 Some(address) => address.encode(out),
37 None => out.put_u8(alloy_rlp::EMPTY_STRING_CODE),
38 }
39 }
40}
41
42impl RlpEcdsaDecodableTx for TxCip64 {
43 const DEFAULT_TX_TYPE: u8 = CIP64_TX_TYPE;
44
45 fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
46 let inner = TxEip1559::rlp_decode_fields(buf)?;
47 let fee_currency = match buf.first() {
48 Some(&alloy_rlp::EMPTY_STRING_CODE) => {
49 *buf = &buf[1..];
50 None
51 }
52 _ => Some(Address::decode(buf)?),
53 };
54 Ok(Self { inner, fee_currency })
55 }
56}
57
58impl Typed2718 for TxCip64 {
59 fn ty(&self) -> u8 {
60 CIP64_TX_TYPE
61 }
62}
63
64impl IsTyped2718 for TxCip64 {
65 fn is_type(type_id: u8) -> bool {
66 type_id == CIP64_TX_TYPE
67 }
68}
69
70impl SignableTransaction<Signature> for TxCip64 {
71 fn set_chain_id(&mut self, chain_id: ChainId) {
72 self.inner.chain_id = chain_id;
73 }
74
75 fn encode_for_signing(&self, out: &mut dyn BufMut) {
76 out.put_u8(CIP64_TX_TYPE);
77 self.rlp_encode(out);
78 }
79
80 fn payload_len_for_signature(&self) -> usize {
81 self.rlp_encoded_length() + 1
82 }
83}
84
85impl Transaction for TxCip64 {
86 fn chain_id(&self) -> Option<ChainId> {
87 self.inner.chain_id()
88 }
89
90 fn nonce(&self) -> u64 {
91 self.inner.nonce()
92 }
93
94 fn gas_limit(&self) -> u64 {
95 self.inner.gas_limit()
96 }
97
98 fn gas_price(&self) -> Option<u128> {
99 self.inner.gas_price()
100 }
101
102 fn max_fee_per_gas(&self) -> u128 {
103 self.inner.max_fee_per_gas()
104 }
105
106 fn max_priority_fee_per_gas(&self) -> Option<u128> {
107 self.inner.max_priority_fee_per_gas()
108 }
109
110 fn max_fee_per_blob_gas(&self) -> Option<u128> {
111 self.inner.max_fee_per_blob_gas()
112 }
113
114 fn priority_fee_or_price(&self) -> u128 {
115 self.inner.priority_fee_or_price()
116 }
117
118 fn is_dynamic_fee(&self) -> bool {
119 self.inner.is_dynamic_fee()
120 }
121
122 fn kind(&self) -> TxKind {
123 self.inner.kind()
124 }
125
126 fn is_create(&self) -> bool {
127 self.inner.is_create()
128 }
129
130 fn value(&self) -> U256 {
131 self.inner.value()
132 }
133
134 fn input(&self) -> &Bytes {
135 self.inner.input()
136 }
137
138 fn access_list(&self) -> Option<&AccessList> {
139 self.inner.access_list()
140 }
141
142 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
143 self.inner.blob_versioned_hashes()
144 }
145
146 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
147 self.inner.authorization_list()
148 }
149
150 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
151 self.inner.effective_gas_price(base_fee)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::{FoundryTransactionRequest, FoundryTxEnvelope, FoundryTypedTx};
159 use alloy_eips::eip2718::{Decodable2718, Encodable2718};
160 use alloy_primitives::{address, hex};
161 use serde_json::json;
162
163 #[test]
164 fn cip64_encoding_and_signing_payload() {
165 let tx = TxCip64 {
166 inner: TxEip1559 {
167 chain_id: 1,
168 max_priority_fee_per_gas: 1,
169 max_fee_per_gas: 2,
170 gas_limit: 21_000,
171 to: address!("1111111111111111111111111111111111111111").into(),
172 ..Default::default()
173 },
174 fee_currency: Some(address!("2222222222222222222222222222222222222222")),
175 };
176 assert_eq!(
178 tx.encoded_for_signing(),
179 hex!(
180 "7bf4018001028252089411111111111111111111111111111111111111118080c0942222222222222222222222222222222222222222"
181 )
182 );
183 let signing_hash = tx.signature_hash();
184 let signed = tx.clone().into_signed(Signature::new(U256::ONE, U256::from(2), false));
185 let envelope = FoundryTxEnvelope::Celo(signed);
186 let encoded = envelope.encoded_2718();
187 assert_eq!(
188 encoded,
189 hex!(
190 "7bf7018001028252089411111111111111111111111111111111111111118080c0942222222222222222222222222222222222222222800102"
191 )
192 );
193 assert_eq!(FoundryTxEnvelope::decode_2718(&mut encoded.as_slice()).unwrap(), envelope);
194 let mut native = tx;
195 native.fee_currency = None;
196 assert_ne!(native.signature_hash(), signing_hash);
197 let native = FoundryTxEnvelope::Celo(native.into_signed(Signature::new(
198 U256::ONE,
199 U256::from(2),
200 false,
201 )));
202 assert_eq!(
203 FoundryTxEnvelope::decode_2718(&mut native.encoded_2718().as_slice()).unwrap(),
204 native
205 );
206 }
207
208 #[test]
209 fn cip64_request_classification_and_conflicts() {
210 let currency = "0x2222222222222222222222222222222222222222";
211 let request = json!({"feeCurrency":currency,"chainId":"0x1","nonce":"0x0","gas":"0x5208","maxFeePerGas":"0x2","maxPriorityFeePerGas":"0x1","to":"0x1111111111111111111111111111111111111111"});
212 let parsed: FoundryTransactionRequest = serde_json::from_value(request.clone()).unwrap();
213 let typed = parsed.build_typed_tx().unwrap();
214 assert!(matches!(typed, FoundryTypedTx::Celo(_)));
215 assert_eq!(
216 serde_json::to_value(FoundryTransactionRequest::from(typed)).unwrap()["feeCurrency"],
217 currency
218 );
219 for (field, value) in [
220 ("type", json!("0x2")),
221 ("gasPrice", json!("0x1")),
222 ("authorizationList", json!([])),
223 ("blobVersionedHashes", json!([])),
224 ("feeToken", json!(currency)),
225 ] {
226 let mut invalid = request.clone();
227 invalid[field] = value;
228 assert!(
229 serde_json::from_value::<FoundryTransactionRequest>(invalid).is_err(),
230 "{field}"
231 );
232 }
233 let native: FoundryTransactionRequest =
234 serde_json::from_value(json!({"type":"0x7b"})).unwrap();
235 assert!(matches!(native, FoundryTransactionRequest::Celo(_)));
236 let ordinary: FoundryTransactionRequest =
237 serde_json::from_value(json!({"feeCurrency":null})).unwrap();
238 assert!(ordinary.is_ethereum());
239 }
240}