Skip to content

Commit 16f5cc5

Browse files
chore(uov7): Serialize UserOperation numbers as hex 0x prefixed (#4353)
1 parent 2bf1de3 commit 16f5cc5

File tree

9 files changed

+120
-55
lines changed

9 files changed

+120
-55
lines changed

rust/tw_encoding/src/hex.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,31 @@ pub mod as_hex {
135135
}
136136
}
137137

138+
pub mod as_hex_prefixed {
139+
use crate::hex::encode;
140+
use serde::{Deserializer, Serialize, Serializer};
141+
use std::fmt;
142+
143+
/// Serializes the `value` as a `0x` prefixed hex.
144+
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
145+
where
146+
T: AsRef<[u8]>,
147+
S: Serializer,
148+
{
149+
encode(value, true).serialize(serializer)
150+
}
151+
152+
pub fn deserialize<'de, D, T, E>(deserializer: D) -> Result<T, D::Error>
153+
where
154+
D: Deserializer<'de>,
155+
T: for<'a> TryFrom<&'a [u8], Error = E>,
156+
E: fmt::Debug,
157+
{
158+
// `as_hex::deserialize` handles the prefix already.
159+
super::as_hex::deserialize(deserializer)
160+
}
161+
}
162+
138163
#[cfg(test)]
139164
mod tests {
140165
use super::*;

rust/tw_evm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ tw_hash = { path = "../tw_hash" }
1515
tw_keypair = { path = "../tw_keypair" }
1616
tw_memory = { path = "../tw_memory" }
1717
tw_misc = { path = "../tw_misc" }
18-
tw_number = { path = "../tw_number" }
18+
tw_number = { path = "../tw_number", features = ["serde"] }
1919
tw_proto = { path = "../tw_proto" }
2020

2121
[dev-dependencies]

rust/tw_evm/src/signature.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Copyright © 2017 Trust Wallet.
44

55
use std::ops::BitXor;
6-
use tw_number::{NumberResult, U256};
6+
use tw_number::{NumberError, NumberResult, U256};
77

88
/// EIP155 Eth encoding of V, of the form 27+v, or 35+chainID*2+v.
99
/// cbindgin:ignore
@@ -19,12 +19,19 @@ pub fn replay_protection(chain_id: U256, v: u8) -> NumberResult<U256> {
1919
}
2020
}
2121

22-
/// Embeds `chain_id` in `v` param, for replay protection, legacy.
22+
/// Embeds legacy protection into `v` param.
2323
#[inline]
2424
pub fn legacy_replay_protection(v: u8) -> NumberResult<U256> {
2525
U256::from(v).checked_add(ETHEREUM_SIGNATURE_V_OFFSET)
2626
}
2727

