Skip to content

Commit

Permalink
Add gas coin split and join at client API (MystenLabs#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
lxfind authored Feb 22, 2022
1 parent a4cd053 commit adb9f15
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 9 deletions.
132 changes: 126 additions & 6 deletions sui_core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use move_core_types::language_storage::TypeTag;
use sui_framework::build_move_package_to_bytes;
use sui_types::crypto::Signature;
use sui_types::{
base_types::*, committee::Committee, error::SuiError, fp_ensure, messages::*,
object::ObjectRead,
base_types::*, coin, committee::Committee, error::SuiError, fp_ensure, gas_coin, messages::*,
object::ObjectRead, SUI_FRAMEWORK_ADDRESS,
};
use typed_store::rocks::open_cf;
use typed_store::Map;
Expand All @@ -24,6 +24,8 @@ use std::{
pin::Pin,
};

use self::client_responses::{MergeCoinResponse, SplitCoinResponse};

/// a Trait object for `signature::Signer` that is:
/// - Pin, i.e. confined to one place in memory (we don't want to copy private keys).
/// - Sync, i.e. can be safely shared between threads.
Expand All @@ -32,11 +34,9 @@ use std::{
///
pub type StableSyncSigner = Pin<Box<dyn signature::Signer<Signature> + Send + Sync>>;

pub mod client_responses;
pub mod client_store;

#[cfg(test)]
use sui_types::SUI_FRAMEWORK_ADDRESS;

pub type AsyncResult<'a, T, E> = future::BoxFuture<'a, Result<T, E>>;

pub struct ClientAddressManager<A> {
Expand Down Expand Up @@ -139,6 +139,36 @@ pub trait Client {
gas_budget: u64,
) -> Result<(CertifiedOrder, OrderEffects), anyhow::Error>;

/// Split the coin object (identified by `coin_object_ref`) into
/// multiple new coins. The amount of each new coin is specified in
/// `split_amounts`. Remaining balance is kept in the original
/// coin object.
/// Note that the order of the new coins in SplitCoinResponse will
/// not be the same as the order of `split_amounts`.
async fn split_coin(
&mut self,
coin_object_ref: ObjectRef,
split_amounts: Vec<u64>,
gas_payment: ObjectRef,
gas_budget: u64,
) -> Result<SplitCoinResponse, anyhow::Error>;

/// Merge the `coin_to_merge` coin object into `primary_coin`.
/// After this merge, the balance of `primary_coin` will become the
/// sum of the two, while `coin_to_merge` will be deleted.
///
/// Returns a pair:
/// (update primary coin object reference, updated gas payment object reference)
///
/// TODO: Support merging a vector of coins.
async fn merge_coins(
&mut self,
primary_coin: ObjectRef,
coin_to_merge: ObjectRef,
gas_payment: ObjectRef,
gas_budget: u64,
) -> Result<MergeCoinResponse, anyhow::Error>;

/// Get the object information
async fn get_object_info(&mut self, object_id: ObjectID) -> Result<ObjectRead, anyhow::Error>;

Expand Down Expand Up @@ -304,7 +334,6 @@ where
&self.authorities
}

#[cfg(test)]
pub async fn get_framework_object_ref(&mut self) -> Result<ObjectRef, anyhow::Error> {
let info = self
.get_object_info(ObjectID::from(SUI_FRAMEWORK_ADDRESS))
Expand Down Expand Up @@ -647,6 +676,97 @@ where
self.execute_transaction(move_publish_order).await
}

async fn split_coin(
&mut self,
coin_object_ref: ObjectRef,
split_amounts: Vec<u64>,
gas_payment: ObjectRef,
gas_budget: u64,
) -> Result<SplitCoinResponse, anyhow::Error> {
// TODO: Hardcode the coin type to be GAS coin for now.
// We should support splitting arbitrary coin type.
let coin_type = gas_coin::GAS::type_tag();

let move_call_order = Order::new_move_call(
self.address,
self.get_framework_object_ref().await?,
coin::COIN_MODULE_NAME.to_owned(),
coin::COIN_SPLIT_VEC_FUNC_NAME.to_owned(),
vec![coin_type],
gas_payment,
vec![coin_object_ref],
vec![bcs::to_bytes(&split_amounts)?],
gas_budget,
&*self.secret,
);
let (certificate, effects) = self.execute_transaction(move_call_order).await?;
if let ExecutionStatus::Failure { gas_used: _, error } = effects.status {
return Err(error.into());
}
let created = &effects.created;
fp_ensure!(
effects.mutated.len() == 2 // coin and gas
&& created.len() == split_amounts.len()
&& created.iter().all(|(_, owner)| owner == &self.address),
SuiError::IncorrectGasSplit.into()
);
let updated_coin = self
.get_object_info(coin_object_ref.0)
.await?
.into_object()?;
let mut new_coins = Vec::with_capacity(created.len());
for ((id, _, _), _) in created {
new_coins.push(self.get_object_info(*id).await?.into_object()?);
}
let updated_gas = self.get_object_info(gas_payment.0).await?.into_object()?;
Ok(SplitCoinResponse {
certificate,
updated_coin,
new_coins,
updated_gas,
})
}

async fn merge_coins(
&mut self,
primary_coin: ObjectRef,
coin_to_merge: ObjectRef,
gas_payment: ObjectRef,
gas_budget: u64,
) -> Result<MergeCoinResponse, anyhow::Error> {
// TODO: Hardcode the coin type to be GAS coin for now.
// We should support merging arbitrary coin type.
let coin_type = gas_coin::GAS::type_tag();

let move_call_order = Order::new_move_call(
self.address,
self.get_framework_object_ref().await?,
coin::COIN_MODULE_NAME.to_owned(),
coin::COIN_JOIN_FUNC_NAME.to_owned(),
vec![coin_type],
gas_payment,
vec![primary_coin, coin_to_merge],
vec![],
gas_budget,
&*self.secret,
);
let (certificate, effects) = self.execute_transaction(move_call_order).await?;
if let ExecutionStatus::Failure { gas_used: _, error } = effects.status {
return Err(error.into());
}
fp_ensure!(
effects.mutated.len() == 2, // coin and gas
SuiError::IncorrectGasMerge.into()
);
let updated_coin = self.get_object_info(primary_coin.0).await?.into_object()?;
let updated_gas = self.get_object_info(gas_payment.0).await?.into_object()?;
Ok(MergeCoinResponse {
certificate,
updated_coin,
updated_gas,
})
}

async fn get_object_info(&mut self, object_id: ObjectID) -> Result<ObjectRead, anyhow::Error> {
self.authorities.get_object_info_execute(object_id).await
}
Expand Down
25 changes: 25 additions & 0 deletions sui_core/src/client/client_responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use sui_types::messages::CertifiedOrder;
use sui_types::object::Object;

pub struct SplitCoinResponse {
/// Certificate of the order
pub certificate: CertifiedOrder,
/// The updated original coin object after split
pub updated_coin: Object,
/// All the newly created coin objects generated from the split
pub new_coins: Vec<Object>,
/// The updated gas payment object after deducting payment
pub updated_gas: Object,
}

pub struct MergeCoinResponse {
/// Certificate of the order
pub certificate: CertifiedOrder,
/// The updated original coin object after merge
pub updated_coin: Object,
/// The updated gas payment object after deducting payment
pub updated_gas: Object,
}
97 changes: 97 additions & 0 deletions sui_core/src/unit_tests/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::{
};
use sui_types::crypto::get_key_pair;
use sui_types::crypto::Signature;
use sui_types::gas_coin::GasCoin;
use sui_types::object::{Data, Object, GAS_VALUE_FOR_TESTING, OBJECT_START_VERSION};
use tokio::runtime::Runtime;
use typed_store::Map;
Expand Down Expand Up @@ -2346,3 +2347,99 @@ async fn test_address_manager() {
assert_eq!(order_effects.created.len(), 1);
assert_eq!(client1.store().objects.iter().count(), 4);
}

#[tokio::test]
async fn test_coin_split() {
let (authority_clients, committee) = init_local_authorities(4).await;
let mut client1 = make_client(authority_clients.clone(), committee);

let coin_object_id = ObjectID::random();
let gas_object_id = ObjectID::random();

// Populate authorities with obj data
let objects = fund_account_with_same_objects(
authority_clients.values().collect(),
&mut client1,
vec![coin_object_id, gas_object_id],
)
.await;
let coin_object = objects.get(&coin_object_id).unwrap();
let gas_object = objects.get(&gas_object_id).unwrap();

let split_amounts = vec![100, 200, 300, 400, 500];
let total_amount: u64 = split_amounts.iter().sum();

let response = client1
.split_coin(
coin_object.to_object_reference(),
split_amounts.clone(),
gas_object.to_object_reference(),
GAS_VALUE_FOR_TESTING,
)
.await
.unwrap();
assert_eq!(
(coin_object_id, coin_object.version().increment()),
(response.updated_coin.id(), response.updated_coin.version())
);
assert_eq!(
(gas_object_id, gas_object.version().increment()),
(response.updated_gas.id(), response.updated_gas.version())
);
let update_coin = GasCoin::try_from(response.updated_coin.data.try_as_move().unwrap()).unwrap();
assert_eq!(update_coin.value(), GAS_VALUE_FOR_TESTING - total_amount);
let split_coin_values = response
.new_coins
.iter()
.map(|o| {
GasCoin::try_from(o.data.try_as_move().unwrap())
.unwrap()
.value()
})
.collect::<BTreeSet<_>>();
assert_eq!(
split_amounts,
split_coin_values.into_iter().collect::<Vec<_>>()
);
}

#[tokio::test]
async fn test_coin_merge() {
let (authority_clients, committee) = init_local_authorities(4).await;
let mut client1 = make_client(authority_clients.clone(), committee);

let coin_object_id1 = ObjectID::random();
let coin_object_id2 = ObjectID::random();
let gas_object_id = ObjectID::random();

// Populate authorities with obj data
let objects = fund_account_with_same_objects(
authority_clients.values().collect(),
&mut client1,
vec![coin_object_id1, coin_object_id2, gas_object_id],
)
.await;
let coin_object1 = objects.get(&coin_object_id1).unwrap();
let coin_object2 = objects.get(&coin_object_id2).unwrap();
let gas_object = objects.get(&gas_object_id).unwrap();

let response = client1
.merge_coins(
coin_object1.to_object_reference(),
coin_object2.to_object_reference(),
gas_object.to_object_reference(),
GAS_VALUE_FOR_TESTING,
)
.await
.unwrap();
assert_eq!(
(coin_object_id1, coin_object1.version().increment()),
(response.updated_coin.id(), response.updated_coin.version())
);
assert_eq!(
(gas_object_id, gas_object.version().increment()),
(response.updated_gas.id(), response.updated_gas.version())
);
let update_coin = GasCoin::try_from(response.updated_coin.data.try_as_move().unwrap()).unwrap();
assert_eq!(update_coin.value(), GAS_VALUE_FOR_TESTING * 2);
}
24 changes: 21 additions & 3 deletions sui_programmability/framework/sources/Coin.move
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,43 @@ module FastX::Coin {

/// Consume the coin `c` and add its value to `self`.
/// Aborts if `c.value + self.value > U64_MAX`
public fun join<T>(self: &mut Coin<T>, c: Coin<T>) {
public fun join<T>(self: &mut Coin<T>, c: Coin<T>, _ctx: &mut TxContext) {
let Coin { id, value } = c;
ID::delete(id);
self.value = self.value + value
}

/// Join everything in `coins` with `self`
public fun join_vec<T>(self: &mut Coin<T>, coins: vector<Coin<T>>) {
public fun join_vec<T>(self: &mut Coin<T>, coins: vector<Coin<T>>, ctx: &mut TxContext) {
let i = 0;
let len = Vector::length(&coins);
while (i < len) {
let coin = Vector::remove(&mut coins, i);
join(self, coin);
join(self, coin, ctx);
i = i + 1
};
// safe because we've drained the vector
Vector::destroy_empty(coins)
}

/// Split coin `self` to two coins, one with balance `split_amount`,
/// and the remaining balance is left is `self`.
public fun split<T>(self: &mut Coin<T>, split_amount: u64, ctx: &mut TxContext) {
let new_coin = withdraw(self, split_amount, ctx);
Transfer::transfer(new_coin, TxContext::get_signer_address(ctx));
}

/// Split coin `self` into multiple coins, each with balance specified
/// in `split_amounts`. Remaining balance is left in `self`.
public fun split_vec<T>(self: &mut Coin<T>, split_amounts: vector<u64>, ctx: &mut TxContext) {
let i = 0;
let len = Vector::length(&split_amounts);
while (i < len) {
split(self, *Vector::borrow(&split_amounts, i), ctx);
i = i + 1;
};
}

/// Subtract `value` from `self` and create a new coin
/// worth `value` with ID `id`.
/// Aborts if `value > self.value`
Expand Down
2 changes: 2 additions & 0 deletions sui_types/src/coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use crate::{

pub const COIN_MODULE_NAME: &IdentStr = ident_str!("Coin");
pub const COIN_STRUCT_NAME: &IdentStr = COIN_MODULE_NAME;
pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
pub const COIN_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");

// Rust version of the Move FastX::Coin::Coin type
#[derive(Debug, Serialize, Deserialize)]
Expand Down
4 changes: 4 additions & 0 deletions sui_types/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ pub enum SuiError {
IncorrectRecipientError,
#[error("Too many authority errors were detected.")]
TooManyIncorrectAuthorities,
#[error("Inconsistent gas coin split result.")]
IncorrectGasSplit,
#[error("Inconsistent gas coin merge result.")]
IncorrectGasMerge,

#[error("Account not found.")]
AccountNotFound,
Expand Down
10 changes: 10 additions & 0 deletions sui_types/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,16 @@ impl ObjectRead {
}
}

/// Returns the object value if there is any, otherwise an Err if
/// the object does not exist or is deleted.
pub fn into_object(self) -> Result<Object, SuiError> {
match self {
Self::Deleted(oref) => Err(SuiError::ObjectDeleted { object_ref: oref }),
Self::NotExists(id) => Err(SuiError::ObjectNotFound { object_id: id }),
Self::Exists(_, o, _) => Ok(o),
}
}

/// Returns the layout of the object if it was requested in the read, None if it was not requested or does not have a layout
/// Returns an Err if the object does not exist or is deleted.
pub fn layout(&self) -> Result<&Option<MoveStructLayout>, SuiError> {
Expand Down

0 comments on commit adb9f15

Please sign in to comment.