Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/gem_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ pub trait Client: Send + Sync + Debug {
R: DeserializeOwned;
}

pub trait ClientBounds: Client + Clone + Send + Sync + 'static {}

impl<T> ClientBounds for T where T: Client + Clone + Send + Sync + 'static {}

pub trait DebugClientBounds: ClientBounds + Debug {}

impl<T> DebugClientBounds for T where T: ClientBounds + Debug {}

#[async_trait]
pub trait ClientExt: Client {
async fn get<R>(&self, path: &str) -> Result<R, ClientError>
Expand Down
68 changes: 68 additions & 0 deletions crates/gem_sui/src/coin_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::{SUI_COIN_TYPE, SUI_COIN_TYPE_FULL};
use primitives::hex::decode_hex;

const SUI_ADDRESS_LENGTH: usize = 32;

pub fn full_coin_type(coin_type: &str) -> String {
let Some((prefix, rest)) = coin_type.split_once("::") else {
return coin_type.to_string();
};
match decode_hex(prefix) {
Ok(bytes) if bytes.len() <= SUI_ADDRESS_LENGTH => {
let mut padded = [0u8; SUI_ADDRESS_LENGTH];
padded[SUI_ADDRESS_LENGTH - bytes.len()..].copy_from_slice(&bytes);
format!("0x{}::{rest}", hex::encode(padded))
}
_ => coin_type.to_string(),
}
}

pub fn coin_type_matches(a: &str, b: &str) -> bool {
full_coin_type(a) == full_coin_type(b)
}

pub fn is_sui_coin(coin_type: &str) -> bool {
coin_type == SUI_COIN_TYPE || coin_type == SUI_COIN_TYPE_FULL
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_full_coin_type() {
assert_eq!(
full_coin_type("0x2::sui::SUI"),
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
);
assert_eq!(
full_coin_type("2::sui::SUI"),
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
);
assert_eq!(
full_coin_type("0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"),
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
);
assert_eq!(full_coin_type("0xabc"), "0xabc");
assert_eq!(full_coin_type("not-a-type::coin::COIN"), "not-a-type::coin::COIN");
}

#[test]
fn test_coin_type_matches() {
assert!(coin_type_matches("0x2::sui::SUI", "0x2::sui::SUI"));
assert!(coin_type_matches("0x2::sui::SUI", "2::sui::SUI"));
assert!(coin_type_matches("2::sui::SUI", "0x2::sui::SUI"));
assert!(coin_type_matches(
"0x2::sui::SUI",
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
));
assert!(!coin_type_matches("0x2::sui::SUI", "0x3::token::TOKEN"));
}

#[test]
fn test_is_sui_coin() {
assert!(is_sui_coin(SUI_COIN_TYPE));
assert!(is_sui_coin(SUI_COIN_TYPE_FULL));
assert!(!is_sui_coin("0x3::token::TOKEN"));
}
}
29 changes: 29 additions & 0 deletions crates/gem_sui/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::{
error::Error,
fmt::{Display, Formatter},
};

#[derive(Debug)]
pub enum SuiError {
InvalidInput(String),
InsufficientBalance,
NoGasCoins,
}

impl Display for SuiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidInput(message) => write!(f, "{message}"),
Self::InsufficientBalance => write!(f, "insufficient Sui coin balance"),
Self::NoGasCoins => write!(f, "No SUI coins available for gas"),
}
}
}

impl Error for SuiError {}

impl SuiError {
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::InvalidInput(message.into())
}
}
12 changes: 12 additions & 0 deletions crates/gem_sui/src/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ impl Default for ObjectDataOptions {
}
}