28+
/// Embeds legacy protection into `v` param.
29+
#[inline]
30+
pub fn legacy_replay_protection_u8(v: u8) -> NumberResult<u8> {
31+
v.checked_add(ETHEREUM_SIGNATURE_V_OFFSET)
32+
.ok_or(NumberError::IntegerOverflow)
33+
}
34+
2835
/// Embeds `chain_id` in `v` param, for replay protection, EIP155.
2936
#[inline]
3037
pub fn eip155_replay_protection(chain_id: U256, v: u8) -> NumberResult<U256> {

rust/tw_evm/src/transaction/user_operation_v0_7.rs

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ use crate::abi::encode::encode_tokens;
66
use crate::abi::non_empty_array::NonEmptyBytes;
77
use crate::abi::token::Token;
88
use crate::address::Address;
9+
use crate::signature::legacy_replay_protection_u8;
910
use crate::transaction::signature::Signature;
1011
use crate::transaction::{SignedTransaction, TransactionCommon, UnsignedTransaction};
11-
use serde::Serialize;
12+
use serde::ser::Error as SerError;
13+
use serde::{Serialize, Serializer};
1214
use tw_coin_entry::error::prelude::*;
13-
use tw_encoding::hex;
15+
use tw_encoding::hex::{self, as_hex_prefixed};
1416
use tw_hash::sha3::keccak256;
1517
use tw_hash::H256;
1618
use tw_memory::Data;
19+
use tw_number::serde::as_u256_hex;
1720
use tw_number::U256;
1821

1922
pub struct PackedUserOperation {
@@ -115,22 +118,45 @@ impl PackedUserOperation {
115118
}
116119
}
117120

121+
#[derive(Serialize)]
122+
#[serde(rename_all = "camelCase")]
118123
pub struct UserOperationV0_7 {
119124
pub sender: Address,
125+
#[serde(serialize_with = "U256::as_hex")]
120126
pub nonce: U256,
127+
128+
#[serde(skip_serializing_if = "Option::is_none")]
121129
pub factory: Option<Address>,
130+
#[serde(skip_serializing_if = "Data::is_empty")]
131+
#[serde(with = "as_hex_prefixed")]
122132
pub factory_data: Data,
133+
134+
#[serde(with = "as_hex_prefixed")]
123135
pub call_data: Data,
136+
137+
#[serde(serialize_with = "as_u256_hex")]
124138
pub call_data_gas_limit: u128,
139+
#[serde(serialize_with = "as_u256_hex")]
125140
pub verification_gas_limit: u128,
141+
#[serde(serialize_with = "U256::as_hex")]
126142
pub pre_verification_gas: U256,
143+
144+
#[serde(serialize_with = "as_u256_hex")]
127145
pub max_fee_per_gas: u128,
146+
#[serde(serialize_with = "as_u256_hex")]
128147
pub max_priority_fee_per_gas: u128,
148+
149+
#[serde(skip_serializing_if = "Option::is_none")]
129150
pub paymaster: Option<Address>,
151+
#[serde(serialize_with = "as_u256_hex")]
130152
pub paymaster_verification_gas_limit: u128,
153+
#[serde(serialize_with = "as_u256_hex")]
131154
pub paymaster_post_op_gas_limit: u128,
155+
#[serde(skip_serializing_if = "Data::is_empty")]
156+
#[serde(with = "as_hex_prefixed")]
132157
pub paymaster_data: Data,
133158

159+
#[serde(skip)]
134160
pub entry_point: Address,
135161
}
136162

@@ -165,8 +191,12 @@ impl UnsignedTransaction for UserOperationV0_7 {
165191
}
166192
}
167193

194+
#[derive(Serialize)]
195+
#[serde(rename_all = "camelCase")]
168196
pub struct SignedUserOperationV0_7 {
197+
#[serde(flatten)]
169198
unsigned: UserOperationV0_7,
199+
#[serde(serialize_with = "serialize_signature_with_legacy_replay_protect")]
170200
signature: Signature,
171201
}
172202

@@ -181,31 +211,7 @@ impl SignedTransaction for SignedUserOperationV0_7 {
181211
type Signature = Signature;
182212

183213
fn encode(&self) -> Data {
184-
let mut signature = self.signature.to_rsv_bytes();
185-
signature[64] += 27;
186-
187-
let prefix = true;
188-
let tx = SignedUserOperationV0_7Serde {
189-
sender: self.unsigned.sender.to_string(),
190-
nonce: self.unsigned.nonce.to_string(),
191-
factory: self.unsigned.factory.map(|addr| addr.to_string()),
192-
factory_data: hex::encode(&self.unsigned.factory_data, prefix),
193-
call_data: hex::encode(&self.unsigned.call_data, prefix),
194-
call_data_gas_limit: self.unsigned.call_data_gas_limit.to_string(),
195-
verification_gas_limit: self.unsigned.verification_gas_limit.to_string(),
196-
pre_verification_gas: self.unsigned.pre_verification_gas.to_string(),
197-
max_fee_per_gas: self.unsigned.max_fee_per_gas.to_string(),
198-
max_priority_fee_per_gas: self.unsigned.max_priority_fee_per_gas.to_string(),
199-
paymaster: self.unsigned.paymaster.map(|addr| addr.to_string()),
200-
paymaster_verification_gas_limit: self
201-
.unsigned
202-
.paymaster_verification_gas_limit
203-
.to_string(),
204-
paymaster_post_op_gas_limit: self.unsigned.paymaster_post_op_gas_limit.to_string(),
205-
paymaster_data: hex::encode(&self.unsigned.paymaster_data, prefix),
206-
signature: hex::encode(signature.as_slice(), prefix),
207-
};
208-
serde_json::to_string(&tx)
214+
serde_json::to_string(self)
209215
.expect("Simple structure should never fail on serialization")
210216
.into_bytes()
211217
}
@@ -216,26 +222,6 @@ impl SignedTransaction for SignedUserOperationV0_7 {
216222
}
217223
}
218224

