use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[doc(no_inline)]
pub use sui_sdk_types::Address;
#[doc(no_inline)]
pub use sui_sdk_types::Argument;
#[doc(no_inline)]
pub use sui_sdk_types::FundsWithdrawal;
#[doc(hidden)]
pub use sui_sdk_types::Identifier;
#[doc(inline)]
pub use sui_sdk_types::Input;
#[doc(no_inline)]
pub use sui_sdk_types::MoveCall;
#[doc(no_inline)]
pub use sui_sdk_types::TypeTag;
#[doc(no_inline)]
pub use sui_sdk_types::WithdrawFrom;
use sui_sdk_types::bcs::ToBcs;
use sui_sdk_types::{
Digest, GasPayment, Mutability, ProgrammableTransaction, SharedInput, Transaction,
TransactionExpiration, TransactionKind,
};
#[cfg(test)]
mod tests;
pub type Result<T> = ::std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Serializing to BCS: {0}")]
Bcs(#[from] sui_sdk_types::bcs::Error),
#[error("invariant violation! object has pure argument")]
ObjInvariantViolation,
#[error("invariant violation! object has id does not match call arg")]
InvalidObjArgUpdate,
#[error(transparent)]
MismatchedObjArgKinds(Box<MismatchedObjArgKindsError>),
#[error("tried to use a pure or funds withdrawal argument as an object argument")]
NotAnObjectArg,
}
#[derive(thiserror::Error, Debug)]
#[error(
"Mismatched Object argument kind for object {id}. \
{old_value:?} is not compatible with {new_value:?}"
)]
pub struct MismatchedObjArgKindsError {
pub id: Address,
pub old_value: Input,
pub new_value: Input,
}
#[derive(Clone, Debug, Default)]
pub struct ProgrammableTransactionBuilder {
inputs: IndexMap<BuilderArg, Input>,
commands: Vec<sui_sdk_types::Command>,
}
impl ProgrammableTransactionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn finish(self) -> ProgrammableTransaction {
let Self { inputs, commands } = self;
let inputs = inputs.into_values().collect();
ProgrammableTransaction { inputs, commands }
}
pub fn pure<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<Argument> {
Ok(self.pure_bytes(value.to_bcs()?, false))
}
pub fn force_separate_pure<T: Serialize>(&mut self, value: T) -> Result<Argument> {
Ok(self.pure_bytes(value.to_bcs()?, true))
}
pub fn pure_bytes(&mut self, bytes: Vec<u8>, force_separate: bool) -> Argument {
let key = if force_separate {
BuilderArg::ForcedNonUniquePure(self.inputs.len())
} else {
BuilderArg::Pure(bytes.clone())
};
let (i, _) = self.inputs.insert_full(key, Input::Pure(bytes));
Argument::Input(i as u16)
}
pub fn obj<O: Into<Input>>(&mut self, obj_arg: O) -> Result<Argument> {
let obj_arg = obj_arg.into();
let id = match &obj_arg {
Input::Pure { .. } => return Err(Error::NotAnObjectArg),
Input::ImmutableOrOwned(object_reference) => *object_reference.object_id(),
Input::Shared(shared) => shared.object_id(),
Input::Receiving(object_reference) => *object_reference.object_id(),
Input::FundsWithdrawal(_) => return Err(Error::NotAnObjectArg),
_ => panic!("unknown Input variant"),
};
let key = BuilderArg::Object(id);
let mut input_arg = obj_arg;
if let Some(old_value) = self.inputs.get(&key) {
if matches!(old_value, Input::Pure { .. }) {
return Err(Error::ObjInvariantViolation);
}
input_arg = match (old_value, input_arg) {
(Input::Shared(shared1), Input::Shared(shared2))
if shared1.version() == shared2.version() =>
{
if shared1.object_id() != shared2.object_id() {
return Err(Error::InvalidObjArgUpdate);
}
let mutable =
shared1.mutability().is_mutable() || shared2.mutability().is_mutable();
Input::Shared(SharedInput::new(
shared2.object_id(),
shared2.version(),
Mutability::from(mutable),
))
}
(old_value, new_value) if old_value != &new_value => {
return Err(Error::MismatchedObjArgKinds(Box::new(
MismatchedObjArgKindsError {
id,
old_value: old_value.clone(),
new_value,
},
)));
}
(_, new_value) => new_value,
};
}
let (i, _) = self.inputs.insert_full(key, input_arg);
Ok(Argument::Input(i as u16))
}
pub fn command(&mut self, command: impl Into<sui_sdk_types::Command>) -> Argument {
let i = self.commands.len();
self.commands.push(command.into());
Argument::Result(i as u16)
}
}
impl ProgrammableTransactionBuilder {
pub fn funds_withdrawal(&mut self, withdrawal: FundsWithdrawal) -> Argument {
let key = BuilderArg::ForcedNonUniquePure(self.inputs.len());
let (i, _) = self
.inputs
.insert_full(key, Input::FundsWithdrawal(withdrawal));
Argument::Input(i as u16)
}
pub fn balance_from_sender(&mut self, amount: u64, coin_type: TypeTag) -> Argument {
self.funds_withdrawal(FundsWithdrawal::new(
amount,
coin_type,
WithdrawFrom::Sender,
))
}
pub fn balance_from_sponsor(&mut self, amount: u64, coin_type: TypeTag) -> Argument {
self.funds_withdrawal(FundsWithdrawal::new(
amount,
coin_type,
WithdrawFrom::Sponsor,
))
}
#[allow(clippy::too_many_arguments)]
pub fn finish_address_balance(
self,
sender: Address,
sponsor: Address,
chain_identifier: Digest,
nonce: u32,
gas_price: u64,
gas_budget: u64,
current_epoch: u64,
) -> Transaction {
Transaction {
kind: TransactionKind::ProgrammableTransaction(self.finish()),
sender,
gas_payment: GasPayment {
objects: vec![],
owner: sponsor,
price: gas_price,
budget: gas_budget,
},
expiration: TransactionExpiration::ValidDuring {
min_epoch: Some(current_epoch),
max_epoch: Some(current_epoch.saturating_add(1)),
min_timestamp: None,
max_timestamp: None,
chain: chain_identifier,
nonce,
},
}
}
}
impl ProgrammableTransactionBuilder {
pub fn split_coins_into_vec(
&mut self,
coin: Argument,
amounts: Vec<Argument>,
) -> Vec<Argument> {
let idxs = 0..amounts.len() as u16;
let Argument::Result(coin_vec) = self.command(Command::SplitCoins(coin, amounts)) else {
panic!("ProgrammableTransactionBuilder::command always gives an Argument::Result")
};
idxs.map(|i| Argument::NestedResult(coin_vec, i)).collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum BuilderArg {
Object(Address),
Pure(Vec<u8>),
ForcedNonUniquePure(usize),
}
impl From<ProgrammableTransactionBuilder> for ProgrammableTransaction {
fn from(value: ProgrammableTransactionBuilder) -> Self {
value.finish()
}
}
impl TryFrom<ProgrammableTransaction> for ProgrammableTransactionBuilder {
type Error = Error;
fn try_from(
ProgrammableTransaction { inputs, commands }: ProgrammableTransaction,
) -> Result<Self> {
use Input::*;
let mut self_ = Self::new();
for input in inputs {
match input {
Pure(value) => {
self_.pure_bytes(value, true);
}
ImmutableOrOwned(object_reference) => {
self_.obj(Input::ImmutableOrOwned(object_reference))?;
}
Shared(shared) => {
self_.obj(Input::Shared(shared.clone()))?;
}
Receiving(object_reference) => {
self_.obj(Input::Receiving(object_reference))?;
}
FundsWithdrawal(funds_withdrawal) => {
let key = BuilderArg::ForcedNonUniquePure(self_.inputs.len());
self_
.inputs
.insert_full(key, Input::FundsWithdrawal(funds_withdrawal));
}
_ => panic!("unknown Input variant"),
}
}
for command in commands {
self_.command(command);
}
Ok(self_)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum Command {
MoveCall(Box<MoveCall>),
TransferObjects(Vec<Argument>, Argument),
SplitCoins(Argument, Vec<Argument>),
MergeCoins(Argument, Vec<Argument>),
Publish(Vec<Vec<u8>>, Vec<Address>),
MakeMoveVec(Option<TypeTag>, Vec<Argument>),
Upgrade(Vec<Vec<u8>>, Vec<Address>, Address, Argument),
}
#[allow(clippy::fallible_impl_from)]
impl From<sui_sdk_types::Command> for Command {
fn from(value: sui_sdk_types::Command) -> Self {
use sui_sdk_types::Command::*;
match value {
MoveCall(args) => Self::MoveCall(Box::new(args)),
TransferObjects(args) => Self::TransferObjects(args.objects, args.address),
SplitCoins(args) => Self::SplitCoins(args.coin, args.amounts),
MergeCoins(args) => Self::MergeCoins(args.coin, args.coins_to_merge),
Publish(args) => Self::Publish(args.modules, args.dependencies),
MakeMoveVector(args) => Self::MakeMoveVec(args.type_, args.elements),
Upgrade(args) => {
Self::Upgrade(args.modules, args.dependencies, args.package, args.ticket)
}
_ => panic!("unknown Command variant"),
}
}
}
impl From<Command> for sui_sdk_types::Command {
fn from(value: Command) -> Self {
use Command::*;
use sui_sdk_types::{
MakeMoveVector, MergeCoins, Publish, SplitCoins, TransferObjects, Upgrade,
};
match value {
MoveCall(move_call) => Self::MoveCall(*move_call),
TransferObjects(objects, address) => {
Self::TransferObjects(TransferObjects { objects, address })
}
SplitCoins(coin, amounts) => Self::SplitCoins(SplitCoins { coin, amounts }),
MergeCoins(coin, coins_to_merge) => Self::MergeCoins(MergeCoins {
coin,
coins_to_merge,
}),
Publish(modules, dependencies) => Self::Publish(Publish {
modules,
dependencies,
}),
MakeMoveVec(type_, elements) => {
Self::MakeMoveVector(MakeMoveVector { type_, elements })
}
Upgrade(modules, dependencies, package, ticket) => Self::Upgrade(Upgrade {
modules,
dependencies,
package,
ticket,
}),
}
}
}
impl Command {
pub fn move_call(
package: Address,
module: Identifier,
function: Identifier,
type_arguments: Vec<TypeTag>,
arguments: Vec<Argument>,
) -> Self {
Self::MoveCall(Box::new(MoveCall {
package,
module,
function,
type_arguments,
arguments,
}))
}
pub const fn make_move_vec(ty: Option<TypeTag>, args: Vec<Argument>) -> Self {
Self::MakeMoveVec(ty, args)
}
}
#[macro_export]
macro_rules! ptb {
($($tt:tt)*) => {
{
let mut builder = $crate::ProgrammableTransactionBuilder::new();
$crate::ptbuilder!(builder { $($tt)* });
builder.finish()
}
};
}
#[macro_export]
macro_rules! ptbuilder {
($builder:ident {}) => { };
($builder:ident {
package $name:ident $value:literal;
$($tt:tt)*
}) => {
let $name: $crate::Address = $value.parse()?;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
package $name:ident;
$($tt:tt)*
}) => {
let $name: $crate::Address = $name;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
package $name:ident: $value:expr_2021;
$($tt:tt)*
}) => {
let $name: $crate::Address = $value;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
input pure $name:ident;
$($tt:tt)*
}) => {
let $name = $builder.pure($name)?;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
input pure $name:ident: $value:expr_2021;
$($tt:tt)*
}) => {
let $name = $builder.pure($value)?;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
input obj $name:ident;
$($tt:tt)*
}) => {
let $name = $builder.obj($name)?;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
input obj $name:ident: $value:expr_2021;
$($tt:tt)*
}) => {
let $name = $builder.obj($value)?;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
type $T:ident;
$($tt:tt)*
}) => {
#[allow(non_snake_case)]
let $T: $crate::TypeTag = $T;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
type $T:ident = $value:expr_2021;
$($tt:tt)*
}) => {
#[allow(non_snake_case)]
let $T: $crate::TypeTag = $value;
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
$package:ident::$module:ident::$fun:ident$(<$($T:ident),+>)?($($arg:ident),* $(,)?);
$($tt:tt)*
}) => {
let _module = stringify!($module);
let _fun = stringify!($fun);
$builder.command($crate::Command::move_call(
$package,
$crate::Identifier::from_static(_module),
$crate::Identifier::from_static(_fun),
vec![$($($T.clone()),+)?],
vec![$($arg),*]
));
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
let $ret:ident = $package:ident::$module:ident::$fun:ident$(<$($T:ident),+>)?($($arg:ident),* $(,)?);
$($tt:tt)*
}) => {
let _module = stringify!($module);
let _fun = stringify!($fun);
let $ret = $builder.command($crate::Command::move_call(
$package,
$crate::Identifier::from_static(_module),
$crate::Identifier::from_static(_fun),
vec![$($($T.clone()),+)?],
vec![$($arg),*]
));
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
let ($($ret:ident),+) = $package:ident::$module:ident::$fun:ident$(<$($T:ident),+>)?($($arg:ident),* $(,)?);
$($tt:tt)*
}) => {
let _module = stringify!($module);
let _fun = stringify!($fun);
let rets = $builder.command($crate::Command::move_call(
$package,
$crate::Identifier::from_static(_module),
$crate::Identifier::from_static(_fun),
vec![$($($T.clone()),+)?],
vec![$($arg),*]
));
$crate::unpack_arg!(rets => { $($ret),+ });
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
$(let $ret:ident =)? command! $variant:ident($($args:tt)*);
$($tt:tt)*
}) => {
$(let $ret =)? $builder.command($crate::Command::$variant($($args)*));
$crate::ptbuilder!($builder { $($tt)* });
};
($builder:ident {
let ($($ret:ident),+) = command! $variant:ident($($args:tt)*);
$($tt:tt)*
}) => {
let rets = $builder.command($crate::Command::$variant($($args)*));
$crate::unpack_arg!(rets => { $($ret),+ });
$crate::ptbuilder!($builder { $($tt)* });
};
}
#[macro_export]
macro_rules! unpack_arg {
($arg:expr_2021 => {
$($name:ident),+ $(,)?
}) => {
let ($($name),+) = if let $crate::Argument::Result(tuple) = $arg {
let mut index = 0;
$(
let $name = $crate::Argument::NestedResult(
tuple, index
);
index += 1;
)+
($($name),+)
} else {
panic!(
"ProgrammableTransactionBuilder::command should always give a Argument::Result"
)
};
};
}