-
Notifications
You must be signed in to change notification settings - Fork 11.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[10/x][programmable transactions] Add migration and builder #8769
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -794,6 +794,30 @@ impl ProgrammableTransaction { | |
} | ||
Ok(()) | ||
} | ||
|
||
fn shared_input_objects(&self) -> impl Iterator<Item = SharedInputObject> + '_ { | ||
self.inputs | ||
.iter() | ||
.filter_map(|arg| match arg { | ||
CallArg::Pure(_) | CallArg::Object(ObjectArg::ImmOrOwnedObject(_)) => None, | ||
CallArg::Object(ObjectArg::SharedObject { | ||
id, | ||
initial_shared_version, | ||
mutable, | ||
}) => Some(vec![SharedInputObject { | ||
id: *id, | ||
initial_shared_version: *initial_shared_version, | ||
mutable: *mutable, | ||
}]), | ||
CallArg::ObjVec(_) => { | ||
panic!( | ||
"not supported in programmable transactions, \ | ||
should be unreachable if the input checker was run" | ||
) | ||
} | ||
}) | ||
.flatten() | ||
} | ||
} | ||
|
||
impl Display for Argument { | ||
|
@@ -900,7 +924,10 @@ impl SingleTransactionKind { | |
Self::Call(_) | Self::ChangeEpoch(_) | Self::ConsensusCommitPrologue(_) => { | ||
Either::Left(self.all_move_call_shared_input_objects()) | ||
} | ||
_ => Either::Right(iter::empty()), | ||
Self::ProgrammableTransaction(pt) => { | ||
Either::Right(Either::Left(pt.shared_input_objects())) | ||
} | ||
_ => Either::Right(Either::Right(iter::empty())), | ||
} | ||
} | ||
|
||
|
@@ -961,7 +988,8 @@ impl SingleTransactionKind { | |
} | ||
} | ||
|
||
pub fn move_call(&self) -> Option<&MoveCall> { | ||
/// Actively being replaced by programmable transactions | ||
pub fn legacy_move_call(&self) -> Option<&MoveCall> { | ||
match &self { | ||
Self::Call(call @ MoveCall { .. }) => Some(call), | ||
_ => None, | ||
|
@@ -1593,10 +1621,11 @@ impl TransactionData { | |
self.kind.shared_input_objects() | ||
} | ||
|
||
pub fn move_calls(&self) -> Vec<&MoveCall> { | ||
/// Actively being replaced by programmable transactions | ||
pub fn legacy_move_calls(&self) -> Vec<&MoveCall> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a document here explaining what's legacy about these move calls, and why you may not want to use this anymore, after programmable transactions takes over the world (i.e. what goes wrong in that case, if you keep using this function). |
||
self.kind | ||
.single_transactions() | ||
.flat_map(|s| s.move_call()) | ||
.flat_map(|s| s.legacy_move_call()) | ||
.collect() | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
// Copyright (c) Mysten Labs, Inc. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any suggestions where this should live? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's only temporary, I don't think we should worry too hard about this -- but this doesn't seem like a bad place for it since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having said that, you may want to re-use the builder without the migration logic in the SDK? cc @patrickkuo |
||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
//! Utility for generating programmable transactions, either by specifying a command or for | ||
//! migrating legacy transactions | ||
|
||
use anyhow::Context; | ||
use indexmap::IndexSet; | ||
use move_core_types::{identifier::Identifier, language_storage::TypeTag}; | ||
use serde::Serialize; | ||
|
||
use crate::{ | ||
base_types::{ObjectID, ObjectRef, SuiAddress}, | ||
messages::{ | ||
Argument, CallArg, Command, MoveCall, MoveModulePublish, ObjectArg, Pay, PayAllSui, PaySui, | ||
ProgrammableMoveCall, ProgrammableTransaction, SingleTransactionKind, TransactionData, | ||
TransactionKind, TransferObject, TransferSui, | ||
}, | ||
}; | ||
|
||
pub fn migrate_transaction_data(mut m: TransactionData) -> anyhow::Result<TransactionData> { | ||
let mut builder = ProgrammableTransactionBuilder::new(); | ||
match m.kind { | ||
TransactionKind::Single(SingleTransactionKind::PaySui(PaySui { | ||
coins: _coins, | ||
recipients, | ||
amounts, | ||
})) => { | ||
builder.pay_sui(recipients, amounts)?; | ||
anyhow::bail!("blocked by gas smashing") | ||
} | ||
TransactionKind::Single(SingleTransactionKind::PayAllSui(PayAllSui { | ||
coins: _coins, | ||
recipient, | ||
})) => { | ||
builder.pay_all_sui(recipient); | ||
anyhow::bail!("blocked by gas smashing") | ||
} | ||
TransactionKind::Single(t) => builder.single_transaction(t)?, | ||
TransactionKind::Batch(ts) => { | ||
for t in ts { | ||
builder.single_transaction(t)? | ||
} | ||
} | ||
}; | ||
let pt = builder.finish(); | ||
m.kind = TransactionKind::Single(SingleTransactionKind::ProgrammableTransaction(pt)); | ||
Ok(m) | ||
} | ||
|
||
#[derive(Default)] | ||
pub struct ProgrammableTransactionBuilder { | ||
inputs: IndexSet<CallArg>, | ||
commands: Vec<Command>, | ||
} | ||
|
||
impl ProgrammableTransactionBuilder { | ||
pub fn new() -> Self { | ||
Self::default() | ||
} | ||
|
||
pub fn finish(self) -> ProgrammableTransaction { | ||
let Self { inputs, commands } = self; | ||
let inputs = inputs.into_iter().collect(); | ||
ProgrammableTransaction { inputs, commands } | ||
} | ||
|
||
pub fn pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> { | ||
Ok(self | ||
.input(CallArg::Pure( | ||
bcs::to_bytes(&value).context("Searlizing pure argument.")?, | ||
)) | ||
.unwrap()) | ||
} | ||
|
||
pub fn obj(&mut self, obj_arg: ObjectArg) -> Argument { | ||
self.input(CallArg::Object(obj_arg)).unwrap() | ||
} | ||
|
||
pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result<Argument> { | ||
match call_arg { | ||
call_arg @ (CallArg::Pure(_) | CallArg::Object(_)) => { | ||
Ok(Argument::Input(self.inputs.insert_full(call_arg).0 as u16)) | ||
} | ||
CallArg::ObjVec(objs) if objs.is_empty() => { | ||
anyhow::bail!( | ||
"Empty ObjVec is not supported in programmable transactions \ | ||
without a type annotation" | ||
) | ||
} | ||
CallArg::ObjVec(objs) => Ok(self.make_obj_vec(objs)), | ||
} | ||
} | ||
|
||
pub fn make_obj_vec(&mut self, objs: impl IntoIterator<Item = ObjectArg>) -> Argument { | ||
let make_vec_args = objs.into_iter().map(|obj| self.obj(obj)).collect(); | ||
self.command(Command::MakeMoveVec(None, make_vec_args)) | ||
} | ||
|
||
pub fn command(&mut self, command: Command) -> Argument { | ||
let i = self.commands.len(); | ||
self.commands.push(command); | ||
Argument::Result(i as u16) | ||
} | ||
|
||
pub fn single_transaction(&mut self, t: SingleTransactionKind) -> anyhow::Result<()> { | ||
match t { | ||
SingleTransactionKind::ProgrammableTransaction(_) => anyhow::bail!( | ||
"ProgrammableTransaction are not supported in ProgrammableTransactionBuilder" | ||
), | ||
SingleTransactionKind::TransferObject(TransferObject { | ||
recipient, | ||
object_ref, | ||
}) => self.transfer_object(recipient, object_ref), | ||
SingleTransactionKind::Publish(MoveModulePublish { modules }) => self.publish(modules), | ||
SingleTransactionKind::Call(MoveCall { | ||
package, | ||
module, | ||
function, | ||
type_arguments, | ||
arguments, | ||
}) => self.move_call(package, module, function, type_arguments, arguments)?, | ||
SingleTransactionKind::TransferSui(TransferSui { recipient, amount }) => { | ||
self.transfer_sui(recipient, amount) | ||
} | ||
SingleTransactionKind::Pay(Pay { | ||
coins, | ||
recipients, | ||
amounts, | ||
}) => self.pay(coins, recipients, amounts)?, | ||
SingleTransactionKind::PaySui(_) | SingleTransactionKind::PayAllSui(_) => { | ||
anyhow::bail!( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this an existing limitation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You cannot map PaySui into a SingleTransacitonKind, but only TransactionData because the coins need to be in the gas vector with gas smashing |
||
"PaySui and PayAllSui cannot be migrated as a single transaction kind, \ | ||
only as a full transaction" | ||
) | ||
} | ||
SingleTransactionKind::ChangeEpoch(_) | ||
| SingleTransactionKind::Genesis(_) | ||
| SingleTransactionKind::ConsensusCommitPrologue(_) => anyhow::bail!( | ||
"System transactions are not expressed with programmable transactions" | ||
), | ||
}; | ||
Ok(()) | ||
} | ||
|
||
/// Will fail to generate if given an empty ObjVec | ||
pub fn move_call( | ||
&mut self, | ||
package: ObjectID, | ||
module: Identifier, | ||
function: Identifier, | ||
type_arguments: Vec<TypeTag>, | ||
call_args: Vec<CallArg>, | ||
) -> anyhow::Result<()> { | ||
let arguments = call_args | ||
.into_iter() | ||
.map(|a| self.input(a)) | ||
.collect::<Result<_, _>>()?; | ||
self.command(Command::MoveCall(Box::new(ProgrammableMoveCall { | ||
package, | ||
module, | ||
function, | ||
type_arguments, | ||
arguments, | ||
}))); | ||
Ok(()) | ||
} | ||
|
||
pub fn publish(&mut self, modules: Vec<Vec<u8>>) { | ||
self.commands.push(Command::Publish(modules)) | ||
} | ||
|
||
pub fn transfer_object(&mut self, recipient: SuiAddress, object_ref: ObjectRef) { | ||
let rec_arg = self.pure(recipient).unwrap(); | ||
let obj_arg = self.obj(ObjectArg::ImmOrOwnedObject(object_ref)); | ||
self.commands | ||
.push(Command::TransferObjects(vec![obj_arg], rec_arg)); | ||
} | ||
|
||
pub fn transfer_sui(&mut self, recipient: SuiAddress, amount: Option<u64>) { | ||
let rec_arg = self.pure(recipient).unwrap(); | ||
let coin_arg = if let Some(amount) = amount { | ||
let amt_arg = self.pure(amount).unwrap(); | ||
self.command(Command::SplitCoin(Argument::GasCoin, amt_arg)) | ||
} else { | ||
Argument::GasCoin | ||
}; | ||
self.command(Command::TransferObjects(vec![coin_arg], rec_arg)); | ||
} | ||
|
||
pub fn pay_all_sui(&mut self, recipient: SuiAddress) { | ||
let rec_arg = self.pure(recipient).unwrap(); | ||
self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg)); | ||
} | ||
|
||
/// Will fail to generate if recipients and amounts do not have the same lengths | ||
pub fn pay_sui( | ||
&mut self, | ||
recipients: Vec<SuiAddress>, | ||
amounts: Vec<u64>, | ||
) -> anyhow::Result<()> { | ||
self.pay_impl(recipients, amounts, Argument::GasCoin) | ||
} | ||
|
||
/// Will fail to generate if recipients and amounts do not have the same lengths. | ||
/// Or if coins is empty | ||
pub fn pay( | ||
&mut self, | ||
coins: Vec<ObjectRef>, | ||
recipients: Vec<SuiAddress>, | ||
amounts: Vec<u64>, | ||
) -> anyhow::Result<()> { | ||
let mut coins = coins.into_iter(); | ||
let Some(coin) = coins.next() | ||
else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Funky formatting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the RFC formatting for let else There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the RFC formatting for this let-else would be let Some(coin) = coins.next() else {
anyhow::bail!("coins vector is empty");
}; According to this section There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I've got to say, the rules as written in the RFC are super ambiguous in general, and I for one welcome our |
||
anyhow::bail!("coins vector is empty"); | ||
}; | ||
let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin)); | ||
let merge_args: Vec<_> = coins | ||
.map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c))) | ||
.collect(); | ||
if !merge_args.is_empty() { | ||
self.command(Command::MergeCoins(coin_arg, merge_args)); | ||
} | ||
self.pay_impl(recipients, amounts, coin_arg) | ||
} | ||
|
||
fn pay_impl( | ||
&mut self, | ||
recipients: Vec<SuiAddress>, | ||
amounts: Vec<u64>, | ||
coin: Argument, | ||
) -> anyhow::Result<()> { | ||
if recipients.len() != amounts.len() { | ||
anyhow::bail!( | ||
"Recipients and amounts mismatch. Got {} recipients but {} amounts", | ||
recipients.len(), | ||
amounts.len() | ||
) | ||
} | ||
for (recipient, amount) in recipients.into_iter().zip(amounts) { | ||
let rec_arg = self.pure(recipient).unwrap(); | ||
let amt_arg = self.pure(amount).unwrap(); | ||
let coin_arg = self.command(Command::SplitCoin(coin, amt_arg)); | ||
self.command(Command::TransferObjects(vec![coin_arg], rec_arg)); | ||
} | ||
Ok(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should implement all dynamic dispatch like this