219-
#[derive(Serialize)]
220-
#[serde(rename_all = "camelCase")]
221-
struct SignedUserOperationV0_7Serde {
222-
sender: String,
223-
nonce: String,
224-
factory: Option<String>,
225-
factory_data: String,
226-
call_data: String,
227-
call_data_gas_limit: String,
228-
verification_gas_limit: String,
229-
pre_verification_gas: String,
230-
max_fee_per_gas: String,
231-
max_priority_fee_per_gas: String,
232-
paymaster: Option<String>,
233-
paymaster_verification_gas_limit: String,
234-
paymaster_post_op_gas_limit: String,
235-
paymaster_data: String,
236-
signature: String,
237-
}
238-
239225
fn concat_u128_be(a: u128, b: u128) -> [u8; 32] {
240226
let a = a.to_be_bytes();
241227
let b = b.to_be_bytes();
@@ -248,6 +234,22 @@ fn concat_u128_be(a: u128, b: u128) -> [u8; 32] {
248234
})
249235
}
250236

237+
pub fn serialize_signature_with_legacy_replay_protect<S>(
238+
signature: &Signature,
239+
serializer: S,
240+
) -> Result<S::Ok, S::Error>
241+
where
242+
S: Serializer,
243+
{
244+
let prefix = true;
245+
let mut rsv = signature.to_rsv_bytes();
246+
rsv[64] =
247+
legacy_replay_protection_u8(rsv[64]).map_err(|e| SerError::custom(format!("{e:?}")))?;
248+
249+
let hex_str = hex::encode(rsv, prefix);
250+
serializer.serialize_str(&hex_str)
251+
}
252+
251253
#[cfg(test)]
252254
mod tests {
253255
use super::*;
@@ -310,7 +312,7 @@ mod tests {
310312
nonce: U256::from(0u64),
311313
init_code: "f471789937856d80e589f5996cf8b0511ddd9de4f471789937856d80e589f5996cf8b0511ddd9de4".decode_hex().unwrap(),
312314
call_data: "00".decode_hex().unwrap(),
313-
account_gas_limits:concat_u128_be(100000u128, 100000u128).to_vec(),
315+
account_gas_limits: concat_u128_be(100000u128, 100000u128).to_vec(),
314316
pre_verification_gas: U256::from(1000000u64),
315317
gas_fees: concat_u128_be(100000u128, 100000u128).to_vec(),
316318
paymaster_and_data: "f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000000001869f00000000000000000000000000015b3800000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b".decode_hex().unwrap(),

rust/tw_evm/tests/barz.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,9 @@ fn test_biz4337_transfer() {
317317
"68109b9caf49f7971b689307c9a77ceec46e4b8fa88421c4276dd846f782d92c"
318318
);
319319

320-
let user_op: serde_json::Value = serde_json::from_slice(&output.encoded).unwrap();
321-
assert_eq!(user_op["callData"], "0x76276c82000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000b0086171ac7b6bd4d046580bca6d6a4b0835c2320000000000000000000000000000000000000000000000000002540befbfbd0000000000000000000000000000000000000000000000000000000000");
320+
let expected = r#"{"sender":"0x2EF648D7C03412B832726fd4683E2625deA047Ba","nonce":"0x00","callData":"0x76276c82000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000b0086171ac7b6bd4d046580bca6d6a4b0835c2320000000000000000000000000000000000000000000000000002540befbfbd0000000000000000000000000000000000000000000000000000000000","callDataGasLimit":"0x0186a0","verificationGasLimit":"0x0186a0","preVerificationGas":"0x0f4240","maxFeePerGas":"0x0186a0","maxPriorityFeePerGas":"0x0186a0","paymaster":"0xb0086171AC7b6BD4D046580bca6d6A4b0835c232","paymasterVerificationGasLimit":"0x01869f","paymasterPostOpGasLimit":"0x015b38","paymasterData":"0x00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b","signature":"0xf6b1f7ad22bcc68ca292bc10d15e82e0eab8c75c1a04f9750e7cff1418d38d9c6c115c510e3f47eb802103d62f88fa7d4a3b2e24e2ddbe7ee68153920ab3f6cc1b"}"#;
321+
let actual = String::from_utf8(output.encoded.to_vec()).unwrap();
322+
assert_eq!(actual, expected);
322323
}
323324

324325
#[test]
@@ -404,8 +405,9 @@ fn test_biz4337_transfer_batch() {
404405
"f6340068891dc3eb78959993151c421dde23982b3a1407c0dbbd62c2c22c3cb8"
405406
);
406407

407-
let user_op: serde_json::Value = serde_json::from_slice(&output.encoded).unwrap();
408-
assert_eq!(user_op["callData"], "0x26da7d880000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000b0086171ac7b6bd4d046580bca6d6a4b0835c2320000000000000000000000000000000000000000000000000002540befbfbd000000000000000000000000000000000000000000000000000000000000000000000000000000000003bbb5660b8687c2aa453a0e42dcb6e0732b1266000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000005ff137d4b0fdcd49dca30c7cf57e578a026d27890000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000");
408+
let expected = r#"{"sender":"0x2EF648D7C03412B832726fd4683E2625deA047Ba","nonce":"0x00","callData":"0x26da7d880000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000b0086171ac7b6bd4d046580bca6d6a4b0835c2320000000000000000000000000000000000000000000000000002540befbfbd000000000000000000000000000000000000000000000000000000000000000000000000000000000003bbb5660b8687c2aa453a0e42dcb6e0732b1266000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000005ff137d4b0fdcd49dca30c7cf57e578a026d27890000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000","callDataGasLimit":"0x0186a0","verificationGasLimit":"0x0186a0","preVerificationGas":"0x0f4240","maxFeePerGas":"0x0186a0","maxPriorityFeePerGas":"0x0186a0","paymaster":"0xb0086171AC7b6BD4D046580bca6d6A4b0835c232","paymasterVerificationGasLimit":"0x01869f","paymasterPostOpGasLimit":"0x015b38","paymasterData":"0x00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b","signature":"0x21ab0bdcd1441aef3e4046a922bab3636d0c74011c1b055c55ad9f39ae9b4dac59bcbf3bc1ff31b367a83360edfc8e9652f1a5c8b07eb76fe5a426835682d6721c"}"#;
409+
let actual = String::from_utf8(output.encoded.to_vec()).unwrap();
410+
assert_eq!(actual, expected);
409411
}
410412

