diff --git a/Cargo.toml b/Cargo.toml index c79b7db36b..c10e696d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,3 +122,18 @@ fuels-macros = { version = "0.71.0", path = "./packages/fuels-macros", default-f fuels-programs = { version = "0.71.0", path = "./packages/fuels-programs", default-features = false } fuels-test-helpers = { version = "0.71.0", path = "./packages/fuels-test-helpers", default-features = false } versions-replacer = { version = "0.71.0", path = "./scripts/versions-replacer", default-features = false } + +[patch.crates-io] +fuel-core-client = { path = "../fuel-core/crates/client"} +fuel-core-types = { path = "../fuel-core/crates/types"} +fuel-core-chain-config = { path = "../fuel-core/crates/chain-config"} +fuel-core-poa = { path = "../fuel-core/crates/services/consensus_module/poa"} +fuel-core-services = { path = "../fuel-core/crates/services"} + +fuel-asm = { path = "../fuel-vm/fuel-asm" } +fuel-crypto = { path = "../fuel-vm/fuel-crypto" } +fuel-merkle = { path = "../fuel-vm/fuel-merkle" } +fuel-storage = { path = "../fuel-vm/fuel-storage" } +fuel-tx = { path = "../fuel-vm/fuel-tx" } +fuel-types = { path = "../fuel-vm/fuel-types" } +fuel-vm = { path = "../fuel-vm/fuel-vm" } diff --git a/e2e/Forc.toml b/e2e/Forc.toml index 357aad5f8c..629bf01643 100644 --- a/e2e/Forc.toml +++ b/e2e/Forc.toml @@ -38,6 +38,7 @@ members = [ 'sway/logs/script_logs', 'sway/logs/script_with_contract_logs', 'sway/predicates/basic_predicate', + 'sway/predicates/data_coins', 'sway/predicates/predicate_blobs', 'sway/predicates/predicate_configurables', 'sway/predicates/predicate_witnesses', @@ -118,3 +119,4 @@ members = [ 'sway/types/scripts/script_u256', 'sway/types/scripts/script_vectors', ] + diff --git a/e2e/sway/predicates/data_coins/Forc.toml b/e2e/sway/predicates/data_coins/Forc.toml new file mode 100644 index 0000000000..80cead06f7 --- /dev/null +++ b/e2e/sway/predicates/data_coins/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "data_coins" + +[dependencies] +std = { path = "../../../../../sway/sway-lib-std" } diff --git a/e2e/sway/predicates/data_coins/src/main.sw b/e2e/sway/predicates/data_coins/src/main.sw new file mode 100644 index 0000000000..0c0bc1ea67 --- /dev/null +++ b/e2e/sway/predicates/data_coins/src/main.sw @@ -0,0 +1,61 @@ +predicate; + +use std::{inputs::*, outputs::*}; + +struct DataCoinConfig { + num_participants: u64, +} + +fn main(_unused: DataCoinConfig) -> bool { + let input_count = input_count(); + let output_count = output_count(); + match (input_count, output_count) { + // transfer some amount from predicate coin + // to data coin and return change to predicate + (1, 2) => { //(predicate_coin, data_coin and predicate change) + // Check: + // - predicate coin amount is bigger than data_coin amount + // - data_coin data can be decoded into `DataCoinConfig` + // - `DataCoinConfig` `num_praticipants == 1` (initial config creation) + let predicate_coin_amount = input_amount(0).unwrap(); + let data_coin_amount = output_amount(0).unwrap(); + + if predicate_coin_amount < data_coin_amount { + return false; + } + + let datacoin_config = output_data_coin_data::(0).unwrap(); + + if datacoin_config.num_participants != 1 { + return false; + } + + return true; + }, + // update input data coin with new amount from predicate coin and incremented `num_participants` + (2, 2) => { //(data_coin and predicate_coin, data_coin and predicate change) + // Check: + // - predicate coin amount is bigger than (output_data_coin_amount - input_data_coin_amount) + // - data_coin data can be decoded into `DataCoinConfig` + // - output `DataCoinConfig` `num_praticipants` is incremented by 1 + let input_data_coin_amount = input_amount(0).unwrap(); + let predicate_coin_amount = input_amount(1).unwrap(); + let output_data_coin_amount = output_amount(0).unwrap(); + + if predicate_coin_amount < (output_data_coin_amount - input_data_coin_amount) + { + return false; + } + + let input_num_participants = input_data_coin_data::(0).unwrap().num_participants; + let output_num_participants = output_data_coin_data::(0).unwrap().num_participants; + + if (output_num_participants - input_num_participants) != 1 { + return false; + } + + return true; + }, + _ => return false, + } +} diff --git a/e2e/tests/predicates.rs b/e2e/tests/predicates.rs index ac6d697532..88e823b5f2 100644 --- a/e2e/tests/predicates.rs +++ b/e2e/tests/predicates.rs @@ -8,7 +8,13 @@ use fuels::{ }, prelude::*, programs::executable::Executable, - types::{coin::Coin, coin_type::CoinType, input::Input, message::Message, output::Output}, + types::{ + coin::{Coin, DataCoin}, + coin_type::CoinType, + input::Input, + message::Message, + output::Output, + }, }; use rand::thread_rng; @@ -113,6 +119,174 @@ async fn setup_predicate_test( )) } +#[tokio::test] +async fn data_coins() -> Result<()> { + abigen!(Predicate( + name = "MyPredicate", + abi = "e2e/sway/predicates/data_coins/out/release/data_coins-abi.json" + )); + + let predicate = Predicate::load_from("sway/predicates/data_coins/out/release/data_coins.bin")?; + + let num_coins = 2; + let num_messages = 0; + let amount = 164; + + let signer = PrivateKeySigner::random(&mut thread_rng()); + + let (coins, messages, asset_id) = + get_test_coins_and_messages(signer.address(), num_coins, num_messages, amount, 0); + + let encoded_data = ABIEncoder::default().encode(&[DataCoinConfig { + num_participants: 5, + } + .into_token()])?; + + let amount_data_coin = 16; + let mut data_coins = vec![setup_single_data_coin( + predicate.address(), + asset_id, + amount_data_coin, + encoded_data, + )]; + + let amount_predicate_coin = 32; + let predicate_coins = + setup_single_asset_coins(predicate.address(), asset_id, 2, amount_predicate_coin); + let [predicate_coin_1, predicate_coin_2] = predicate_coins.clone().try_into().unwrap(); + + let provider = setup_test_provider2( + [coins, predicate_coins].concat(), + data_coins.clone(), + messages, + None, + None, + ) + .await?; + let chain_id = provider.consensus_parameters().await?.chain_id(); + + let wallet = Wallet::new(signer, provider.clone()); + + let predicate = predicate.with_provider(provider.clone()); + + { + let predicate_input_coin = Input::resource_predicate( + CoinType::Coin(predicate_coin_1), + predicate.code().to_vec(), + predicate.data().to_vec(), + ); + + let data_coin_data = DataCoinConfig { + num_participants: 1, + }; + let encoded_data = ABIEncoder::default().encode(&[data_coin_data.into_token()])?; + + let outputs = vec![ + Output::data_coin( + predicate.address().into(), + amount_data_coin, + asset_id, + encoded_data, + ), + Output::change(predicate.address().into(), 0, asset_id), + ]; + + let mut tb = ScriptTransactionBuilder::prepare_transfer( + vec![predicate_input_coin], + outputs, + TxPolicies::default(), + ); + tb.add_signer(wallet.signer().clone())?; + + let tx = tb.build(&provider).await?; + + let tx_id = tx.id(chain_id); + + dbg!(&tx.inputs()); + + let tx_status = provider.send_transaction_and_await_commit(tx).await?; + + dbg!(&tx_status); + + let tx_from_client = match provider + .get_transaction_by_id(&tx_id) + .await? + .unwrap() + .transaction + { + TransactionType::Script(script) => script, + _ => panic!("nani"), + }; + + dbg!(&tx_from_client.outputs()); + } + // data coin input data coin output + { + dbg!("2"); + let data_coin = data_coins.pop().unwrap(); + let data_coin_input = Input::resource_predicate( + CoinType::DataCoin(data_coin), + predicate.code().to_vec(), + predicate.data().to_vec(), + ); + let predicate_input_coin = Input::resource_predicate( + CoinType::Coin(predicate_coin_2), + predicate.code().to_vec(), + predicate.data().to_vec(), + ); + + let output_data_coin_data = DataCoinConfig { + num_participants: 6, + }; + let output_encoded_data = + ABIEncoder::default().encode(&[output_data_coin_data.into_token()])?; + + let outputs = vec![ + Output::data_coin( + predicate.address().into(), + amount_data_coin * 2, + asset_id, + output_encoded_data, + ), + Output::change(predicate.address().into(), 0, asset_id), + ]; + + let mut tb = ScriptTransactionBuilder::prepare_transfer( + vec![data_coin_input, predicate_input_coin], + outputs, + TxPolicies::default(), + ); + tb.add_signer(wallet.signer().clone())?; + + let tx = tb.build(&provider).await.unwrap(); + + let tx_id = tx.id(chain_id); + + dbg!(&tx.inputs()); + + let tx_status = provider + .send_transaction_and_await_commit(tx) + .await + .unwrap(); + + dbg!(&tx_status); + + let tx_from_client = match provider + .get_transaction_by_id(&tx_id) + .await? + .unwrap() + .transaction + { + TransactionType::Script(script) => script, + _ => panic!("nani"), + }; + + dbg!(&tx_from_client.outputs()); + } + + Ok(()) +} + #[tokio::test] async fn transfer_coins_and_messages_to_predicate() -> Result<()> { let num_coins = 16; diff --git a/packages/fuels-core/src/types/transaction_builders.rs b/packages/fuels-core/src/types/transaction_builders.rs index 1465d4f012..753881818e 100644 --- a/packages/fuels-core/src/types/transaction_builders.rs +++ b/packages/fuels-core/src/types/transaction_builders.rs @@ -464,6 +464,8 @@ macro_rules! impl_tx_builder_trait { pub(crate) use impl_tx_builder_trait; +use super::coin::DataCoin; + pub(crate) fn estimate_max_fee_w_tolerance( tx: T, tolerance: f32, @@ -1374,6 +1376,20 @@ fn resolve_signed_resource( create_coin_input(coin, num_witnesses + *witness_idx_offset as u16) }) } + CoinType::DataCoin(coin) => { + let owner = &coin.owner; + + unresolved_witness_indexes + .owner_to_idx_offset + .get(owner) + .ok_or(error_transaction!( + Builder, + "signature missing for coin with owner: `{owner:?}`" + )) + .map(|witness_idx_offset| { + create_data_coin_input(coin, num_witnesses + *witness_idx_offset as u16) + }) + } CoinType::Message(message) => { let recipient = &message.recipient; @@ -1401,7 +1417,8 @@ fn resolve_predicate_resource( data: Vec, ) -> Result { match resource { - CoinType::Coin(coin) => Ok(create_coin_predicate(coin.asset_id, coin, code, data)), + CoinType::Coin(coin) => Ok(create_coin_predicate(coin, code, data)), + CoinType::DataCoin(coin) => Ok(create_data_coin_predicate(coin, code, data)), CoinType::Message(message) => Ok(create_coin_message_predicate(message, code, data)), CoinType::Unknown => Err(error_transaction!( Builder, @@ -1421,6 +1438,18 @@ pub fn create_coin_input(coin: Coin, witness_index: u16) -> FuelInput { ) } +pub fn create_data_coin_input(coin: DataCoin, witness_index: u16) -> FuelInput { + FuelInput::data_coin_signed( + coin.utxo_id, + coin.owner.into(), + coin.amount, + coin.asset_id, + TxPointer::default(), + witness_index, + coin.data, + ) +} + pub fn create_coin_message_input(message: Message, witness_index: u16) -> FuelInput { if message.data.is_empty() { FuelInput::message_coin_signed( @@ -1442,21 +1471,34 @@ pub fn create_coin_message_input(message: Message, witness_index: u16) -> FuelIn } } -pub fn create_coin_predicate( - asset_id: AssetId, - coin: Coin, +pub fn create_coin_predicate(coin: Coin, code: Vec, predicate_data: Vec) -> FuelInput { + FuelInput::coin_predicate( + coin.utxo_id, + coin.owner.into(), + coin.amount, + coin.asset_id, + TxPointer::default(), + 0u64, + code, + predicate_data, + ) +} + +pub fn create_data_coin_predicate( + coin: DataCoin, code: Vec, predicate_data: Vec, ) -> FuelInput { - FuelInput::coin_predicate( + FuelInput::data_coin_predicate( coin.utxo_id, coin.owner.into(), coin.amount, - asset_id, + coin.asset_id, TxPointer::default(), 0u64, code, predicate_data, + coin.data, ) } diff --git a/packages/fuels-core/src/types/wrappers/coin.rs b/packages/fuels-core/src/types/wrappers/coin.rs index c1daeed1ae..259be1ae63 100644 --- a/packages/fuels-core/src/types/wrappers/coin.rs +++ b/packages/fuels-core/src/types/wrappers/coin.rs @@ -1,6 +1,6 @@ #![cfg(feature = "std")] -use fuel_core_chain_config::CoinConfig; +use fuel_core_chain_config::{CoinConfig, ConfigCoin, ConfigDataCoin}; use fuel_core_client::client::types::{ coins::Coin as ClientCoin, primitives::{AssetId, UtxoId}, @@ -40,7 +40,7 @@ impl From for Coin { impl From for CoinConfig { fn from(coin: Coin) -> CoinConfig { - Self { + Self::Coin(ConfigCoin { tx_id: *coin.utxo_id.tx_id(), output_index: coin.utxo_id.output_index(), tx_pointer_block_height: coin.block_created.into(), @@ -48,6 +48,45 @@ impl From for CoinConfig { owner: coin.owner.into(), amount: coin.amount, asset_id: coin.asset_id, - } + }) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct DataCoin { + pub amount: u64, + pub block_created: u32, + pub asset_id: AssetId, + pub utxo_id: UtxoId, + pub owner: Bech32Address, + pub status: CoinStatus, + pub data: Vec, +} +// TODO: add this when fuel-core-client is updated +// impl From for Coin { +// fn from(coin: ClientCoin) -> Self { +// Self { +// amount: coin.amount, +// block_created: coin.block_created, +// asset_id: coin.asset_id, +// utxo_id: coin.utxo_id, +// owner: Bech32Address::from(coin.owner), +// status: CoinStatus::Unspent, +// } +// } +// } + +impl From for CoinConfig { + fn from(coin: DataCoin) -> CoinConfig { + Self::DataCoin(ConfigDataCoin { + tx_id: *coin.utxo_id.tx_id(), + output_index: coin.utxo_id.output_index(), + tx_pointer_block_height: coin.block_created.into(), + tx_pointer_tx_idx: Default::default(), + owner: coin.owner.into(), + amount: coin.amount, + asset_id: coin.asset_id, + data: coin.data, + }) } } diff --git a/packages/fuels-core/src/types/wrappers/coin_type.rs b/packages/fuels-core/src/types/wrappers/coin_type.rs index 6fad1be8f6..cd94efa29a 100644 --- a/packages/fuels-core/src/types/wrappers/coin_type.rs +++ b/packages/fuels-core/src/types/wrappers/coin_type.rs @@ -6,9 +6,12 @@ use crate::types::{ AssetId, bech32::Bech32Address, coin::Coin, coin_type_id::CoinTypeId, message::Message, }; +use super::coin::DataCoin; + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CoinType { Coin(Coin), + DataCoin(DataCoin), Message(Message), Unknown, } @@ -27,6 +30,7 @@ impl CoinType { pub fn id(&self) -> Option { match self { CoinType::Coin(coin) => Some(CoinTypeId::UtxoId(coin.utxo_id)), + CoinType::DataCoin(coin) => Some(CoinTypeId::UtxoId(coin.utxo_id)), CoinType::Message(message) => Some(CoinTypeId::Nonce(message.nonce)), CoinType::Unknown => None, } @@ -35,6 +39,7 @@ impl CoinType { pub fn amount(&self) -> u64 { match self { CoinType::Coin(coin) => coin.amount, + CoinType::DataCoin(coin) => coin.amount, CoinType::Message(message) => message.amount, CoinType::Unknown => 0, } @@ -43,6 +48,7 @@ impl CoinType { pub fn coin_asset_id(&self) -> Option { match self { CoinType::Coin(coin) => Some(coin.asset_id), + CoinType::DataCoin(coin) => Some(coin.asset_id), CoinType::Message(_) => None, CoinType::Unknown => None, } @@ -51,6 +57,7 @@ impl CoinType { pub fn asset_id(&self, base_asset_id: AssetId) -> Option { match self { CoinType::Coin(coin) => Some(coin.asset_id), + CoinType::DataCoin(coin) => Some(coin.asset_id), CoinType::Message(_) => Some(base_asset_id), CoinType::Unknown => None, } @@ -59,6 +66,7 @@ impl CoinType { pub fn owner(&self) -> Option<&Bech32Address> { match self { CoinType::Coin(coin) => Some(&coin.owner), + CoinType::DataCoin(coin) => Some(&coin.owner), CoinType::Message(message) => Some(&message.recipient), CoinType::Unknown => None, } diff --git a/packages/fuels-core/src/types/wrappers/transaction.rs b/packages/fuels-core/src/types/wrappers/transaction.rs index bd472c9320..454ce40578 100644 --- a/packages/fuels-core/src/types/wrappers/transaction.rs +++ b/packages/fuels-core/src/types/wrappers/transaction.rs @@ -11,7 +11,7 @@ use fuel_tx::{ ScriptData, ScriptGasLimit, WitnessLimit, Witnesses, }, input::{ - coin::{CoinPredicate, CoinSigned}, + coin::{CoinPredicate, CoinSigned, DataCoinPredicate, DataCoinSigned}, message::{ MessageCoinPredicate, MessageCoinSigned, MessageDataPredicate, MessageDataSigned, }, @@ -320,7 +320,9 @@ fn extract_coin_type_id(input: &Input) -> Option { pub fn extract_owner_or_recipient(input: &Input) -> Option { let addr = match input { Input::CoinSigned(CoinSigned { owner, .. }) - | Input::CoinPredicate(CoinPredicate { owner, .. }) => Some(owner), + | Input::CoinPredicate(CoinPredicate { owner, .. }) + | Input::DataCoinSigned(DataCoinSigned { owner, .. }) + | Input::DataCoinPredicate(DataCoinPredicate { owner, .. }) => Some(owner), Input::MessageCoinSigned(MessageCoinSigned { recipient, .. }) | Input::MessageCoinPredicate(MessageCoinPredicate { recipient, .. }) | Input::MessageDataSigned(MessageDataSigned { recipient, .. }) diff --git a/packages/fuels-test-helpers/src/lib.rs b/packages/fuels-test-helpers/src/lib.rs index dc4350acdd..8ab8c27c11 100644 --- a/packages/fuels-test-helpers/src/lib.rs +++ b/packages/fuels-test-helpers/src/lib.rs @@ -8,13 +8,13 @@ use fuel_types::{AssetId, Nonce}; use fuels_accounts::provider::Provider; use fuels_core::types::{ bech32::Bech32Address, - coin::{Coin, CoinStatus}, + coin::{Coin, CoinStatus, DataCoin}, errors::Result, message::{Message, MessageStatus}, }; pub use node_types::*; use rand::{Fill, Rng, SeedableRng, rngs::StdRng}; -use utils::{into_coin_configs, into_message_configs}; +use utils::{into_coin_configs, into_coin_configs2, into_message_configs}; pub use wallets_config::*; mod node_types; @@ -105,6 +105,29 @@ pub fn setup_single_asset_coins( coins } +pub fn setup_single_data_coin( + owner: &Bech32Address, + asset_id: AssetId, + amount: u64, + data: Vec, +) -> DataCoin { + let mut rng = rand::thread_rng(); + let mut r = Bytes32::zeroed(); + r.try_fill(&mut rng) + .expect("failed to fill with random data"); + let utxo_id = UtxoId::new(r, 0); + + DataCoin { + owner: owner.clone(), + utxo_id, + amount, + asset_id, + status: CoinStatus::Unspent, + block_created: Default::default(), + data, + } +} + pub fn setup_single_message( sender: &Bech32Address, recipient: &Bech32Address, @@ -123,8 +146,40 @@ pub fn setup_single_message( } } +pub fn setup_single_asset_data_coins( + owner: &Bech32Address, + asset_id: AssetId, + num_coins: u64, + amount_per_coin: u64, + data: Vec, +) -> Vec { + let mut rng = rand::thread_rng(); + + let coins: Vec = (1..=num_coins) + .map(|_i| { + let mut r = Bytes32::zeroed(); + r.try_fill(&mut rng) + .expect("failed to fill with random data"); + let utxo_id = UtxoId::new(r, 0); + + DataCoin { + owner: owner.clone(), + utxo_id, + amount: amount_per_coin, + asset_id, + status: CoinStatus::Unspent, + block_created: Default::default(), + data: data.clone(), + } + }) + .collect(); + + coins +} + pub async fn setup_test_provider( coins: Vec, + // data_coins: Vec, messages: Vec, node_config: Option, chain_config: Option, @@ -153,6 +208,37 @@ pub async fn setup_test_provider( Provider::from(address).await } +pub async fn setup_test_provider2( + coins: Vec, + data_coins: Vec, + messages: Vec, + node_config: Option, + chain_config: Option, +) -> Result { + let node_config = node_config.unwrap_or_default(); + let chain_config = chain_config.unwrap_or_else(testnet_chain_config); + + let coin_configs = into_coin_configs2(coins, data_coins); + let message_configs = into_message_configs(messages); + + let state_config = StateConfig { + coins: coin_configs, + messages: message_configs, + ..StateConfig::local_testnet() + }; + + let srv = FuelService::start(node_config, chain_config, state_config).await?; + + let address = srv.bound_address(); + + tokio::spawn(async move { + let _own_the_handle = srv; + let () = futures::future::pending().await; + }); + + Provider::from(address).await +} + // Testnet ChainConfig with increased tx size and contract size limits fn testnet_chain_config() -> ChainConfig { let mut consensus_parameters = ConsensusParameters::default(); diff --git a/packages/fuels-test-helpers/src/utils.rs b/packages/fuels-test-helpers/src/utils.rs index d459f6335f..1bd7c582d0 100644 --- a/packages/fuels-test-helpers/src/utils.rs +++ b/packages/fuels-test-helpers/src/utils.rs @@ -1,10 +1,24 @@ use fuel_core_chain_config::{CoinConfig, MessageConfig}; -use fuels_core::types::{coin::Coin, message::Message}; +use fuels_core::types::{ + coin::{Coin, DataCoin}, + message::Message, +}; -pub(crate) fn into_coin_configs(coins: Vec) -> Vec { +pub(crate) fn into_coin_configs( + coins: Vec, /*, data_coins: Vec*/ +) -> Vec { coins .into_iter() .map(Into::into) + // .chain(data_coins.into_iter().map(Into::into)) + .collect::>() +} + +pub(crate) fn into_coin_configs2(coins: Vec, data_coins: Vec) -> Vec { + coins + .into_iter() + .map(Into::into) + .chain(data_coins.into_iter().map(Into::into)) .collect::>() }