impl ObjectDataOptions {
pub fn owner_only() -> Self {
Self {
show_type: false,
show_owner: true,
show_display: false,
show_content: false,
show_bcs: false,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SuiData<T> {
Expand Down
9 changes: 7 additions & 2 deletions crates/gem_sui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod address;
pub use address::validate_address;
pub mod coin_type;
pub use coin_type::{coin_type_matches, full_coin_type, is_sui_coin};
#[cfg(feature = "rpc")]
pub mod rpc;
#[cfg(feature = "rpc")]
Expand All @@ -15,18 +17,21 @@ pub mod transfer_builder;
#[cfg(feature = "rpc")]
pub use transfer_builder::*;

pub mod error;
pub mod gas_budget;
pub mod jsonrpc;
pub mod operations;
pub mod tx_builder;

#[cfg(feature = "signer")]
pub mod signer;

pub use error::SuiError;
use models::Coin;
pub use models::ObjectId;
pub use operations::*;
use std::error::Error;
use sui_transaction_builder::ObjectInput;
pub use tx_builder::{decode_transaction, stake::*, transfer::*, validate_and_hash};
pub use tx_builder::{stake, transfer};

pub const SUI_SYSTEM_ID: &str = "sui_system";

Expand Down
20 changes: 20 additions & 0 deletions crates/gem_sui/src/models/coin_asset.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use num_bigint::BigInt;
use serde::{Deserialize, Serialize};
use serde_serializers::{deserialize_bigint_from_str, deserialize_u64_from_str, serialize_bigint, serialize_u64};
#[cfg(feature = "rpc")]
use std::{error::Error, str::FromStr};
use sui_transaction_builder::ObjectInput;
use sui_types::{Address, Digest};

#[cfg(feature = "rpc")]
use super::SuiCoin;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CoinAsset {
Expand All @@ -22,6 +27,21 @@ impl CoinAsset {
}
}

#[cfg(feature = "rpc")]
impl TryFrom<SuiCoin> for CoinAsset {
type Error = Box<dyn Error + Send + Sync>;

fn try_from(coin: SuiCoin) -> Result<Self, Self::Error> {
Ok(Self {
coin_object_id: Address::from_str(&coin.coin_object_id)?,
coin_type: coin.coin_type,
digest: Digest::from_str(&coin.digest)?,
balance: coin.balance,
version: coin.version.parse()?,
})
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CoinResponse {
Expand Down
1 change: 1 addition & 0 deletions crates/gem_sui/src/models/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub struct SuiTransaction {
#[serde(rename_all = "camelCase")]
pub struct SuiStatus {
pub status: String,
pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
7 changes: 0 additions & 7 deletions crates/gem_sui/src/operations/mod.rs

This file was deleted.

52 changes: 0 additions & 52 deletions crates/gem_sui/src/operations/tx.rs

This file was deleted.

12 changes: 2 additions & 10 deletions crates/gem_sui/src/provider/balances_mapper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::models::Balance as SuiBalance;
use crate::models::staking::SuiStakeDelegation;
use crate::{SUI_COIN_TYPE, SUI_COIN_TYPE_FULL};
use crate::{coin_type_matches, is_sui_coin};
use num_bigint::BigUint;
use primitives::{AssetBalance, AssetId, Balance, Chain};

Expand Down Expand Up @@ -60,7 +60,7 @@ pub fn map_assets_balances(balances: Vec<SuiBalance>) -> Vec<AssetBalance> {
balances
.into_iter()
.filter_map(|balance| {
let asset_id = if balance.coin_type == SUI_COIN_TYPE || balance.coin_type == SUI_COIN_TYPE_FULL {
let asset_id = if is_sui_coin(&balance.coin_type) {
None // Skip native coin as it's handled separately
} else {
Some(AssetId::from_token(Chain::Sui, &balance.coin_type))
Expand All @@ -71,14 +71,6 @@ pub fn map_assets_balances(balances: Vec<SuiBalance>) -> Vec<AssetBalance> {
.collect()
}

fn coin_type_matches(coin_type: &str, token_id: &str) -> bool {
// Remove 0x prefix and normalize for comparison
let coin_type_normalized = coin_type.strip_prefix("0x").unwrap_or(coin_type).to_lowercase();
let token_id_normalized = token_id.strip_prefix("0x").unwrap_or(token_id).to_lowercase();

coin_type_normalized == token_id_normalized
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 4 additions & 0 deletions crates/gem_sui/src/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ impl<C: Client + Clone> SuiClient<C> {
Ok(self.client.call::<ResultData<Vec<SuiCoin>>>("suix_getCoins", params).await?.data)
}

pub async fn get_coin_assets_by_type(&self, address: &str, coin_type: &str) -> Result<Vec<CoinAsset>, Box<dyn Error + Send + Sync>> {
self.get_coins(address, coin_type).await?.into_iter().map(CoinAsset::try_from).collect()
}

pub async fn get_object(&self, object_id: String) -> Result<SuiObject, Box<dyn Error + Send + Sync>> {
let params = serde_json::json!([object_id, {"showContent": true}]);
Ok(self.client.call::<ResultData<SuiObject>>("sui_getObject", params).await?.data)
Expand Down
2 changes: 1 addition & 1 deletion crates/gem_sui/src/transfer_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
ESTIMATION_GAS_BUDGET, SUI_COIN_TYPE, SuiClient,
gas_budget::GAS_BUDGET_MULTIPLIER,
models::{Coin, Gas, TokenTransferInput, TransferInput},
operations::{encode_token_transfer, encode_transfer},
tx_builder::{encode_token_transfer, encode_transfer},
};
use futures::try_join;
use gem_client::Client;
Expand Down
52 changes: 52 additions & 0 deletions crates/gem_sui/src/tx_builder/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#[cfg(feature = "rpc")]
use crate::{SUI_COIN_TYPE, SuiClient, SuiError, models::CoinAsset};
#[cfg(feature = "rpc")]
use gem_client::Client;
#[cfg(feature = "rpc")]
use num_traits::ToPrimitive;
use sui_transaction_builder::ObjectInput;

#[derive(Clone)]
pub struct TransactionBuilderInput {
pub sender: String,
pub gas_price: u64,
pub gas_budget: u64,
pub gas_objects: Vec<ObjectInput>,
}

impl TransactionBuilderInput {
pub fn new(sender: impl Into<String>, gas_price: u64, gas_budget: u64, gas_objects: Vec<ObjectInput>) -> Self {
Self {
sender: sender.into(),
gas_price,
gas_budget,
gas_objects,
}
}

pub fn with_gas_budget(&self, gas_budget: u64) -> Self {
let mut input = self.clone();
input.gas_budget = gas_budget;
input
}

#[cfg(feature = "rpc")]
pub async fn prefetch<C: Client + Clone>(client: &SuiClient<C>, sender: &str, gas_budget: u64) -> Result<Self, SuiError> {
let gas_price = client
.get_gas_price()
.await
.map_err(|err| SuiError::invalid_input(err.to_string()))?
.to_u64()
.ok_or_else(|| SuiError::invalid_input("Sui gas price overflow"))?;
let gas_coins = client
.get_coin_assets_by_type(sender, SUI_COIN_TYPE)
.await
.map_err(|err| SuiError::invalid_input(err.to_string()))?;
if gas_coins.is_empty() {
return Err(SuiError::NoGasCoins);
}
let gas_objects = gas_coins.iter().map(CoinAsset::to_input).collect();

Ok(Self::new(sender, gas_price, gas_budget, gas_objects))
}
}
Loading
Loading