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

Commit e2aa7ca

Browse files
authored
Sui Address Balance support and coin-model refactor (#1163)
1 parent 5e19820 commit e2aa7ca

20 files changed

Lines changed: 462 additions & 301 deletions

File tree

crates/gem_sui/src/lib.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ pub mod tx_builder;
2525
pub mod signer;
2626

2727
pub use error::SuiError;
28-
use models::Coin;
2928
pub use models::ObjectId;
29+
use models::{Coin, OwnedCoins};
3030
use std::error::Error;
3131
use sui_transaction_builder::ObjectInput;
3232
use sui_types::Address;
@@ -71,14 +71,13 @@ pub fn sui_clock_object_input() -> ObjectInput {
7171
ObjectInput::shared(sui_clock_object_id(), 1, false)
7272
}
7373

74-
pub fn validate_enough_balance(coins: &[Coin], amount: u64) -> Option<Box<dyn Error + Send + Sync>> {
75-
if coins.is_empty() {
76-
return Some("coins list is empty".into());
74+
pub fn validate_enough_balance(coins: &OwnedCoins<Coin>, amount: u64) -> Option<Box<dyn Error + Send + Sync>> {
75+
let total = coins.total();
76+
if total == 0 {
77+
return Some("no spendable coin objects or address balance".into());
7778
}
78-
79-
let total_amount: u64 = coins.iter().map(|x| x.balance).sum();
80-
if total_amount < amount {
81-
return Some(format!("total amount ({}) is less than amount to send ({})", total_amount, amount).into());
79+
if total < amount {
80+
return Some(format!("total amount ({}) is less than amount to send ({})", total, amount).into());
8281
}
8382
None
8483
}

crates/gem_sui/src/models/coin.rs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde_serializers::deserialize_bigint_from_str;
77

88
#[cfg(feature = "rpc")]
99
use super::account::Owner;
10+
use super::core::Coin;
1011

1112
#[derive(Debug, Clone, Serialize, Deserialize)]
1213
#[serde(rename_all = "camelCase")]
@@ -24,16 +25,57 @@ pub struct SuiObject {
2425
pub version: String,
2526
}
2627

27-
#[cfg(feature = "rpc")]
28-
#[derive(Debug, Clone, Serialize, Deserialize)]
29-
#[serde(rename_all = "camelCase")]
30-
pub struct SuiCoin {
28+
#[derive(Debug, Clone, PartialEq, Eq)]
29+
pub struct OwnedCoins<T> {
3130
pub coin_type: String,
32-
pub coin_object_id: String,
33-
#[serde(deserialize_with = "deserialize_bigint_from_str")]
34-
pub balance: BigInt,
35-
pub version: String,
36-
pub digest: String,
31+
pub coins: Vec<T>,
32+
pub address_balance: u64,
33+
}
34+
35+
impl<T> Default for OwnedCoins<T> {
36+
fn default() -> Self {
37+
Self {
38+
coin_type: String::new(),
39+
coins: Vec::new(),
40+
address_balance: 0,
41+
}
42+
}
43+
}
44+
45+
impl<T> OwnedCoins<T> {
46+
pub fn new(coin_type: String, coins: Vec<T>, address_balance: u64) -> Self {
47+
Self {
48+
coin_type,
49+
coins,
50+
address_balance,
51+
}
52+
}
53+
54+
pub fn map<U>(self, f: impl FnMut(T) -> U) -> OwnedCoins<U> {
55+
OwnedCoins {
56+
coin_type: self.coin_type,
57+
coins: self.coins.into_iter().map(f).collect(),
58+
address_balance: self.address_balance,
59+
}
60+
}
61+
62+
pub fn try_map<U, E>(self, f: impl FnMut(T) -> Result<U, E>) -> Result<OwnedCoins<U>, E> {
63+
Ok(OwnedCoins {
64+
coin_type: self.coin_type,
65+
coins: self.coins.into_iter().map(f).collect::<Result<_, _>>()?,
66+
address_balance: self.address_balance,
67+
})
68+
}
69+
}
70+
71+
impl OwnedCoins<Coin> {
72+
pub fn coin_total(&self) -> u64 {
73+
self.coins.iter().map(|coin| coin.balance).fold(0, u64::saturating_add)
74+
}
75+
76+
pub fn total(&self) -> u64 {
77+
self.coin_total().saturating_add(self.address_balance)
78+
}
3779
}
3880

3981
#[cfg(feature = "rpc")]
@@ -43,6 +85,9 @@ pub struct Balance {
4385
pub coin_type: String,
4486
#[serde(deserialize_with = "deserialize_bigint_from_str")]
4587
pub total_balance: BigInt,
88+
#[serde(default)]
89+
/// Amount in the per-address balance accumulator.
90+
pub address_balance: u64,
4691
}
4792

4893
#[cfg(feature = "rpc")]

crates/gem_sui/src/models/coin_asset.rs

Lines changed: 0 additions & 51 deletions
This file was deleted.

crates/gem_sui/src/models/core.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
1+
use super::OwnedCoins;
12
use bcs;
23
use gem_encoding::encode_base64;
34
use std::error::Error;
45
use sui_transaction_builder::ObjectInput;
5-
use sui_types::Transaction;
6+
use sui_types::{Address, Digest, Transaction};
67

7-
#[derive(Debug, PartialEq, Clone)]
8+
#[derive(Debug, PartialEq, Eq, Clone)]
89
pub struct Coin {
910
pub coin_type: String,
1011
pub balance: u64,
1112
pub object: Object,
1213
}
1314

14-
#[derive(Debug, PartialEq, Clone)]
15+
impl Coin {
16+
pub fn to_input(&self) -> ObjectInput {
17+
self.object.to_input()
18+
}
19+
}
20+
21+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1522
pub struct Object {
16-
pub object_id: String,
17-
pub digest: String,
23+
pub object_id: Address,
24+
pub digest: Digest,
1825
pub version: u64,
1926
}
2027

2128
impl Object {
2229
pub fn to_input(&self) -> ObjectInput {
23-
ObjectInput::owned(self.object_id.parse().unwrap(), self.version, self.digest.parse().unwrap())
30+
ObjectInput::owned(self.object_id, self.version, self.digest)
2431
}
2532
}
2633

@@ -36,7 +43,7 @@ pub struct StakeInput {
3643
pub validator: String,
3744
pub stake_amount: u64,
3845
pub gas: Gas,
39-
pub coins: Vec<Coin>,
46+
pub coins: OwnedCoins<Coin>,
4047
}
4148

4249
#[derive(Debug, PartialEq, Clone)]
@@ -52,7 +59,7 @@ pub struct TransferInput {
5259
pub sender: String,
5360
pub recipient: String,
5461
pub amount: u64,
55-
pub coins: Vec<Coin>,
62+
pub coins: OwnedCoins<Coin>,
5663
pub send_max: bool,
5764
pub gas: Gas,
5865
}
@@ -62,7 +69,7 @@ pub struct TokenTransferInput {
6269
pub sender: String,
6370
pub recipient: String,
6471
pub amount: u64,
65-
pub tokens: Vec<Coin>,
72+
pub tokens: OwnedCoins<Coin>,
6673
pub gas: Gas,
6774
pub gas_coin: Coin,
6875
}

crates/gem_sui/src/models/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
pub mod account;
22
pub mod coin;
3-
pub mod coin_asset;
43
pub mod core;
54
pub mod inspect;
65
pub mod object_id;
76
pub mod staking;
7+
#[cfg(test)]
8+
pub mod testkit;
89
pub mod transaction;
910

1011
pub use coin::*;
11-
pub use coin_asset::{CoinAsset, CoinResponse};
1212
pub use core::*;
1313
pub use inspect::{InspectCommandResult, InspectEffects, InspectEvent, InspectGasUsed, InspectResult, InspectReturnValue};
1414
pub use object_id::ObjectId;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use crate::SUI_COIN_TYPE;
2+
use crate::models::{Coin, Object, OwnedCoins};
3+
4+
impl Object {
5+
pub fn mock() -> Self {
6+
Self {
7+
object_id: "0xabcdef1234567890abcdef1234567890abcdef12".parse().unwrap(),
8+
digest: "HdfF7hswRuvbXbEXjGjmUCt7gLybhvbPvvK8zZbCqyD8".parse().unwrap(),
9+
version: 100,
10+
}
11+
}
12+
}
13+
14+
impl Coin {
15+
pub fn mock_sui() -> Self {
16+
Self {
17+
coin_type: SUI_COIN_TYPE.to_string(),
18+
balance: 5_000_000_000,
19+
object: Object::mock(),
20+
}
21+
}
22+
}
23+
24+
impl OwnedCoins<Coin> {
25+
pub fn mock_sui() -> Self {
26+
Self::new(SUI_COIN_TYPE.to_string(), vec![Coin::mock_sui()], 0)
27+
}
28+
}

crates/gem_sui/src/provider/preload.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use primitives::{
1212
use crate::{
1313
ESTIMATION_GAS_BUDGET, SUI_COIN_TYPE,
1414
gas_budget::GAS_BUDGET_MULTIPLIER,
15-
models::{SuiCoin, SuiObject},
15+
models::{Coin, OwnedCoins, SuiObject},
1616
};
1717
use crate::{
1818
provider::preload_mapper::{map_transaction_data, map_transaction_rate_rates},
@@ -27,13 +27,13 @@ impl ChainTransactionLoad for SuiClient {
2727
}
2828

2929
async fn get_transaction_load(&self, input: TransactionLoadInput) -> Result<TransactionLoadData, Box<dyn Error + Sync + Send>> {
30-
let (gas_coins, coins, objects) = self.get_coins_for_input_type(input.sender_address.as_str(), input.input_type.clone()).await?;
30+
let (sui_coins, token_coins, objects) = self.get_coins_for_input_type(input.sender_address.as_str(), input.input_type.clone()).await?;
3131

32-
let estimate_bytes = map_transaction_data(input.clone(), gas_coins.clone(), coins.clone(), objects.clone(), ESTIMATION_GAS_BUDGET)?;
32+
let estimate_bytes = map_transaction_data(input.clone(), sui_coins.clone(), token_coins.clone(), objects.clone(), ESTIMATION_GAS_BUDGET)?;
3333
let fee = self.estimate_fee(&estimate_bytes, &input.gas_price, input.is_max_value).await?;
3434

3535
let message_bytes = match estimated_gas_budget(&input.input_type, &fee)? {
36-
Some(budget) => map_transaction_data(input, gas_coins, coins, objects, budget)?,
36+
Some(budget) => map_transaction_data(input, sui_coins, token_coins, objects, budget)?,
3737
None => estimate_bytes,
3838
};
3939

@@ -75,27 +75,27 @@ impl SuiClient {
7575
&self,
7676
address: &str,
7777
input_type: TransactionInputType,
78-
) -> Result<(Vec<SuiCoin>, Vec<SuiCoin>, Vec<SuiObject>), Box<dyn Error + Send + Sync>> {
78+
) -> Result<(OwnedCoins<Coin>, Option<OwnedCoins<Coin>>, Vec<SuiObject>), Box<dyn Error + Send + Sync>> {
7979
match input_type {
8080
TransactionInputType::Transfer(asset) => match asset.id.token_id {
81-
None => Ok((self.get_coins(address, SUI_COIN_TYPE).await?, Vec::new(), Vec::new())),
81+
None => Ok((self.get_coins(address, SUI_COIN_TYPE).await?, None, Vec::new())),
8282
Some(token_id) => {
83-
let (gas_coins, coins) = futures::try_join!(self.get_coins(address, SUI_COIN_TYPE), self.get_coins(address, &token_id))?;
84-
Ok((gas_coins, coins, Vec::new()))
83+
let (gas_coins, token_coins) = futures::try_join!(self.get_coins(address, SUI_COIN_TYPE), self.get_coins(address, &token_id))?;
84+
Ok((gas_coins, Some(token_coins), Vec::new()))
8585
}
8686
},
8787
TransactionInputType::Stake(_, stake_type) => match stake_type {
88-
StakeType::Stake(_) => Ok((self.get_coins(address, SUI_COIN_TYPE).await?, Vec::new(), Vec::new())),
88+
StakeType::Stake(_) => Ok((self.get_coins(address, SUI_COIN_TYPE).await?, None, Vec::new())),
8989
StakeType::Unstake(delegation) => {
9090
let (gas_coins, staked_object) = futures::try_join!(self.get_coins(address, SUI_COIN_TYPE), self.get_object(delegation.base.delegation_id.clone()))?;
91-
Ok((gas_coins, Vec::new(), vec![staked_object]))
91+
Ok((gas_coins, None, vec![staked_object]))
9292
}
9393
StakeType::Redelegate(_) | StakeType::Rewards(_) | StakeType::Withdraw(_) | StakeType::Freeze(_) | StakeType::Unfreeze(_) => {
9494
Err("Unsupported stake type for Sui".into())
9595
}
9696
},
97-
TransactionInputType::Swap(_, _, _) => Ok((Vec::new(), Vec::new(), Vec::new())),
98-
TransactionInputType::Generic(_, _, _) => Ok((Vec::new(), Vec::new(), Vec::new())),
97+
TransactionInputType::Swap(_, _, _) => Ok((OwnedCoins::default(), None, Vec::new())),
98+
TransactionInputType::Generic(_, _, _) => Ok((OwnedCoins::default(), None, Vec::new())),
9999
TransactionInputType::TransferNft(_, _) | TransactionInputType::Account(_, _) => Err("Unsupported transaction type for Sui".into()),
100100
_ => Err("Unsupported transaction type for Sui".into()),
101101
}

0 commit comments

Comments
 (0)