diff --git a/crates/sui-core/src/gateway_state.rs b/crates/sui-core/src/gateway_state.rs index 6824e8a0f865d..ed19558e3021c 100644 --- a/crates/sui-core/src/gateway_state.rs +++ b/crates/sui-core/src/gateway_state.rs @@ -1629,6 +1629,7 @@ where Ok(data) } + // TODO: consolidate this with Pay transactions async fn split_coin( &self, signer: SuiAddress, @@ -1660,6 +1661,7 @@ where Ok(data) } + // TODO: consolidate this with Pay transactions async fn split_coin_equal( &self, signer: SuiAddress, @@ -1691,6 +1693,7 @@ where Ok(data) } + // TODO: consolidate this with Pay transactions async fn merge_coins( &self, signer: SuiAddress, diff --git a/crates/sui-json-rpc/src/api.rs b/crates/sui-json-rpc/src/api.rs index 8df12d3abc9f5..3d3f3974d9861 100644 --- a/crates/sui-json-rpc/src/api.rs +++ b/crates/sui-json-rpc/src/api.rs @@ -238,6 +238,10 @@ pub trait RpcTransactionBuilder { amount: Option, ) -> RpcResult; + /// Send Coin to a list of addresses, where `T` can be any coin type, following a list of amounts, + /// The object specified in the `gas` field will be used to pay the gas fee for the transaction. + /// The gas object can not appear in `input_coins`. If the gas object is not specified, the RPC server + /// will auto-select one. #[method(name = "pay")] async fn pay( &self, @@ -255,6 +259,50 @@ pub trait RpcTransactionBuilder { gas_budget: u64, ) -> RpcResult; + /// Send SUI coins to a list of addresses, following a list of amounts. + /// This is for SUI coin only and does not require a separate gas coin object. + /// Specifically, what pay_sui does are: + /// 1. debit each input_coin to create new coin following the order of + /// amounts and assign it to the corresponding recipient. + /// 2. accumulate all residual SUI from input coins left and deposit all SUI to the first + /// input coin, then use the first input coin as the gas coin object. + /// 3. the balance of the first input coin after tx is sum(input_coins) - sum(amounts) - actual_gas_cost + /// 4. all other input coints other than the first one are deleted. + #[method(name = "paySui")] + async fn pay_sui( + &self, + /// the transaction signer's Sui address + signer: SuiAddress, + /// the Sui coins to be used in this transaction, including the coin for gas payment. + input_coins: Vec, + /// the recipients' addresses, the length of this vector must be the same as amounts. + recipients: Vec, + /// the amounts to be transferred to recipients, following the same order + amounts: Vec, + /// the gas budget, the transaction will fail if the gas cost exceed the budget + gas_budget: u64, + ) -> RpcResult; + + /// Send all SUI coins to one recipient. + /// This is for SUI coin only and does not require a separate gas coin object. + /// Specifically, what pay_all_sui does are: + /// 1. accumulate all SUI from input coins and deposit all SUI to the first input coin + /// 2. transfer the updated first coin to the recipient and also use this first coin as gas coin object. + /// 3. the balance of the first input coin after tx is sum(input_coins) - actual_gas_cost. + /// 4. all other input coins other than the first are deleted. + #[method(name = "payAllSui")] + async fn pay_all_sui( + &self, + /// the transaction signer's Sui address + signer: SuiAddress, + /// the Sui coins to be used in this transaction, including the coin for gas payment. + input_coins: Vec, + /// the recipient address, + recipient: SuiAddress, + /// the gas budget, the transaction will fail if the gas cost exceed the budget + gas_budget: u64, + ) -> RpcResult; + /// Create an unsigned transaction to execute a Move call on the network, by calling the specified function in the module of a given package. #[method(name = "moveCall")] async fn move_call( diff --git a/crates/sui-json-rpc/src/gateway_api.rs b/crates/sui-json-rpc/src/gateway_api.rs index 9ea79b33be508..958d906e1a934 100644 --- a/crates/sui-json-rpc/src/gateway_api.rs +++ b/crates/sui-json-rpc/src/gateway_api.rs @@ -219,6 +219,35 @@ impl RpcTransactionBuilderServer for TransactionBuilderImpl { Ok(TransactionBytes::from_data(data)?) } + async fn pay_sui( + &self, + signer: SuiAddress, + input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas_budget: u64, + ) -> RpcResult { + let data = self + .client + .pay_sui(signer, input_coins, recipients, amounts, gas_budget) + .await?; + Ok(TransactionBytes::from_data(data)?) + } + + async fn pay_all_sui( + &self, + signer: SuiAddress, + input_coins: Vec, + recipient: SuiAddress, + gas_budget: u64, + ) -> RpcResult { + let data = self + .client + .pay_all_sui(signer, input_coins, recipient, gas_budget) + .await?; + Ok(TransactionBytes::from_data(data)?) + } + async fn publish( &self, sender: SuiAddress, diff --git a/crates/sui-json-rpc/src/transaction_builder_api.rs b/crates/sui-json-rpc/src/transaction_builder_api.rs index 4cf8cf156f4d5..fd8f85ed26f73 100644 --- a/crates/sui-json-rpc/src/transaction_builder_api.rs +++ b/crates/sui-json-rpc/src/transaction_builder_api.rs @@ -112,6 +112,35 @@ impl RpcTransactionBuilderServer for FullNodeTransactionBuilderApi { Ok(TransactionBytes::from_data(data)?) } + async fn pay_sui( + &self, + signer: SuiAddress, + input_coins: Vec, + recipients: Vec, + amounts: Vec, + gas_budget: u64, + ) -> RpcResult { + let data = self + .builder + .pay_sui(signer, input_coins, recipients, amounts, gas_budget) + .await?; + Ok(TransactionBytes::from_data(data)?) + } + + async fn pay_all_sui( + &self, + signer: SuiAddress, + input_coins: Vec, + recipient: SuiAddress, + gas_budget: u64, + ) -> RpcResult { + let data = self + .builder + .pay_all_sui(signer, input_coins, recipient, gas_budget) + .await?; + Ok(TransactionBytes::from_data(data)?) + } + async fn publish( &self, sender: SuiAddress, diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index 1038884f7a260..3f3220dd1479a 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -2039,6 +2039,7 @@ "name": "Transaction Builder API" } ], + "description": "Send Coin to a list of addresses, where `T` can be any coin type, following a list of amounts, The object specified in the `gas` field will be used to pay the gas fee for the transaction. The gas object can not appear in `input_coins`. If the gas object is not specified, the RPC server will auto-select one.", "params": [ { "name": "signer", @@ -2109,6 +2110,132 @@ } } }, + { + "name": "sui_payAllSui", + "tags": [ + { + "name": "Transaction Builder API" + } + ], + "description": "Send all SUI coins to one recipient. This is for SUI coin only and does not require a separate gas coin object. Specifically, what pay_all_sui does are: 1. accumulate all SUI from input coins and deposit all SUI to the first input coin 2. transfer the updated first coin to the recipient and also use this first coin as gas coin object. 3. the balance of the first input coin after tx is sum(input_coins) - actual_gas_cost. 4. all other input coins other than the first are deleted.", + "params": [ + { + "name": "signer", + "description": "the transaction signer's Sui address", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "input_coins", + "description": "the Sui coins to be used in this transaction, including the coin for gas payment.", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectID" + } + } + }, + { + "name": "recipient", + "description": "the recipient address,", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "gas_budget", + "description": "the gas budget, the transaction will fail if the gas cost exceed the budget", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, + { + "name": "sui_paySui", + "tags": [ + { + "name": "Transaction Builder API" + } + ], + "description": "Send SUI coins to a list of addresses, following a list of amounts. This is for SUI coin only and does not require a separate gas coin object. Specifically, what pay_sui does are: 1. debit each input_coin to create new coin following the order of amounts and assign it to the corresponding recipient. 2. accumulate all residual SUI from input coins left and deposit all SUI to the first input coin, then use the first input coin as the gas coin object. 3. the balance of the first input coin after tx is sum(input_coins) - sum(amounts) - actual_gas_cost 4. all other input coints other than the first one are deleted.", + "params": [ + { + "name": "signer", + "description": "the transaction signer's Sui address", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiAddress" + } + }, + { + "name": "input_coins", + "description": "the Sui coins to be used in this transaction, including the coin for gas payment.", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectID" + } + } + }, + { + "name": "recipients", + "description": "the recipients' addresses, the length of this vector must be the same as amounts.", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SuiAddress" + } + } + }, + { + "name": "amounts", + "description": "the amounts to be transferred to recipients, following the same order", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "gas_budget", + "description": "the gas budget, the transaction will fail if the gas cost exceed the budget", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ], + "result": { + "name": "TransactionBytes", + "required": true, + "schema": { + "$ref": "#/components/schemas/TransactionBytes" + } + } + }, { "name": "sui_publish", "tags": [ diff --git a/crates/sui-transaction-builder/src/lib.rs b/crates/sui-transaction-builder/src/lib.rs index 7808886e7eb50..ea28432d3ff04 100644 --- a/crates/sui-transaction-builder/src/lib.rs +++ b/crates/sui-transaction-builder/src/lib.rs @@ -364,6 +364,7 @@ impl TransactionBuilder { )) } + // TODO: consolidate this with Pay transactions pub async fn split_coin( &self, signer: SuiAddress, @@ -395,6 +396,7 @@ impl TransactionBuilder { )) } + // TODO: consolidate this with Pay transactions pub async fn split_coin_equal( &self, signer: SuiAddress, @@ -426,6 +428,7 @@ impl TransactionBuilder { )) } + // TODO: consolidate this with Pay transactions pub async fn merge_coins( &self, signer: SuiAddress, diff --git a/crates/sui/src/client_commands.rs b/crates/sui/src/client_commands.rs index 1bb141adaf0a6..2903c2739e1b7 100644 --- a/crates/sui/src/client_commands.rs +++ b/crates/sui/src/client_commands.rs @@ -185,7 +185,7 @@ pub enum SuiClientCommands { #[clap(long)] amount: Option, }, - /// Pay SUI to recipients following specified amounts, with input coins. + /// Pay coins to recipients following specified amounts, with input coins. /// Length of recipients must be the same as that of amounts. #[clap(name = "pay")] Pay { @@ -197,7 +197,7 @@ pub enum SuiClientCommands { #[clap(long, multiple_occurrences = false, multiple_values = true)] recipients: Vec, - /// The amounts to be transferred, following the order of recipients. + /// The amounts to be paid, following the order of recipients. #[clap(long, multiple_occurrences = false, multiple_values = true)] amounts: Vec, @@ -206,10 +206,50 @@ pub enum SuiClientCommands { #[clap(long)] gas: Option, - /// Gas budget for this transfer + /// Gas budget for this transaction + #[clap(long)] + gas_budget: u64, + }, + + /// Pay SUI coins to recipients following following specified amounts, with input coins. + /// Length of recipients must be the same as that of amounts. + /// The input coins also include the coin for gas payment, so no extra gas coin is required. + #[clap(name = "pay_sui")] + PaySui { + /// The input coins to be used for pay recipients, including the gas coin. + #[clap(long, multiple_occurrences = false, multiple_values = true)] + input_coins: Vec, + + /// The recipient addresses, must be of same length as amounts. + #[clap(long, multiple_occurrences = false, multiple_values = true)] + recipients: Vec, + + /// The amounts to be paid, following the order of recipients. + #[clap(long, multiple_occurrences = false, multiple_values = true)] + amounts: Vec, + + /// Gas budget for this transaction + #[clap(long)] + gas_budget: u64, + }, + + /// Pay all residual SUI coins to the recipient with input coins, after deducting the gas cost. + /// The input coins also include the coin for gas payment, so no extra gas coin is required. + #[clap(name = "pay_all_sui")] + PayAllSui { + /// The input coins to be used for pay recipients, including the gas coin. + #[clap(long, multiple_occurrences = false, multiple_values = true)] + input_coins: Vec, + + /// The recipient address. + #[clap(long, multiple_occurrences = false)] + recipient: SuiAddress, + + /// Gas budget for this transaction #[clap(long)] gas_budget: u64, }, + /// Synchronize client state with authorities. #[clap(name = "sync")] SyncClientState { @@ -496,6 +536,82 @@ impl SuiClientCommands { SuiClientCommandResult::Pay(cert, effects) } + SuiClientCommands::PaySui { + input_coins, + recipients, + amounts, + gas_budget, + } => { + ensure!( + !input_coins.is_empty(), + "PaySui transaction requires a non-empty list of input coins" + ); + ensure!( + !recipients.is_empty(), + "PaySui transaction requires a non-empty list of recipient addresses" + ); + ensure!( + recipients.len() == amounts.len(), + format!( + "Found {:?} recipient addresses, but {:?} recipient amounts", + recipients.len(), + amounts.len() + ), + ); + let signer = context.get_object_owner(&input_coins[0]).await?; + let data = context + .client + .transaction_builder() + .pay_sui(signer, input_coins, recipients, amounts, gas_budget) + .await?; + let signature = context.config.keystore.sign(&signer, &data.to_bytes())?; + let response = context + .execute_transaction(Transaction::new(data, signature)) + .await?; + + let cert = response.certificate; + let effects = response.effects; + if matches!(effects.status, SuiExecutionStatus::Failure { .. }) { + return Err(anyhow!( + "Error executing PaySui transaction: {:#?}", + effects.status + )); + } + SuiClientCommandResult::PaySui(cert, effects) + } + + SuiClientCommands::PayAllSui { + input_coins, + recipient, + gas_budget, + } => { + ensure!( + !input_coins.is_empty(), + "PayAllSui transaction requires a non-empty list of input coins" + ); + let signer = context.get_object_owner(&input_coins[0]).await?; + let data = context + .client + .transaction_builder() + .pay_all_sui(signer, input_coins, recipient, gas_budget) + .await?; + + let signature = context.config.keystore.sign(&signer, &data.to_bytes())?; + let response = context + .execute_transaction(Transaction::new(data, signature)) + .await?; + + let cert = response.certificate; + let effects = response.effects; + if matches!(effects.status, SuiExecutionStatus::Failure { .. }) { + return Err(anyhow!( + "Error executing PayAllSui transaction: {:#?}", + effects.status + )); + } + SuiClientCommandResult::PayAllSui(cert, effects) + } + SuiClientCommands::Addresses => { SuiClientCommandResult::Addresses(context.config.keystore.addresses()) } @@ -918,6 +1034,12 @@ impl Display for SuiClientCommandResult { SuiClientCommandResult::Pay(cert, effects) => { write!(writer, "{}", write_cert_and_effects(cert, effects)?)?; } + SuiClientCommandResult::PaySui(cert, effects) => { + write!(writer, "{}", write_cert_and_effects(cert, effects)?)?; + } + SuiClientCommandResult::PayAllSui(cert, effects) => { + write!(writer, "{}", write_cert_and_effects(cert, effects)?)?; + } SuiClientCommandResult::Addresses(addresses) => { writeln!(writer, "Showing {} results.", addresses.len())?; for address in addresses { @@ -1137,6 +1259,8 @@ pub enum SuiClientCommandResult { ), TransferSui(SuiCertifiedTransaction, SuiTransactionEffects), Pay(SuiCertifiedTransaction, SuiTransactionEffects), + PaySui(SuiCertifiedTransaction, SuiTransactionEffects), + PayAllSui(SuiCertifiedTransaction, SuiTransactionEffects), Addresses(Vec), Objects(Vec), SyncClientState,