Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit e7e2de9

Browse files
authored
Cleanup Cetus tx_builder.rs and add tests (#1133)
1 parent e1445e8 commit e7e2de9

9 files changed

Lines changed: 125 additions & 31 deletions

File tree

‎crates/gem_encoding/src/protobuf/decode.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ fn read_varint(data: &[u8], position: &mut usize) -> MessageResult<u64> {
8686
while *position < data.len() {
8787
let byte = data[*position];
8888
*position += 1;
89-
result |= u64::from(byte & 0x7f) << shift;
89+
let value = u64::from(byte & 0x7f);
90+
if shift == 63 && value > 1 {
91+
return Err("protobuf varint overflows u64".into());
92+
}
93+
result |= value << shift;
9094
if byte & 0x80 == 0 {
9195
return Ok(result);
9296
}
@@ -186,6 +190,13 @@ mod tests {
186190
assert_eq!(values, vec!["test", "7"]);
187191
}
188192

193+
#[test]
194+
fn test_visit_fields_rejects_varint_overflow() {
195+
let message = [vec![0x08], vec![0xff; 9], vec![0x02]].concat();
196+
197+
assert_eq!(visit_fields(&message, |_| Ok(())).unwrap_err().to_string(), "protobuf varint overflows u64");
198+
}
199+
189200
#[derive(Debug, Default, PartialEq)]
190201
struct TestMessage {
191202
name: Option<String>,

‎crates/gem_encoding/src/protobuf/encode.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ pub fn encode_bytes_field(field_number: u32, data: &[u8]) -> Vec<u8> {
3535
return Vec::new();
3636
}
3737

38-
[field_tag(field_number, WIRE_LENGTH_DELIMITED), encode_varint(data.len() as u64), data.to_vec()].concat()
38+
let tag = field_tag(field_number, WIRE_LENGTH_DELIMITED);
39+
let len = encode_varint(data.len() as u64);
40+
let mut buf = Vec::with_capacity(tag.len() + len.len() + data.len());
41+
buf.extend_from_slice(&tag);
42+
buf.extend_from_slice(&len);
43+
buf.extend_from_slice(data);
44+
buf
3945
}
4046

4147
pub fn encode_string_field(field_number: u32, value: &str) -> Vec<u8> {

‎crates/gem_sui/src/address.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ use sui_types::Address;
44

55
pub struct SuiAddress(Address);
66

7+
impl From<SuiAddress> for Address {
8+
fn from(value: SuiAddress) -> Self {
9+
value.0
10+
}
11+
}
12+
713
impl AddressTrait for SuiAddress {
814
fn try_parse(address: &str) -> Option<Self> {
915
Address::from_str(address).ok().map(Self)

‎crates/gem_sui/src/rpc/proto/checkpoints.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,24 @@ pub struct CheckpointedTransactionInfo {
8383
proto_decode!(CheckpointedTransactionInfo {
8484
1 => transaction: optional_string,
8585
});
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::*;
90+
use gem_encoding::protobuf::MessageDecode;
91+
92+
#[test]
93+
fn test_checkpoint_response_wire_bytes_decode() {
94+
let response = hex::decode("0a2a082a1211636865636b706f696e742d6469676573741a0a180728633a04707265762a0722050a03747831").unwrap();
95+
let checkpoint = GetCheckpointResponse::decode(&response).unwrap().checkpoint.unwrap();
96+
let summary = checkpoint.summary.unwrap();
97+
let contents = checkpoint.contents.unwrap();
98+
99+
assert_eq!(checkpoint.sequence_number, Some(42));
100+
assert_eq!(checkpoint.digest.as_deref(), Some("checkpoint-digest"));
101+
assert_eq!(summary.epoch, Some(7));
102+
assert_eq!(summary.total_network_transactions, Some(99));
103+
assert_eq!(summary.previous_digest.as_deref(), Some("prev"));
104+
assert_eq!(contents.transactions[0].transaction.as_deref(), Some("tx1"));
105+
}
106+
}

‎crates/gem_sui/src/rpc/proto/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ pub(crate) trait WithMut: Sized {
3737
}
3838
}
3939

40-
impl<T> WithMut for T {}
40+
impl<T: gem_encoding::protobuf::MessageEncode> WithMut for T {}

‎crates/gem_sui/src/rpc/proto/objects.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,24 @@ impl OwnerKind {
160160
}
161161
}
162162
}
163+
164+
#[cfg(test)]
165+
mod tests {
166+
use super::*;
167+
use gem_encoding::protobuf::MessageDecode;
168+
169+
#[test]
170+
fn test_object_response_wire_bytes_decode() {
171+
let response = hex::decode("0a2b120530786162631807220a6f626a2d6469676573742a0b0801120730786f776e65723204636f696ea8067b").unwrap();
172+
let object = GetObjectResponse::decode(&response).unwrap().object.unwrap();
173+
let owner = object.owner.unwrap();
174+
175+
assert_eq!(object.object_id.as_deref(), Some("0xabc"));
176+
assert_eq!(object.version, Some(7));
177+
assert_eq!(object.digest.as_deref(), Some("obj-digest"));
178+
assert_eq!(owner.kind(), OwnerKind::Address);
179+
assert_eq!(owner.address.as_deref(), Some("0xowner"));
180+
assert_eq!(object.object_type.as_deref(), Some("coin"));
181+
assert_eq!(object.balance, Some(123));
182+
}
183+
}

‎crates/gem_sui/src/rpc/proto/transactions.rs‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ proto_decode!(Event {
265265
#[cfg(test)]
266266
mod tests {
267267
use super::*;
268-
use gem_encoding::protobuf::{MessageDecode, encode_message_field, encode_string_field};
268+
use gem_encoding::protobuf::{MessageDecode, MessageEncode, encode_message_field, encode_string_field};
269269

270270
#[test]
271271
fn test_execute_transaction_response_decode() {
@@ -275,4 +275,32 @@ mod tests {
275275

276276
assert_eq!(ExecuteTransactionResponse::decode(&response).unwrap().transaction.unwrap().digest.unwrap(), digest);
277277
}
278+
279+
#[test]
280+
fn test_request_wire_bytes() {
281+
let transaction = Transaction::from_transaction_bcs(vec![1, 2, 3]);
282+
let read_mask = FieldMask::from_paths(["digest"]);
283+
let simulate = SimulateTransactionRequest {
284+
transaction: Some(transaction.clone()),
285+
read_mask: Some(read_mask.clone()),
286+
checks: Some(TransactionChecks::Disabled),
287+
};
288+
let execute = ExecuteTransactionRequest {
289+
transaction: Some(transaction),
290+
signatures: vec![UserSignature {
291+
bcs: Some(Bcs::new("UserSignatureBytes", vec![0xaa, 0xbb])),
292+
scheme: Some(0),
293+
}],
294+
read_mask: Some(read_mask),
295+
};
296+
297+
assert_eq!(
298+
hex::encode(simulate.encode()),
299+
"0a180a160a0f5472616e73616374696f6e44617461120301020312080a066469676573741801"
300+
);
301+
assert_eq!(
302+
hex::encode(execute.encode()),
303+
"0a180a160a0f5472616e73616374696f6e446174611203010203121c0a180a12557365725369676e617475726542797465731202aabb10001a080a06646967657374"
304+
);
305+
}
278306
}

‎crates/gem_sui/src/tx_builder/transaction.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ use sui_types::{Address, Identifier, TypeTag};
1414
const MODULE_COIN: &str = "coin";
1515
const FUNCTION_ZERO: &str = "zero";
1616

17-
pub fn move_call(txb: &mut TransactionBuilder, package: impl ToString, module: &str, function: &str, type_args: &[&str], arguments: Vec<Argument>) -> Result<Argument, SuiError> {
18-
let package = package.to_string();
17+
pub fn move_call(txb: &mut TransactionBuilder, package: Address, module: &str, function: &str, type_args: &[&str], arguments: Vec<Argument>) -> Result<Argument, SuiError> {
1918
let type_args = type_args
2019
.iter()
2120
.map(|value| {
@@ -25,7 +24,7 @@ pub fn move_call(txb: &mut TransactionBuilder, package: impl ToString, module: &
2524
})
2625
.collect::<Result<Vec<_>, _>>()?;
2726
let function = Function::new(
28-
Address::from_str(&package).map_err(|err| SuiError::invalid_input(format!("Invalid Sui address {package}: {err}")))?,
27+
package,
2928
Identifier::new(module).map_err(|err| SuiError::invalid_input(err.to_string()))?,
3029
Identifier::new(function).map_err(|err| SuiError::invalid_input(err.to_string()))?,
3130
)

‎crates/swapper/src/cetus_clmm/tx_builder.rs‎

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use super::{
99
use crate::{Quote, SwapperError, SwapperQuoteData, fees::ReferralFee, fees::apply_slippage_in_bp};
1010
use gem_sui::{
1111
EMPTY_ADDRESS, ESTIMATION_GAS_BUDGET, SuiClient,
12+
address::SuiAddress,
1213
gas_budget::GAS_BUDGET_MULTIPLIER,
1314
is_sui_coin,
1415
models::{CoinAsset, TxOutput},
@@ -18,7 +19,8 @@ use gem_sui::{
1819
into_balance, move_call,
1920
},
2021
};
21-
use std::{collections::HashMap, fmt::Display, str::FromStr};
22+
use primitives::Address as AddressTrait;
23+
use std::{collections::HashMap, fmt::Display};
2224
use sui_transaction_builder::{Argument, ObjectInput, TransactionBuilder};
2325
use sui_types::{Address, Digest};
2426

@@ -112,7 +114,7 @@ pub(super) fn build_batch_quote_inspect(quotes: &[(&Hop, u64)]) -> Result<Vec<u8
112114
let amount = txb.pure(amount_in);
113115
move_call(
114116
&mut txb,
115-
CETUS_CLMM_PUBLISHED_AT,
117+
cetus_clmm_publish_at(),
116118
MODULE_POOL,
117119
FUNCTION_CALCULATE_SWAP_RESULT,
118120
&[&hop.coin_a, &hop.coin_b],
@@ -132,20 +134,20 @@ pub(super) fn build_batch_multi_hop_quote_inspect(routes: &[(&Hop, &Hop, u64)])
132134
let amount = txb.pure(amount_in);
133135
let csr1 = move_call(
134136
&mut txb,
135-
CETUS_CLMM_PUBLISHED_AT,
137+
cetus_clmm_publish_at(),
136138
MODULE_POOL,
137139
FUNCTION_CALCULATE_SWAP_RESULT,
138140
&[&hop1.coin_a, &hop1.coin_b],
139141
vec![pool1, a2b1, by_amount_in_1, amount],
140142
)
141143
.map_err(error)?;
142-
let amount2 = move_call(&mut txb, CETUS_CLMM_PUBLISHED_AT, MODULE_POOL, FUNCTION_CALCULATED_SWAP_RESULT_AMOUNT_OUT, &[], vec![csr1]).map_err(error)?;
144+
let amount2 = move_call(&mut txb, cetus_clmm_publish_at(), MODULE_POOL, FUNCTION_CALCULATED_SWAP_RESULT_AMOUNT_OUT, &[], vec![csr1]).map_err(error)?;
143145
let pool2 = txb.object(shared_object_input(&hop2.pool_id, hop2.pool_init_version, false)?);
144146
let a2b2 = txb.pure(&hop2.a2b);
145147
let by_amount_in_2 = txb.pure(&true);
146148
move_call(
147149
&mut txb,
148-
CETUS_CLMM_PUBLISHED_AT,
150+
cetus_clmm_publish_at(),
149151
MODULE_POOL,
150152
FUNCTION_CALCULATE_SWAP_RESULT,
151153
&[&hop2.coin_a, &hop2.coin_b],
@@ -159,10 +161,10 @@ pub(super) fn build_batch_multi_hop_quote_inspect(routes: &[(&Hop, &Hop, u64)])
159161
pub(super) fn build_pool_id_inspect(coin_a: &str, coin_b: &str, tick_spacing: u32) -> Result<Vec<u8>, SwapperError> {
160162
let mut txb = TransactionBuilder::new();
161163
let tick = txb.pure(&tick_spacing);
162-
let key = move_call(&mut txb, CETUS_CLMM_PUBLISHED_AT, MODULE_FACTORY, FUNCTION_NEW_POOL_KEY, &[coin_a, coin_b], vec![tick]).map_err(error)?;
164+
let key = move_call(&mut txb, cetus_clmm_publish_at(), MODULE_FACTORY, FUNCTION_NEW_POOL_KEY, &[coin_a, coin_b], vec![tick]).map_err(error)?;
163165
let pools = txb.object(shared_object_input(CETUS_POOLS_REGISTRY, CETUS_SHARED_INIT_VERSION, false)?);
164-
let info = move_call(&mut txb, CETUS_CLMM_PUBLISHED_AT, MODULE_FACTORY, FUNCTION_POOL_SIMPLE_INFO, &[], vec![pools, key]).map_err(error)?;
165-
move_call(&mut txb, CETUS_CLMM_PUBLISHED_AT, MODULE_FACTORY, FUNCTION_POOL_ID, &[], vec![info]).map_err(error)?;
166+
let info = move_call(&mut txb, cetus_clmm_publish_at(), MODULE_FACTORY, FUNCTION_POOL_SIMPLE_INFO, &[], vec![pools, key]).map_err(error)?;
167+
move_call(&mut txb, cetus_clmm_publish_at(), MODULE_FACTORY, FUNCTION_POOL_ID, &[], vec![info]).map_err(error)?;
166168
inspect_transaction_kind_bytes(txb)
167169
}
168170

@@ -183,6 +185,7 @@ fn build_transaction(
183185
input: &BuildInput<'_>,
184186
) -> Result<TxOutput, SwapperError> {
185187
let mut txb = TransactionBuilder::new();
188+
let published_at = SuiAddress::from_str(published_at).map(Address::from)?;
186189
let input_coin = build_input_coin(&mut txb, route.input_coin_type(), input.amount, input.from_coins).map_err(error)?;
187190
let (swap_coin, swap_amount) = match route.fee_side {
188191
FeeSide::Input => {
@@ -266,7 +269,7 @@ fn build_transaction(
266269
.map_err(error)?;
267270
}
268271

269-
let sender = recipient(&quote.request.wallet_address)?;
272+
let sender = SuiAddress::from_str(&quote.request.wallet_address).map(Address::from)?;
270273
let output_coin = from_balance(&mut txb, route.output_coin_type(), current_balance).map_err(error)?;
271274

272275
let pay_output_fee = match route.fee_side {
@@ -279,7 +282,8 @@ fn build_transaction(
279282
.split_coins(output_coin, vec![fee_amount_arg])
280283
.pop()
281284
.ok_or_else(|| SwapperError::TransactionError("Cetus CLMM output fee split failed".into()))?;
282-
transfer_coin(&mut txb, fee_coin, recipient(&referral_fee.address)?);
285+
let recipient = SuiAddress::from_str(&referral_fee.address).map(Address::from)?;
286+
transfer_coin(&mut txb, fee_coin, recipient);
283287
}
284288

285289
let min_out = apply_slippage_in_bp(&route.net_amount_out(), quote.request.options.slippage.bps);
@@ -290,7 +294,12 @@ fn build_transaction(
290294
.ok_or_else(|| SwapperError::TransactionError("Cetus CLMM min-out split failed".into()))?;
291295
txb.merge_coins(output_coin, vec![split_off]);
292296

293-
let dest = swap_recipient(quote)?;
297+
let dest = SuiAddress::from_str(if quote.request.destination_address.is_empty() {
298+
&quote.request.wallet_address
299+
} else {
300+
&quote.request.destination_address
301+
})
302+
.map(Address::from)?;
294303
if dest == sender && is_sui_coin(route.output_coin_type()) {
295304
let gas = txb.gas();
296305
txb.merge_coins(gas, vec![output_coin]);
@@ -325,7 +334,8 @@ fn pay_referral_fee(txb: &mut TransactionBuilder, input_coin: Argument, fee: u64
325334
.split_coins(input_coin, vec![fee_amount])
326335
.pop()
327336
.ok_or_else(|| SwapperError::TransactionError("Sui referral fee split failed".into()))?;
328-
transfer_coin(txb, fee_coin, recipient(&referral_fee.address)?);
337+
let recipient = SuiAddress::from_str(&referral_fee.address).map(Address::from)?;
338+
transfer_coin(txb, fee_coin, recipient);
329339
Ok(input_coin)
330340
}
331341

@@ -341,25 +351,17 @@ fn transfer_coin(txb: &mut TransactionBuilder, coin: Argument, recipient: Addres
341351
txb.transfer_objects(vec![coin], recipient);
342352
}
343353

344-
fn swap_recipient(quote: &Quote) -> Result<Address, SwapperError> {
345-
if quote.request.destination_address.is_empty() {
346-
recipient(&quote.request.wallet_address)
347-
} else {
348-
recipient(&quote.request.destination_address)
349-
}
350-
}
351-
352-
fn recipient(value: &str) -> Result<Address, SwapperError> {
353-
Address::from_str(value).map_err(|err| SwapperError::TransactionError(format!("Invalid Sui address {value}: {err}")))
354+
fn cetus_clmm_publish_at() -> Address {
355+
SuiAddress::from_str(CETUS_CLMM_PUBLISHED_AT).map(Address::from).unwrap()
354356
}
355357

356358
fn shared_object_input(object_id: &str, initial_shared_version: u64, mutable: bool) -> Result<ObjectInput, SwapperError> {
357-
let address = Address::from_str(object_id).map_err(|err| SwapperError::ComputeQuoteError(format!("Invalid Sui object id {object_id}: {err}")))?;
359+
let address = SuiAddress::from_str(object_id).map(Address::from)?;
358360
Ok(ObjectInput::shared(address, initial_shared_version, mutable))
359361
}
360362

361363
fn inspect_transaction_kind_bytes(mut txb: TransactionBuilder) -> Result<Vec<u8>, SwapperError> {
362-
txb.set_sender(Address::from_str(EMPTY_ADDRESS).map_err(|err| SwapperError::ComputeQuoteError(format!("Invalid inspect sender: {err}")))?);
364+
txb.set_sender(SuiAddress::from_str(EMPTY_ADDRESS).map(Address::from)?);
363365
txb.set_gas_price(0);
364366
txb.set_gas_budget(0);
365367
txb.add_gas_objects(vec![ObjectInput::owned(Address::ZERO, 0, Digest::ZERO)]);

0 commit comments

Comments
 (0)