use crate::Argument;
use crate::Function;
use crate::ObjectInput;
use crate::TransactionBuilder;
use crate::builder::ResolvedArgument;
use crate::intent::BoxError;
use crate::intent::Intent;
use crate::intent::IntentResolver;
use crate::intent::MAX_ARGUMENTS;
use crate::intent::MAX_GAS_OBJECTS;
use futures::StreamExt;
use std::collections::BTreeMap;
use std::collections::HashSet;
use sui_rpc::field::FieldMask;
use sui_rpc::field::FieldMaskUtil;
use sui_rpc::proto::sui::rpc::v2::GetBalanceRequest;
use sui_rpc::proto::sui::rpc::v2::ListOwnedObjectsRequest;
use sui_sdk_types::Address;
use sui_sdk_types::Identifier;
use sui_sdk_types::StructTag;
pub struct Coin {
coin_type: StructTag,
balance: u64,
use_gas_coin: bool,
}
pub type CoinWithBalance = Coin;
impl Coin {
pub fn new(coin_type: StructTag, balance: u64) -> Self {
Self {
coin_type,
balance,
use_gas_coin: true,
}
}
pub fn sui(balance: u64) -> Self {
Self {
coin_type: StructTag::sui(),
balance,
use_gas_coin: true,
}
}
pub fn with_use_gas_coin(self, use_gas_coin: bool) -> Self {
Self {
use_gas_coin,
..self
}
}
}
pub struct Balance {
coin_type: StructTag,
balance: u64,
use_gas_coin: bool,
}
impl Balance {
pub fn new(coin_type: StructTag, balance: u64) -> Self {
Self {
coin_type,
balance,
use_gas_coin: true,
}
}
pub fn sui(balance: u64) -> Self {
Self {
coin_type: StructTag::sui(),
balance,
use_gas_coin: true,
}
}
pub fn with_use_gas_coin(self, use_gas_coin: bool) -> Self {
Self {
use_gas_coin,
..self
}
}
}
impl Intent for Coin {
fn register(self, builder: &mut TransactionBuilder) -> Argument {
builder.register_resolver(Resolver);
builder.unresolved(self)
}
}
impl Intent for Balance {
fn register(self, builder: &mut TransactionBuilder) -> Argument {
builder.register_resolver(Resolver);
builder.unresolved(self)
}
}
#[derive(Debug)]
struct Resolver;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IntentKind {
Coin,
Balance,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CoinType {
Gas,
Coin(StructTag),
}
struct Request {
id: usize,
balance: u64,
kind: IntentKind,
}
#[async_trait::async_trait]
impl IntentResolver for Resolver {
async fn resolve(
&self,
builder: &mut TransactionBuilder,
client: &mut sui_rpc::Client,
) -> Result<(), BoxError> {
let mut requests: BTreeMap<CoinType, Vec<Request>> = BTreeMap::new();
let mut zero_values = Vec::new();
for (id, intent) in builder.intents.extract_if(.., |_id, intent| {
intent.downcast_ref::<Coin>().is_some() || intent.downcast_ref::<Balance>().is_some()
}) {
if let Some(coin) = intent.downcast_ref::<Coin>() {
if coin.balance == 0 {
zero_values.push((id, coin.coin_type.clone(), IntentKind::Coin));
} else {
let coin_type = if coin.coin_type == StructTag::sui() && coin.use_gas_coin {
CoinType::Gas
} else {
CoinType::Coin(coin.coin_type.clone())
};
requests.entry(coin_type).or_default().push(Request {
id,
balance: coin.balance,
kind: IntentKind::Coin,
});
}
} else if let Some(bal) = intent.downcast_ref::<Balance>() {
if bal.balance == 0 {
zero_values.push((id, bal.coin_type.clone(), IntentKind::Balance));
} else {
let coin_type = if bal.coin_type == StructTag::sui() && bal.use_gas_coin {
CoinType::Gas
} else {
CoinType::Coin(bal.coin_type.clone())
};
requests.entry(coin_type).or_default().push(Request {
id,
balance: bal.balance,
kind: IntentKind::Balance,
});
}
}
}
for (id, coin_type, kind) in zero_values {
Resolver::resolve_zero(builder, coin_type, id, kind);
}
for (coin_type, requests) in requests {
Resolver::resolve_merge_and_split(builder, client, coin_type, &requests).await?;
}
Ok(())
}
}
impl Resolver {
fn resolve_zero(
builder: &mut TransactionBuilder,
coin_type: StructTag,
request_id: usize,
kind: IntentKind,
) {
let (module, function) = match kind {
IntentKind::Coin => ("coin", "zero"),
IntentKind::Balance => ("balance", "zero"),
};
let result = builder.move_call(
Function::new(
Address::TWO,
Identifier::from_static(module),
Identifier::from_static(function),
)
.with_type_args(vec![coin_type.into()]),
vec![],
);
*builder.arguments.get_mut(&request_id).unwrap() = ResolvedArgument::ReplaceWith(result);
}
fn into_balance(
builder: &mut TransactionBuilder,
coin_type: &StructTag,
coin_arg: Argument,
) -> Argument {
builder.move_call(
Function::new(
Address::TWO,
Identifier::from_static("coin"),
Identifier::from_static("into_balance"),
)
.with_type_args(vec![coin_type.clone().into()]),
vec![coin_arg],
)
}
fn send_funds(
builder: &mut TransactionBuilder,
coin_type: &StructTag,
coin_arg: Argument,
owner: Address,
) {
let owner_arg = builder.pure(&owner);
builder.move_call(
Function::new(
Address::TWO,
Identifier::from_static("coin"),
Identifier::from_static("send_funds"),
)
.with_type_args(vec![coin_type.clone().into()]),
vec![coin_arg, owner_arg],
);
}
fn resolve_individual_withdrawals(
builder: &mut TransactionBuilder,
coin_type: &StructTag,
requests: &[Request],
) {
for request in requests {
let result = match request.kind {
IntentKind::Coin => {
builder.funds_withdrawal_coin(coin_type.clone().into(), request.balance)
}
IntentKind::Balance => {
builder.funds_withdrawal_balance(coin_type.clone().into(), request.balance)
}
};
*builder.arguments.get_mut(&request.id).unwrap() =
ResolvedArgument::ReplaceWith(result);
}
}
async fn resolve_merge_and_split(
builder: &mut TransactionBuilder,
client: &mut sui_rpc::Client,
coin_type: CoinType,
requests: &[Request],
) -> Result<(), BoxError> {
let (coin_type, use_gas_coin) = match coin_type {
CoinType::Gas => (StructTag::sui(), true),
CoinType::Coin(t) => (t, false),
};
let coin_type = &coin_type;
let sender = builder
.sender()
.ok_or("Sender must be set to resolve Coin/Balance intents")?;
if requests.is_empty() {
return Err("BUG: requests is empty".into());
}
let sum = requests.iter().map(|r| r.balance).sum();
let balance_response = client
.state_client()
.get_balance(
GetBalanceRequest::default()
.with_owner(sender)
.with_coin_type(coin_type),
)
.await?;
let _chain_id = {
use sui_rpc::client::ResponseExt;
(balance_response.chain_id(), balance_response.epoch())
};
let balance = balance_response
.into_inner()
.balance
.take()
.unwrap_or_default();
if balance.balance() < sum {
return Err(format!(
"address {} does not have sufficient balance of {}: requested {} available {}",
sender,
coin_type,
sum,
balance.balance()
)
.into());
}
if balance.address_balance() >= sum {
Self::resolve_individual_withdrawals(builder, coin_type, requests);
return Ok(());
}
let excludes = builder.used_object_ids();
let (coins, remaining) =
Self::select_coins(client, &sender, coin_type, sum, &excludes).await?;
if balance.address_balance() < remaining {
return Err(format!(
"unable to find sufficient coins of type {}. \
requested {} found {} and AB of {} is insufficient to cover difference.",
coin_type,
sum,
sum - remaining,
balance.address_balance(),
)
.into());
}
if coins.is_empty() {
Self::resolve_individual_withdrawals(builder, coin_type, requests);
return Ok(());
}
let has_balance_intents = requests.iter().any(|r| r.kind == IntentKind::Balance);
let used_ab = remaining > 0;
let (base, deps) = if use_gas_coin {
Self::prepare_gas_coin_sources(
builder,
&coins,
remaining,
coin_type,
balance.address_balance(),
)
} else {
Self::prepare_coin_sources(builder, coins, remaining, coin_type)
};
let amounts = requests.iter().map(|r| builder.pure(&r.balance)).collect();
let outputs = builder.split_coins(base, amounts);
if !deps.is_empty() {
builder
.commands
.last_entry()
.unwrap()
.get_mut()
.dependencies
.extend(deps);
}
if !use_gas_coin && (used_ab || has_balance_intents) {
Self::send_funds(builder, coin_type, base, sender);
}
for (result, request) in outputs.into_iter().zip(requests.iter()) {
let final_arg = match request.kind {
IntentKind::Balance => Self::into_balance(builder, coin_type, result),
IntentKind::Coin => result,
};
*builder.arguments.get_mut(&request.id).unwrap() =
ResolvedArgument::ReplaceWith(final_arg);
}
Ok(())
}
fn prepare_coin_sources(
builder: &mut TransactionBuilder,
coins: Vec<ObjectInput>,
remaining: u64,
coin_type: &StructTag,
) -> (Argument, Vec<Argument>) {
let coin_args: Vec<Argument> = coins.into_iter().map(|c| builder.object(c)).collect();
let (&first, rest) = coin_args.split_first().expect("coins must not be empty");
let mut deps = Vec::new();
for chunk in rest.chunks(MAX_ARGUMENTS) {
builder.merge_coins(first, chunk.to_vec());
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
}
if remaining > 0 {
let ab_coin = builder.funds_withdrawal_coin(coin_type.clone().into(), remaining);
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
builder.merge_coins(first, vec![ab_coin]);
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
}
(first, deps)
}
fn prepare_gas_coin_sources(
builder: &mut TransactionBuilder,
coins: &[ObjectInput],
remaining: u64,
coin_type: &StructTag,
address_balance: u64,
) -> (Argument, Vec<Argument>) {
let gas = builder.gas();
let mut deps = Vec::new();
let (use_as_gas, excess) = coins.split_at(std::cmp::min(
coins.len(),
MAX_GAS_OBJECTS.saturating_sub(builder.gas.len()),
));
builder.add_gas_objects(use_as_gas.iter().cloned());
for chunk in excess
.iter()
.map(|coin| builder.object(coin.clone()))
.collect::<Vec<_>>()
.chunks(MAX_ARGUMENTS)
{
builder.merge_coins(gas, chunk.to_vec());
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
}
if remaining > 0 {
let ab_coin = builder.funds_withdrawal_coin(coin_type.clone().into(), address_balance);
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
builder.merge_coins(gas, vec![ab_coin]);
deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
}
(gas, deps)
}
async fn select_coins(
client: &mut sui_rpc::Client,
owner_address: &Address,
coin_type: &StructTag,
amount: u64,
excludes: &HashSet<Address>,
) -> Result<(Vec<ObjectInput>, u64), BoxError> {
let coin_struct = StructTag::coin(coin_type.clone().into());
let list_request = ListOwnedObjectsRequest::default()
.with_owner(owner_address)
.with_object_type(&coin_struct)
.with_page_size(500u32)
.with_read_mask(FieldMask::from_paths([
"object_id",
"version",
"digest",
"balance",
"owner",
]));
let mut coin_stream = Box::pin(client.list_owned_objects(list_request));
let mut selected_coins = Vec::new();
let mut remaining = amount;
while let Some(object_result) = coin_stream.next().await {
let object = object_result?;
let coin = ObjectInput::try_from_object_proto(&object)?;
if excludes.contains(&coin.object_id()) {
continue;
}
remaining = remaining.saturating_sub(object.balance());
selected_coins.push(coin);
if remaining == 0 && selected_coins.len() >= 500 {
break;
}
}
Ok((selected_coins, remaining))
}
}