411413
#[test]

rust/tw_number/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ arbitrary = { version = "1", features = ["derive"], optional = true }
1313
lazy_static = "1.4.0"
1414
primitive-types = "0.10.1"
1515
serde = { version = "1.0", features = ["derive"], optional = true }
16+
tw_encoding = { path = "../tw_encoding" }
1617
tw_hash = { path = "../tw_hash" }
1718
tw_memory = { path = "../tw_memory" }
1819

rust/tw_number/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Copyright © 2017 Trust Wallet.
44

55
mod i256;
6+
pub mod serde;
67
mod sign;
78
mod u256;
89

rust/tw_number/src/serde.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
5+
use crate::U256;
6+
use serde::Serializer;
7+
8+
pub fn as_u256_hex<T, S>(num: &T, serializer: S) -> Result<S::Ok, S::Error>
9+
where
10+
S: Serializer,
11+
T: Into<U256> + Copy,
12+
{
13+
let num_u256: U256 = (*num).into();
14+
num_u256.as_hex(serializer)
15+
}

rust/tw_number/src/u256.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ mod impl_serde {
236236
use serde::de::Error as DeError;
237237
use serde::{Deserialize, Deserializer, Serializer};
238238
use std::str::FromStr;
239+
use tw_encoding::hex;
239240

240241
impl U256 {
241242
pub fn as_decimal_str<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -259,6 +260,17 @@ mod impl_serde {
259260
{
260261
crate::serde_common::from_num_or_decimal_str::<'de, U256, u64, D>(deserializer)
261262
}
263+
264+
pub fn as_hex<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
265+
where
266+
S: Serializer,
267+
{
268+
let prefix = true;
269+
let min_bytes_len = 1;
270+
271+
let hex_str = hex::encode(self.to_big_endian_compact_min_len(min_bytes_len), prefix);
272+
serializer.serialize_str(&hex_str)
273+
}
262274
}
263275
}
264276

0 commit comments

Comments
 (0)