diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index 84296954a..cec1b3138 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -259,10 +259,24 @@ pub struct Order { } impl Order { - pub fn fill(self, fill_amount_in_ask_currency: Amount) -> Self { + pub fn fill( + self, + chain_config: &ChainConfig, + block_height: BlockHeight, + fill_amount_in_ask_currency: Amount, + ) -> Self { + let (ask_balance, give_balance) = match chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .orders_version() + { + common::chain::OrdersVersion::V0 => (self.ask_balance, self.give_balance), + common::chain::OrdersVersion::V1 => (self.initially_asked, self.initially_given), + }; let filled_amount = orders_accounting::calculate_filled_amount( - self.ask_balance, - self.give_balance, + ask_balance, + give_balance, fill_amount_in_ask_currency, ) .expect("must succeed"); diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 21292d0ed..8c311e227 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -34,8 +34,8 @@ use common::{ tokens::{get_referenced_token_ids, make_token_id, IsTokenFrozen, TokenId, TokenIssuance}, transaction::OutPointSourceId, AccountCommand, AccountNonce, AccountSpending, Block, DelegationId, Destination, GenBlock, - Genesis, OrderData, OrderId, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, - UtxoOutPoint, + Genesis, OrderAccountCommand, OrderData, OrderId, PoolId, SignedTransaction, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, CoinOrTokenId, Fee, Id, Idable, H256}, }; @@ -583,18 +583,24 @@ async fn calculate_tx_fee_and_collect_token_info( AccountCommand::ConcludeOrder(order_id) | AccountCommand::FillOrder(order_id, _, _) => { let order = db_tx.get_order(*order_id).await?.expect("must exist"); - match order.ask_currency { - CoinOrTokenId::Coin => {} - CoinOrTokenId::TokenId(id) => { - token_ids.insert(id); - } - }; - match order.give_currency { - CoinOrTokenId::Coin => {} - CoinOrTokenId::TokenId(id) => { - token_ids.insert(id); - } - }; + if let Some(id) = order.ask_currency.token_id() { + token_ids.insert(id); + } + if let Some(id) = order.give_currency.token_id() { + token_ids.insert(id); + } + } + }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + let order = db_tx.get_order(*order_id).await?.expect("must exist"); + if let Some(id) = order.ask_currency.token_id() { + token_ids.insert(id); + } + if let Some(id) = order.give_currency.token_id() { + token_ids.insert(id); + } } }, }; @@ -641,7 +647,9 @@ async fn fetch_utxo( .expect("must be present"); Ok(Some(utxo)) } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => Ok(None), + TxInput::Account(_) | TxInput::AccountCommand(_, _) | TxInput::OrderAccountCommand(_) => { + Ok(None) + } } } @@ -808,9 +816,26 @@ async fn prefetch_orders( let mut ask_balances = BTreeMap::::new(); let mut give_balances = BTreeMap::::new(); + let to_order_data = |order: &Order| { + let ask = order.ask_currency.to_output_value(order.initially_asked); + let give = order.give_currency.to_output_value(order.initially_given); + OrderData::new(order.conclude_destination.clone(), ask, give) + }; + for input in inputs { match input { TxInput::Utxo(_) | TxInput::Account(_) => {} + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + let order = db_tx.get_order(*order_id).await?.expect("must be present "); + let order_data = to_order_data(&order); + + ask_balances.insert(*order_id, order.ask_balance); + give_balances.insert(*order_id, order.give_balance); + orders_data.insert(*order_id, order_data); + } + }, TxInput::AccountCommand(_, account_command) => match account_command { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -822,21 +847,10 @@ async fn prefetch_orders( AccountCommand::FillOrder(order_id, _, _) | AccountCommand::ConcludeOrder(order_id) => { let order = db_tx.get_order(*order_id).await?.expect("must be present "); + let order_data = to_order_data(&order); + ask_balances.insert(*order_id, order.ask_balance); give_balances.insert(*order_id, order.give_balance); - let ask = match order.ask_currency { - CoinOrTokenId::Coin => OutputValue::Coin(order.initially_asked), - CoinOrTokenId::TokenId(token_id) => { - OutputValue::TokenV1(token_id, order.initially_asked) - } - }; - let give = match order.give_currency { - CoinOrTokenId::Coin => OutputValue::Coin(order.initially_given), - CoinOrTokenId::TokenId(token_id) => { - OutputValue::TokenV1(token_id, order.initially_given) - } - }; - let order_data = OrderData::new(order.conclude_destination.clone(), ask, give); orders_data.insert(*order_id, order_data); } }, @@ -888,7 +902,9 @@ async fn update_tables_from_consensus_data( ) .await; } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => {} + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => {} } } @@ -1194,7 +1210,8 @@ async fn update_tables_from_transaction_inputs( } AccountCommand::FillOrder(order_id, fill_amount_in_ask_currency, _) => { let order = db_tx.get_order(*order_id).await?.expect("must exist"); - let order = order.fill(*fill_amount_in_ask_currency); + let order = + order.fill(&chain_config, block_height, *fill_amount_in_ask_currency); db_tx.set_order_at_height(*order_id, &order, block_height).await?; } @@ -1205,6 +1222,21 @@ async fn update_tables_from_transaction_inputs( db_tx.set_order_at_height(*order_id, &order, block_height).await?; } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, fill_amount_in_ask_currency, _) => { + let order = db_tx.get_order(*order_id).await?.expect("must exist"); + let order = + order.fill(&chain_config, block_height, *fill_amount_in_ask_currency); + + db_tx.set_order_at_height(*order_id, &order, block_height).await?; + } + OrderAccountCommand::ConcludeOrder(order_id) => { + let order = db_tx.get_order(*order_id).await?.expect("must exist"); + let order = order.conclude(); + + db_tx.set_order_at_height(*order_id, &order, block_height).await?; + } + }, TxInput::Account(outpoint) => { match outpoint.account() { AccountSpending::DelegationBalance(delegation_id, amount) => { diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index dac7f27c0..d3f9ae49b 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -646,6 +646,7 @@ fn update_statistics( } AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => {} }, + TxInput::OrderAccountCommand(..) => {} }); } diff --git a/api-server/stack-test-suite/tests/v2/block.rs b/api-server/stack-test-suite/tests/v2/block.rs index 823bba735..51097e410 100644 --- a/api-server/stack-test-suite/tests/v2/block.rs +++ b/api-server/stack-test-suite/tests/v2/block.rs @@ -279,7 +279,9 @@ async fn get_tx_additional_data( TxInput::Utxo(outpoint) => { db_tx.get_utxo(outpoint.clone()).await.unwrap().map(|utxo| utxo.into_output()) } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, }; input_utxos.push(utxo); } diff --git a/api-server/stack-test-suite/tests/v2/orders.rs b/api-server/stack-test-suite/tests/v2/orders.rs index cb0313dc1..f1c9d1237 100644 --- a/api-server/stack-test-suite/tests/v2/orders.rs +++ b/api-server/stack-test-suite/tests/v2/orders.rs @@ -13,15 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{make_order_id, AccountCommand, AccountNonce, OrderData}; +use common::chain::{ + make_order_id, AccountCommand, AccountNonce, OrderAccountCommand, OrderData, OrdersVersion, +}; use super::*; #[rstest] #[trace] -#[case(Seed::from_entropy())] +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] #[tokio::test] -async fn create_fill_conclude_order(#[case] seed: Seed) { +async fn create_fill_conclude_order(#[case] seed: Seed, #[case] version: OrdersVersion) { + use common::chain::config::create_unit_test_config_builder; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -31,7 +37,25 @@ async fn create_fill_conclude_order(#[case] seed: Seed) { let web_server_state = { let mut rng = make_seedable_rng(seed); - let chain_config = create_unit_test_config(); + let chain_config = create_unit_test_config_builder() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + common::chain::ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, + common::chain::HtlcActivated::Yes, + common::chain::OrdersActivated::Yes, + version, + ), + )]) + .expect("cannot fail"), + ) + .build(); let chainstate_blocks = { let mut tf = TestFramework::builder(&mut rng) @@ -62,22 +86,29 @@ async fn create_fill_conclude_order(#[case] seed: Seed) { tf.process_block(block1.clone(), BlockSource::Local).unwrap(); // Fill order + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + Amount::from_atoms(1), + Destination::AnyoneCanSpend, + ), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + Amount::from_atoms(1), + Destination::AnyoneCanSpend, + )) + } + }; let tx2 = TransactionBuilder::new() .add_input( TxInput::Utxo(issue_and_mint_result.change_outpoint), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - Amount::from_atoms(1), - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(issue_and_mint_result.token_id, Amount::from_atoms(1)), Destination::AnyoneCanSpend, @@ -89,14 +120,17 @@ async fn create_fill_conclude_order(#[case] seed: Seed) { tf.process_block(block2.clone(), BlockSource::Local).unwrap(); // Conclude order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx3 = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::Coin(Amount::from_atoms(1)), Destination::AnyoneCanSpend, diff --git a/api-server/stack-test-suite/tests/v2/transaction.rs b/api-server/stack-test-suite/tests/v2/transaction.rs index 2e7288896..43ef4dec0 100644 --- a/api-server/stack-test-suite/tests/v2/transaction.rs +++ b/api-server/stack-test-suite/tests/v2/transaction.rs @@ -181,7 +181,9 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) { TxInput::Utxo(outpoint) => { Some(signed_tx1.outputs()[outpoint.output_index() as usize].clone()) } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, }); let transaction = signed_tx2.transaction(); @@ -327,7 +329,9 @@ async fn ok(#[case] seed: Seed) { TxInput::Utxo(outpoint) => { Some(prev_tx.outputs()[outpoint.output_index() as usize].clone()) } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, }); let expected_transaction = json!({ diff --git a/api-server/stack-test-suite/tests/v2/transactions.rs b/api-server/stack-test-suite/tests/v2/transactions.rs index 5872f4ea2..5c44191b2 100644 --- a/api-server/stack-test-suite/tests/v2/transactions.rs +++ b/api-server/stack-test-suite/tests/v2/transactions.rs @@ -116,7 +116,9 @@ async fn ok(#[case] seed: Seed) { TxInput::Utxo(outpoint) => Some( prev_tx.outputs()[outpoint.output_index() as usize].clone(), ), - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, }) .collect(); diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index 9ebefb004..65b5ee053 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -1515,6 +1515,7 @@ async fn orders<'a, S: for<'b> Transactional<'b>>( rng: &mut (impl Rng + CryptoRng), storage: &'a mut S, ) { + let chain_config = common::chain::config::create_regtest(); { let db_tx = storage.transaction_ro().await.unwrap(); let random_order_id = OrderId::new(H256::random_using(rng)); @@ -1627,7 +1628,7 @@ async fn orders<'a, S: for<'b> Transactional<'b>>( ); // Fill one order - let order2_filled = order2.clone().fill(Amount::from_atoms(1)); + let order2_filled = order2.clone().fill(&chain_config, block_height, Amount::from_atoms(1)); db_tx .set_order_at_height(order2_id, &order2_filled, block_height.next_height()) .await diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index 49b5157e4..a59c8421c 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -24,8 +24,8 @@ use common::{ block::ConsensusData, output_value::OutputValue, tokens::{IsTokenUnfreezable, NftIssuance, TokenId, TokenTotalSupply}, - AccountCommand, AccountSpending, Block, ChainConfig, Destination, OrderId, - OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountSpending, Block, ChainConfig, Destination, OrderAccountCommand, + OrderId, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}, Uint256, @@ -318,6 +318,24 @@ pub fn tx_input_to_json(inp: &TxInput, chain_config: &ChainConfig) -> serde_json }) } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(id, fill, dest) => { + json!({ + "input_type": "OrderAccountCommand", + "command": "FillOrder", + "order_id": Address::new(chain_config, *id).expect("addressable").to_string(), + "fill_atoms": json!({"atoms": fill.into_atoms().to_string()}), + "destination": Address::new(chain_config, dest.clone()).expect("addressable").as_str(), + }) + } + OrderAccountCommand::ConcludeOrder(order_id) => { + json!({ + "input_type": "OrderAccountCommand", + "command": "ConcludeOrder", + "order_id": Address::new(chain_config, *order_id).expect("addressable").to_string(), + }) + } + }, TxInput::AccountCommand(nonce, cmd) => match cmd { AccountCommand::MintTokens(token_id, amount) => { json!({ diff --git a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs index 6a1875ef1..af6fd793b 100644 --- a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs +++ b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs @@ -18,7 +18,7 @@ use std::{collections::BTreeMap, num::NonZeroU64}; use common::{ chain::{ output_value::OutputValue, timelock::OutputTimeLock, AccountCommand, AccountSpending, - AccountType, ChainConfig, TxInput, TxOutput, UtxoOutPoint, + AccountType, ChainConfig, OrderAccountCommand, OrderId, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Fee, Subsidy}, }; @@ -110,6 +110,16 @@ impl ConstrainedValueAccumulator { &mut temp_tokens_accounting, )?; + insert_or_increase(&mut total_to_deduct, id, to_deduct)?; + } + TxInput::OrderAccountCommand(command) => { + let (id, to_deduct) = accumulator.process_input_order_account_command( + chain_config, + block_height, + command, + &mut temp_orders_accounting, + )?; + insert_or_increase(&mut total_to_deduct, id, to_deduct)?; } } @@ -295,67 +305,130 @@ impl ConstrainedValueAccumulator { chain_config.token_change_metadata_uri_fee(), )), AccountCommand::ConcludeOrder(id) => { - let order_data = orders_accounting_delta - .get_order_data(id) - .map_err(|_| orders_accounting::Error::ViewFail)? - .ok_or(orders_accounting::Error::OrderDataNotFound(*id))?; - let ask_balance = orders_accounting_delta - .get_ask_balance(id) - .map_err(|_| orders_accounting::Error::ViewFail)?; - let give_balance = orders_accounting_delta - .get_give_balance(id) - .map_err(|_| orders_accounting::Error::ViewFail)?; + self.process_conclude_order_command(*id, orders_accounting_delta) + } + AccountCommand::FillOrder(id, fill_amount_in_ask_currency, _) => self + .process_fill_order_command( + chain_config, + block_height, + *id, + *fill_amount_in_ask_currency, + orders_accounting_delta, + ), + } + } - { - // Ensure that spending won't result in negative balance - let _ = orders_accounting_delta.conclude_order(*id)?; - let _ = orders_accounting_delta.get_ask_balance(id)?; - let _ = orders_accounting_delta.get_give_balance(id)?; - } + fn process_input_order_account_command( + &mut self, + chain_config: &ChainConfig, + block_height: BlockHeight, + command: &OrderAccountCommand, + orders_accounting_delta: &mut O, + ) -> Result<(CoinOrTokenId, Amount), Error> + where + O: OrdersAccountingOperations + OrdersAccountingView, + { + match command { + OrderAccountCommand::FillOrder(id, amount, _) => self.process_fill_order_command( + chain_config, + block_height, + *id, + *amount, + orders_accounting_delta, + ), + OrderAccountCommand::ConcludeOrder(order_id) => { + self.process_conclude_order_command(*order_id, orders_accounting_delta) + } + } + } - let initially_asked = output_value_amount(order_data.ask())?; - let filled_amount = (initially_asked - ask_balance) - .ok_or(Error::NegativeAccountBalance(AccountType::Order(*id)))?; + fn process_conclude_order_command( + &mut self, + id: OrderId, + orders_accounting_delta: &mut O, + ) -> Result<(CoinOrTokenId, Amount), Error> + where + O: OrdersAccountingOperations + OrdersAccountingView, + { + let order_data = orders_accounting_delta + .get_order_data(&id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .ok_or(orders_accounting::Error::OrderDataNotFound(id))?; + let ask_balance = orders_accounting_delta + .get_ask_balance(&id) + .map_err(|_| orders_accounting::Error::ViewFail)?; + let give_balance = orders_accounting_delta + .get_give_balance(&id) + .map_err(|_| orders_accounting::Error::ViewFail)?; + + { + // Ensure that spending won't result in negative balance + let _ = orders_accounting_delta.conclude_order(id)?; + let _ = orders_accounting_delta.get_ask_balance(&id)?; + let _ = orders_accounting_delta.get_give_balance(&id)?; + } - let ask_currency = CoinOrTokenId::from_output_value(order_data.ask()) - .ok_or(Error::UnsupportedTokenVersion)?; - insert_or_increase(&mut self.unconstrained_value, ask_currency, filled_amount)?; + let initially_asked = output_value_amount(order_data.ask())?; + let filled_amount = (initially_asked - ask_balance) + .ok_or(Error::NegativeAccountBalance(AccountType::Order(id)))?; - let give_currency = CoinOrTokenId::from_output_value(order_data.give()) - .ok_or(Error::UnsupportedTokenVersion)?; - insert_or_increase(&mut self.unconstrained_value, give_currency, give_balance)?; + let ask_currency = CoinOrTokenId::from_output_value(order_data.ask()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, ask_currency, filled_amount)?; - Ok((CoinOrTokenId::Coin, Amount::ZERO)) - } - AccountCommand::FillOrder(id, fill_amount_in_ask_currency, _) => { - let order_data = orders_accounting_delta - .get_order_data(id) - .map_err(|_| orders_accounting::Error::ViewFail)? - .ok_or(orders_accounting::Error::OrderDataNotFound(*id))?; - let filled_amount = orders_accounting::calculate_fill_order( - &orders_accounting_delta, - *id, - *fill_amount_in_ask_currency, - )?; + let give_currency = CoinOrTokenId::from_output_value(order_data.give()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, give_currency, give_balance)?; - { - // Ensure that spending won't result in negative balance - let _ = - orders_accounting_delta.fill_order(*id, *fill_amount_in_ask_currency)?; - let _ = orders_accounting_delta.get_ask_balance(id)?; - let _ = orders_accounting_delta.get_give_balance(id)?; - } + Ok((CoinOrTokenId::Coin, Amount::ZERO)) + } + + fn process_fill_order_command( + &mut self, + chain_config: &ChainConfig, + block_height: BlockHeight, + order_id: OrderId, + fill_amount_in_ask_currency: Amount, + orders_accounting_delta: &mut O, + ) -> Result<(CoinOrTokenId, Amount), Error> + where + O: OrdersAccountingOperations + OrdersAccountingView, + { + let order_data = orders_accounting_delta + .get_order_data(&order_id) + .map_err(|_| orders_accounting::Error::ViewFail)? + .ok_or(orders_accounting::Error::OrderDataNotFound(order_id))?; + let orders_version = chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .orders_version(); + let filled_amount = orders_accounting::calculate_fill_order( + &orders_accounting_delta, + order_id, + fill_amount_in_ask_currency, + orders_version, + )?; + + { + // Ensure that spending won't result in negative balance + let _ = orders_accounting_delta.fill_order( + order_id, + fill_amount_in_ask_currency, + orders_version, + )?; + let _ = orders_accounting_delta.get_ask_balance(&order_id)?; + let _ = orders_accounting_delta.get_give_balance(&order_id)?; + } - let give_currency = CoinOrTokenId::from_output_value(order_data.give()) - .ok_or(Error::UnsupportedTokenVersion)?; - insert_or_increase(&mut self.unconstrained_value, give_currency, filled_amount)?; + let give_currency = CoinOrTokenId::from_output_value(order_data.give()) + .ok_or(Error::UnsupportedTokenVersion)?; + insert_or_increase(&mut self.unconstrained_value, give_currency, filled_amount)?; - let ask_currency = CoinOrTokenId::from_output_value(order_data.ask()) - .ok_or(Error::UnsupportedTokenVersion)?; + let ask_currency = CoinOrTokenId::from_output_value(order_data.ask()) + .ok_or(Error::UnsupportedTokenVersion)?; - Ok((ask_currency, *fill_amount_in_ask_currency)) - } - } + Ok((ask_currency, fill_amount_in_ask_currency)) } pub fn from_outputs( diff --git a/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs b/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs index 85b0cc4e3..3b454103c 100644 --- a/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs +++ b/chainstate/constraints-value-accumulator/src/tests/orders_constraints.rs @@ -17,9 +17,11 @@ use std::collections::BTreeMap; use common::{ chain::{ - config::create_unit_test_config, output_value::OutputValue, tokens::TokenId, - AccountCommand, AccountNonce, Destination, OrderData, OrderId, OutPointSourceId, TxInput, - TxOutput, UtxoOutPoint, + config::{create_unit_test_config, create_unit_test_config_builder}, + output_value::OutputValue, + tokens::TokenId, + AccountCommand, AccountNonce, Destination, OrderAccountCommand, OrderData, OrderId, + OrdersVersion, OutPointSourceId, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Fee, Id, H256}, }; @@ -252,11 +254,31 @@ fn create_order_constraints(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_constraints(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_constraints(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); - let chain_config = create_unit_test_config(); + let chain_config = create_unit_test_config_builder() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + common::chain::ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, + common::chain::HtlcActivated::Yes, + common::chain::OrdersActivated::Yes, + version, + ), + )]) + .expect("cannot fail"), + ) + .build(); let block_height = BlockHeight::one(); let pos_store = InMemoryPoSAccounting::new(); @@ -284,12 +306,8 @@ fn fill_order_constraints(#[case] seed: Seed) { // use in command more than provided in input { - let inputs = vec![ - TxInput::Utxo(UtxoOutPoint::new( - OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), - 0, - )), - TxInput::AccountCommand( + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( AccountNonce::new(0), AccountCommand::FillOrder( order_id, @@ -297,6 +315,18 @@ fn fill_order_constraints(#[case] seed: Seed) { Destination::AnyoneCanSpend, ), ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + (ask_amount + Amount::from_atoms(1)).unwrap(), + Destination::AnyoneCanSpend, + )), + }; + let inputs = vec![ + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -328,15 +358,23 @@ fn fill_order_constraints(#[case] seed: Seed) { // fill with coins instead of tokens { + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let inputs = vec![ TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), 0, )), - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -361,15 +399,23 @@ fn fill_order_constraints(#[case] seed: Seed) { // try to print coins in output { + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let inputs = vec![ TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), 0, )), - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -409,15 +455,23 @@ fn fill_order_constraints(#[case] seed: Seed) { // try to print tokens in output { + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let inputs = vec![ TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), 0, )), - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -462,15 +516,23 @@ fn fill_order_constraints(#[case] seed: Seed) { { // partially use input in command + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let inputs = vec![ TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), 0, )), - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -513,15 +575,23 @@ fn fill_order_constraints(#[case] seed: Seed) { } // valid case + let fill_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let inputs = vec![ TxInput::Utxo(UtxoOutPoint::new( OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), 0, )), - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), + fill_command, ]; let input_utxos = vec![ Some(TxOutput::Transfer( @@ -559,11 +629,31 @@ fn fill_order_constraints(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn conclude_order_constraints(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn conclude_order_constraints(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); - let chain_config = create_unit_test_config(); + let chain_config = create_unit_test_config_builder() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + common::chain::ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::FrozenTokensValidationVersion::V1, + common::chain::HtlcActivated::Yes, + common::chain::OrdersActivated::Yes, + version, + ), + )]) + .expect("cannot fail"), + ) + .build(); let block_height = BlockHeight::one(); let pos_store = InMemoryPoSAccounting::new(); @@ -591,10 +681,16 @@ fn conclude_order_constraints(#[case] seed: Seed) { // try to print coins in output { - let inputs = vec![TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - )]; + let conclude_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + let inputs = vec![conclude_command]; let input_utxos = vec![None]; let outputs = vec![TxOutput::Transfer( @@ -627,10 +723,16 @@ fn conclude_order_constraints(#[case] seed: Seed) { // try to print tokens in output { - let inputs = vec![TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - )]; + let conclude_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + let inputs = vec![conclude_command]; let input_utxos = vec![None]; let outputs = vec![TxOutput::Transfer( @@ -665,10 +767,16 @@ fn conclude_order_constraints(#[case] seed: Seed) { { // partially use input in command - let inputs = vec![TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - )]; + let conclude_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + let inputs = vec![conclude_command]; let input_utxos = vec![None]; let outputs = vec![TxOutput::Transfer( @@ -701,10 +809,16 @@ fn conclude_order_constraints(#[case] seed: Seed) { } // valid case - let inputs = vec![TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - )]; + let conclude_command = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + let inputs = vec![conclude_command]; let input_utxos = vec![None]; let outputs = diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index b849e829e..d78bd9f99 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -143,6 +143,7 @@ impl BanScore for ConnectTransactionError { ConnectTransactionError::InputCheck(e) => e.ban_score(), ConnectTransactionError::OrdersAccountingError(err) => err.ban_score(), ConnectTransactionError::AttemptToCreateOrderFromAccounts => 100, + ConnectTransactionError::ConcludeInputAmountsDontMatch(_, _) => 100, } } } @@ -364,8 +365,11 @@ impl BanScore for CheckTransactionError { CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, CheckTransactionError::HtlcsAreNotActivated => 100, CheckTransactionError::OrdersAreNotActivated(_) => 100, + CheckTransactionError::AttemptToFillOrderWithZero(_, _) => 100, CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, CheckTransactionError::ChangeTokenMetadataUriNotActivated => 100, + CheckTransactionError::OrdersV1AreNotActivated(_) => 100, + CheckTransactionError::DeprecatedOrdersCommands(_) => 100, } } } @@ -572,6 +576,7 @@ impl BanScore for IOPolicyError { IOPolicyError::InvalidInputTypeInTx => 100, IOPolicyError::MultiplePoolCreated => 100, IOPolicyError::MultipleDelegationCreated => 100, + IOPolicyError::MultipleOrdersCreated => 100, IOPolicyError::ProduceBlockInTx => 100, IOPolicyError::MultipleAccountCommands => 100, IOPolicyError::AttemptToUseAccountInputInReward => 100, @@ -676,6 +681,7 @@ impl BanScore for orders_accounting::Error { Error::InvariantOrderGiveBalanceExistForConcludeUndo(_) => 100, Error::OrderOverflow(_) => 100, Error::OrderOverbid(_, _, _) => 100, + Error::OrderUnderbid(_, _) => 100, Error::AttemptedConcludeNonexistingOrderData(_) => 100, Error::InvariantNonzeroAskBalanceForMissingOrder(_) => 100, Error::InvariantNonzeroGiveBalanceForMissingOrder(_) => 100, diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index 70bf99da8..9b9e724cc 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -301,7 +301,8 @@ impl BlockProcessingErrorClassification for ConnectTransactionError { | ConnectTransactionError::IOPolicyError(_, _) | ConnectTransactionError::TotalFeeRequiredOverflow | ConnectTransactionError::InsufficientCoinsFee(_, _) - | ConnectTransactionError::AttemptToSpendFrozenToken(_) => { + | ConnectTransactionError::AttemptToSpendFrozenToken(_) + | ConnectTransactionError::ConcludeInputAmountsDontMatch(_, _) => { BlockProcessingErrorClass::BadBlock } @@ -802,11 +803,13 @@ impl BlockProcessingErrorClassification for CheckTransactionError { | CheckTransactionError::DeprecatedTokenOperationVersion(_, _) | CheckTransactionError::HtlcsAreNotActivated | CheckTransactionError::OrdersAreNotActivated(_) + | CheckTransactionError::AttemptToFillOrderWithZero(_, _) | CheckTransactionError::ChangeTokenMetadataUriNotActivated + | CheckTransactionError::OrdersV1AreNotActivated(_) + | CheckTransactionError::DeprecatedOrdersCommands(_) | CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => { BlockProcessingErrorClass::BadBlock } - CheckTransactionError::PropertyQueryError(err) => err.classify(), CheckTransactionError::TokensError(err) => err.classify(), } @@ -913,6 +916,7 @@ impl BlockProcessingErrorClassification for orders_accounting::Error { | Error::InvariantOrderGiveBalanceExistForConcludeUndo(_) | Error::OrderOverflow(_) | Error::OrderOverbid(_, _, _) + | Error::OrderUnderbid(_, _) | Error::AttemptedConcludeNonexistingOrderData(_) | Error::UnsupportedTokenVersion | Error::InvariantNonzeroAskBalanceForMissingOrder(_) diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index a62f40f42..7d0fb8848 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -541,7 +541,9 @@ where None => Ok(None), } } - TxInput::Account(..) | TxInput::AccountCommand(..) => Ok(None), + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => Ok(None), }) .collect::, _>>() } diff --git a/chainstate/src/rpc/types/account.rs b/chainstate/src/rpc/types/account.rs index 26b7235c8..3a6f6e831 100644 --- a/chainstate/src/rpc/types/account.rs +++ b/chainstate/src/rpc/types/account.rs @@ -17,7 +17,8 @@ use common::{ address::{AddressError, RpcAddress}, chain::{ tokens::{IsTokenUnfreezable, TokenId}, - AccountCommand, AccountSpending, ChainConfig, DelegationId, Destination, OrderId, + AccountCommand, AccountSpending, ChainConfig, DelegationId, Destination, + OrderAccountCommand, OrderId, }, primitives::amount::RpcAmountOut, }; @@ -134,3 +135,35 @@ impl RpcAccountCommand { Ok(result) } } + +#[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] +#[serde(tag = "type", content = "content")] +pub enum RpcOrderAccountCommand { + ConcludeOrder { + order_id: RpcAddress, + }, + FillOrder { + order_id: RpcAddress, + fill_value: RpcAmountOut, + destination: RpcAddress, + }, +} + +impl RpcOrderAccountCommand { + pub fn new( + chain_config: &ChainConfig, + command: &OrderAccountCommand, + ) -> Result { + let result = match command { + OrderAccountCommand::ConcludeOrder(order_id) => RpcOrderAccountCommand::ConcludeOrder { + order_id: RpcAddress::new(chain_config, *order_id)?, + }, + OrderAccountCommand::FillOrder(id, fill, dest) => RpcOrderAccountCommand::FillOrder { + order_id: RpcAddress::new(chain_config, *id)?, + fill_value: RpcAmountOut::from_amount(*fill, chain_config.coin_decimals()), + destination: RpcAddress::new(chain_config, dest.clone())?, + }, + }; + Ok(result) + } +} diff --git a/chainstate/src/rpc/types/input.rs b/chainstate/src/rpc/types/input.rs index 4c51b3253..7c278ae94 100644 --- a/chainstate/src/rpc/types/input.rs +++ b/chainstate/src/rpc/types/input.rs @@ -19,7 +19,7 @@ use common::{ primitives::Id, }; -use super::account::{RpcAccountCommand, RpcAccountSpending}; +use super::account::{RpcAccountCommand, RpcAccountSpending, RpcOrderAccountCommand}; #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] @@ -36,6 +36,9 @@ pub enum RpcTxInput { nonce: u64, command: RpcAccountCommand, }, + OrderAccountCommand { + command: RpcOrderAccountCommand, + }, } impl RpcTxInput { @@ -59,6 +62,9 @@ impl RpcTxInput { nonce: nonce.value(), command: RpcAccountCommand::new(chain_config, command)?, }, + TxInput::OrderAccountCommand(cmd) => RpcTxInput::OrderAccountCommand { + command: RpcOrderAccountCommand::new(chain_config, cmd)?, + }, }; Ok(result) } diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index a4750d06e..dbe8ac10b 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -215,6 +215,7 @@ impl<'f> BlockBuilder<'f> { TxInput::AccountCommand(nonce, op) => { self.account_nonce_tracker.insert(op.clone().into(), *nonce); } + TxInput::OrderAccountCommand(..) => {} }; }); diff --git a/chainstate/test-framework/src/pos_block_builder.rs b/chainstate/test-framework/src/pos_block_builder.rs index 8153214b8..bbb494694 100644 --- a/chainstate/test-framework/src/pos_block_builder.rs +++ b/chainstate/test-framework/src/pos_block_builder.rs @@ -455,6 +455,7 @@ impl<'f> PoSBlockBuilder<'f> { TxInput::AccountCommand(nonce, op) => { self.account_nonce_tracker.insert(op.clone().into(), *nonce); } + TxInput::OrderAccountCommand(..) => {} }; }); diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index 67a8f5714..c74446322 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -34,8 +34,8 @@ use common::{ TokenTotalSupply, }, AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, AccountType, DelegationId, - Destination, GenBlockId, OrderData, OrderId, OutPointSourceId, PoolId, Transaction, - TxInput, TxOutput, UtxoOutPoint, + Destination, GenBlockId, OrderAccountCommand, OrderData, OrderId, OrdersVersion, + OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, BlockHeight, CoinOrTokenId, Id, Idable, H256}, }; @@ -520,12 +520,6 @@ impl<'a> RandomTxMaker<'a> { if !is_frozen_token(order_data.ask(), tokens_cache) && !is_frozen_token(order_data.give(), tokens_cache) { - let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); - result_inputs.push(TxInput::AccountCommand( - new_nonce, - AccountCommand::ConcludeOrder(order_id), - )); - let available_give_balance = orders_cache.get_give_balance(&order_id).unwrap(); let give_output = @@ -539,6 +533,10 @@ impl<'a> RandomTxMaker<'a> { let filled_output = output_value_with_amount(order_data.ask(), filled_amount); + result_inputs.push(TxInput::OrderAccountCommand( + OrderAccountCommand::ConcludeOrder(order_id), + )); + let _ = orders_cache.conclude_order(order_id).unwrap(); self.account_command_used = true; @@ -1005,30 +1003,33 @@ impl<'a> RandomTxMaker<'a> { get_random_order_to_fill(self.orders_store, &orders_cache, &fill_value) { let filled_value = - calculate_filled_order_value(&orders_cache, order_id, amount_to_spend); + calculate_filled_order_value(&orders_cache, order_id, amount_to_spend) + .unwrap(); - if !is_frozen_token(&filled_value, tokens_cache) { - let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); - let input = TxInput::AccountCommand( - new_nonce, - AccountCommand::FillOrder( - order_id, - amount_to_spend, + if let Some(filled_value) = filled_value { + if !is_frozen_token(&filled_value, tokens_cache) { + let input = + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + amount_to_spend, + key_manager + .new_destination(self.chainstate.get_chain_config(), rng), + )); + + let output = TxOutput::Transfer( + filled_value, key_manager .new_destination(self.chainstate.get_chain_config(), rng), - ), - ); - - let output = TxOutput::Transfer( - filled_value, - key_manager.new_destination(self.chainstate.get_chain_config(), rng), - ); + ); - let _ = orders_cache.fill_order(order_id, amount_to_spend).unwrap(); - self.account_command_used = true; + let _ = orders_cache + .fill_order(order_id, amount_to_spend, OrdersVersion::V1) + .unwrap(); + self.account_command_used = true; - result_inputs.push(input); - result_outputs.push(output); + result_inputs.push(input); + result_outputs.push(output); + } } } } else if switch == 6 { @@ -1229,30 +1230,37 @@ impl<'a> RandomTxMaker<'a> { &orders_cache, order_id, Amount::from_atoms(atoms), - ); - - if !is_frozen_token(&filled_value, tokens_cache) { - result_outputs.push(TxOutput::Transfer( - filled_value, - key_manager - .new_destination(self.chainstate.get_chain_config(), rng), - )); + ) + .unwrap(); - let new_nonce = self.get_next_nonce(AccountType::Order(order_id)); - result_inputs.push(TxInput::AccountCommand( - new_nonce, - AccountCommand::FillOrder( - order_id, - Amount::from_atoms(atoms), + if let Some(filled_value) = filled_value { + if !is_frozen_token(&filled_value, tokens_cache) { + result_outputs.push(TxOutput::Transfer( + filled_value, key_manager .new_destination(self.chainstate.get_chain_config(), rng), - ), - )); - - let _ = orders_cache - .fill_order(order_id, Amount::from_atoms(atoms)) - .unwrap(); - self.account_command_used = true; + )); + + result_inputs.push(TxInput::OrderAccountCommand( + OrderAccountCommand::FillOrder( + order_id, + Amount::from_atoms(atoms), + key_manager.new_destination( + self.chainstate.get_chain_config(), + rng, + ), + ), + )); + + let _ = orders_cache + .fill_order( + order_id, + Amount::from_atoms(atoms), + OrdersVersion::V1, + ) + .unwrap(); + self.account_command_used = true; + } } } } @@ -1473,10 +1481,17 @@ fn calculate_filled_order_value( view: &impl OrdersAccountingView, order_id: OrderId, fill: Amount, -) -> OutputValue { +) -> Result, orders_accounting::Error> { let order_data = view.get_order_data(&order_id).unwrap().unwrap(); - let filled_amount = orders_accounting::calculate_fill_order(view, order_id, fill).unwrap(); + let result = orders_accounting::calculate_fill_order(view, order_id, fill, OrdersVersion::V1); - output_value_with_amount(order_data.give(), filled_amount) + match result { + Ok(filled_amount) => Ok(Some(output_value_with_amount( + order_data.give(), + filled_amount, + ))), + Err(orders_accounting::Error::OrderUnderbid(..)) => Ok(None), + Err(e) => Err(e), + } } diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index d71c31d7e..ad194fdec 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -13,7 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{AccountCommand, AccountSpending, Destination, TxInput, TxOutput}; +use common::chain::{ + AccountCommand, AccountSpending, Destination, OrderAccountCommand, TxInput, TxOutput, +}; use orders_accounting::OrdersAccountingView; use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; @@ -184,6 +186,22 @@ impl<'a> SignatureDestinationGetter<'a> { } AccountCommand::FillOrder(_, _, d) => Ok(d.clone()), }, + TxInput::OrderAccountCommand(command) => match command { + OrderAccountCommand::FillOrder(_, _, d) => Ok(d.clone()), + OrderAccountCommand::ConcludeOrder(order_id) => { + let order_data = orders_view + .get_order_data(order_id) + .map_err(|_| { + SignatureDestinationGetterError::OrdersAccountingViewError( + orders_accounting::Error::ViewFail, + ) + })? + .ok_or(SignatureDestinationGetterError::OrderDataNotFound( + *order_id, + ))?; + Ok(order_data.conclude_key().clone()) + } + }, } }; diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index dcdcae7dd..8fb455079 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -372,7 +372,9 @@ pub fn sign_witnesses( TxInput::Utxo(outpoint) => { Some(utxo_view.utxo(outpoint).unwrap().unwrap().output().clone()) } - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .collect::>(); let input_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); diff --git a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs index 279169f82..71e877b28 100644 --- a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs +++ b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs @@ -15,6 +15,8 @@ use std::collections::BTreeMap; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + use super::*; use chainstate_storage::{BlockchainStorageRead, Transactional}; use chainstate_test_framework::{ @@ -24,10 +26,8 @@ use common::{ chain::{ output_value::OutputValue, tokens::{make_token_id, NftIssuance, TokenAuxiliaryData, TokenIssuanceV0}, - ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, - OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, - Transaction, TxInput, TxOutput, UtxoOutPoint, + Destination, OutPointSourceId, TokenIssuanceVersion, Transaction, TxInput, TxOutput, + UtxoOutPoint, }, primitives::{Amount, Id, Idable}, }; @@ -116,16 +116,9 @@ fn store_fungible_token_v0(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), )]) .unwrap(), ) @@ -199,16 +192,9 @@ fn store_nft_v0(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), )]) .unwrap(), ) @@ -507,29 +493,7 @@ fn reorg_store_coin_disposable(#[case] seed: Seed) { fn store_aux_data_from_issue_nft(#[case] seed: Seed) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng) - .with_chain_config( - common::chain::config::Builder::test_chain() - .chainstate_upgrades( - NetUpgrades::initialize(vec![( - BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), - )]) - .unwrap(), - ) - .genesis_unittest(Destination::AnyoneCanSpend) - .build(), - ) - .build(); + let mut tf = TestFramework::builder(&mut rng).build(); let token_id = make_token_id(&[TxInput::from_utxo(tf.genesis().get_id().into(), 0)]).unwrap(); diff --git a/chainstate/test-suite/src/tests/delegation_tests.rs b/chainstate/test-suite/src/tests/delegation_tests.rs index d84153d44..b14aa1d51 100644 --- a/chainstate/test-suite/src/tests/delegation_tests.rs +++ b/chainstate/test-suite/src/tests/delegation_tests.rs @@ -1760,3 +1760,77 @@ fn delegate_same_pool_as_staking(#[case] seed: Seed) { ) .unwrap(); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn delegate_and_spend_with_multiple_inputs_in_single_tx(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (_, _, delegation_id, _, transfer_outpoint) = prepare_delegation(&mut rng, &mut tf); + let available_amount = get_coin_amount_from_outpoint(&tf.storage, &transfer_outpoint); + let amount_to_delegate = (available_amount / 2).unwrap(); + let change = (available_amount - amount_to_delegate).unwrap(); + + // Delegate staking + let delegate_staking_tx = TransactionBuilder::new() + .add_input(transfer_outpoint.into(), empty_witness(&mut rng)) + .add_output(TxOutput::DelegateStaking(amount_to_delegate, delegation_id)) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change), + Destination::AnyoneCanSpend, + )) + .build(); + let delegate_staking_tx_id = delegate_staking_tx.transaction().get_id(); + + tf.make_block_builder() + .add_transaction(delegate_staking_tx) + .build_and_process(&mut rng) + .unwrap(); + + let original_delegation_balance = + PoSAccountingStorageRead::::get_delegation_balance( + &tf.storage, + delegation_id, + ) + .unwrap() + .unwrap(); + assert_eq!(amount_to_delegate, original_delegation_balance); + + // Spend entire delegated amount in multiple inputs + let atoms_per_input = test_utils::split_value(&mut rng, amount_to_delegate.into_atoms()); + let mut tx_builder = TransactionBuilder::new(); + for (i, atoms) in atoms_per_input.iter().enumerate() { + tx_builder = tx_builder.add_input( + TxInput::from_account( + AccountNonce::new(i as u64), + AccountSpending::DelegationBalance(delegation_id, Amount::from_atoms(*atoms)), + ), + empty_witness(&mut rng), + ); + } + + let tx = tx_builder + .add_input( + UtxoOutPoint::new(delegate_staking_tx_id.into(), 1).into(), + empty_witness(&mut rng), + ) + .add_output(TxOutput::LockThenTransfer( + OutputValue::Coin(amount_to_delegate), + Destination::AnyoneCanSpend, + OutputTimeLock::ForBlockCount(1), + )) + .build(); + + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + let delegation_balance = PoSAccountingStorageRead::::get_delegation_balance( + &tf.storage, + delegation_id, + ) + .unwrap(); + assert_eq!(None, delegation_balance); + }); +} diff --git a/chainstate/test-suite/src/tests/fungible_tokens.rs b/chainstate/test-suite/src/tests/fungible_tokens.rs index 1de20308c..cf8d4b798 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens.rs @@ -21,16 +21,14 @@ use chainstate::{ }; use chainstate_test_framework::{get_output_value, TestFramework, TransactionBuilder}; use common::chain::tokens::{Metadata, NftIssuanceV0, TokenIssuanceV0, TokenTransfer}; -use common::chain::{FrozenTokensValidationVersion, RewardDistributionVersion, UtxoOutPoint}; +use common::chain::UtxoOutPoint; use common::primitives::{id, BlockHeight, Id}; use common::{ chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, TokenData, TokenId}, - ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - HtlcActivated, OrdersActivated, OutPointSourceId, TokenIssuanceVersion, TokensFeeVersion, - TxInput, TxOutput, + Destination, OutPointSourceId, TokenIssuanceVersion, TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -46,6 +44,8 @@ use test_utils::{ }; use tx_verifier::CheckTransactionError; +use super::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramework { TestFramework::builder(rng) .with_chain_config( @@ -53,16 +53,9 @@ fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramewor .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), )]) .unwrap(), ) @@ -961,16 +954,9 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), )]) .unwrap(), ) @@ -1028,29 +1014,15 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { common::chain::NetUpgrades::initialize(vec![ ( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), ), ( BlockHeight::new(2), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), ), ]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index 7d9142a1f..171ec98c5 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -53,7 +53,10 @@ use tx_verifier::{ CheckTransactionError, }; -use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; +use crate::tests::helpers::{ + chainstate_upgrade_builder::ChainstateUpgradeBuilder, issue_token_from_block, + mint_tokens_in_block, +}; fn make_issuance( rng: &mut impl Rng, @@ -6066,29 +6069,19 @@ fn test_change_metadata_uri_activation(#[case] seed: Seed) { common::chain::NetUpgrades::initialize(vec![ ( BlockHeight::zero(), - common::chain::ChainstateUpgrade::new( - common::chain::TokenIssuanceVersion::V1, - common::chain::RewardDistributionVersion::V1, - common::chain::TokensFeeVersion::V1, - common::chain::DataDepositFeeVersion::V1, - common::chain::ChangeTokenMetadataUriActivated::No, - common::chain::FrozenTokensValidationVersion::V1, - common::chain::HtlcActivated::Yes, - common::chain::OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .change_token_metadata_uri_activated( + common::chain::ChangeTokenMetadataUriActivated::No, + ) + .build(), ), ( BlockHeight::new(3), - common::chain::ChainstateUpgrade::new( - common::chain::TokenIssuanceVersion::V1, - common::chain::RewardDistributionVersion::V1, - common::chain::TokensFeeVersion::V1, - common::chain::DataDepositFeeVersion::V1, - common::chain::ChangeTokenMetadataUriActivated::Yes, - common::chain::FrozenTokensValidationVersion::V1, - common::chain::HtlcActivated::Yes, - common::chain::OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .change_token_metadata_uri_activated( + common::chain::ChangeTokenMetadataUriActivated::Yes, + ) + .build(), ), ]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/helpers/chainstate_upgrade_builder.rs b/chainstate/test-suite/src/tests/helpers/chainstate_upgrade_builder.rs new file mode 100644 index 000000000..4d7a461f0 --- /dev/null +++ b/chainstate/test-suite/src/tests/helpers/chainstate_upgrade_builder.rs @@ -0,0 +1,80 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::chain::{ + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct ChainstateUpgradeBuilder { + token_issuance_version: TokenIssuanceVersion, + reward_distribution_version: RewardDistributionVersion, + tokens_fee_version: TokensFeeVersion, + data_deposit_fee_version: DataDepositFeeVersion, + change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated, + frozen_tokens_validation_version: FrozenTokensValidationVersion, + htlc_activated: HtlcActivated, + orders_activated: OrdersActivated, + orders_version: OrdersVersion, +} + +macro_rules! builder_method { + ($name:ident: $type:ty) => { + #[doc = concat!("Set the `", stringify!($name), "` field.")] + #[must_use = "ChainstateUpgradeBuilder dropped prematurely"] + pub fn $name(mut self, $name: $type) -> Self { + self.$name = $name; + self + } + }; +} + +impl ChainstateUpgradeBuilder { + pub fn latest() -> Self { + Self { + token_issuance_version: TokenIssuanceVersion::V1, + reward_distribution_version: RewardDistributionVersion::V1, + tokens_fee_version: TokensFeeVersion::V1, + data_deposit_fee_version: DataDepositFeeVersion::V1, + change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated::Yes, + frozen_tokens_validation_version: FrozenTokensValidationVersion::V1, + htlc_activated: HtlcActivated::Yes, + orders_activated: OrdersActivated::Yes, + orders_version: OrdersVersion::V1, + } + } + + pub fn build(self) -> ChainstateUpgrade { + ChainstateUpgrade::new( + self.token_issuance_version, + self.reward_distribution_version, + self.tokens_fee_version, + self.data_deposit_fee_version, + self.change_token_metadata_uri_activated, + self.frozen_tokens_validation_version, + self.htlc_activated, + self.orders_activated, + self.orders_version, + ) + } + + builder_method!(token_issuance_version: TokenIssuanceVersion); + builder_method!(change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated); + builder_method!(htlc_activated: HtlcActivated); + builder_method!(orders_activated: OrdersActivated); + builder_method!(orders_version: OrdersVersion); +} diff --git a/chainstate/test-suite/src/tests/helpers/mod.rs b/chainstate/test-suite/src/tests/helpers/mod.rs index 8dee793b5..2fc5f8792 100644 --- a/chainstate/test-suite/src/tests/helpers/mod.rs +++ b/chainstate/test-suite/src/tests/helpers/mod.rs @@ -34,6 +34,7 @@ use randomness::{CryptoRng, Rng}; pub mod block_creation_helpers; pub mod block_index_handle_impl; pub mod block_status_helpers; +pub mod chainstate_upgrade_builder; pub mod in_memory_storage_wrapper; pub mod pos; diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index dd7d51849..51c0c0002 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -38,10 +38,8 @@ use common::{ signed_transaction::SignedTransaction, timelock::OutputTimeLock, tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, - AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, - ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + AccountCommand, AccountNonce, ChainConfig, Destination, HtlcActivated, + TokenIssuanceVersion, TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -54,6 +52,8 @@ use tx_verifier::{ input_check::HashlockError, }; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + struct TestFixture { alice_sk: PrivateKey, bob_sk: PrivateKey, @@ -580,29 +580,15 @@ fn fork_activation(#[case] seed: Seed) { common::chain::NetUpgrades::initialize(vec![ ( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::No, - OrdersActivated::No, - ), + ChainstateUpgradeBuilder::latest() + .htlc_activated(HtlcActivated::No) + .build(), ), ( BlockHeight::new(2), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::No, - ), + ChainstateUpgradeBuilder::latest() + .htlc_activated(HtlcActivated::Yes) + .build(), ), ]) .unwrap(), @@ -685,29 +671,15 @@ fn spend_tokens(#[case] seed: Seed) { common::chain::NetUpgrades::initialize(vec![ ( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), ), ( BlockHeight::new(2), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), ), ]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/nft_burn.rs b/chainstate/test-suite/src/tests/nft_burn.rs index 5ab906c8b..17159360b 100644 --- a/chainstate/test-suite/src/tests/nft_burn.rs +++ b/chainstate/test-suite/src/tests/nft_burn.rs @@ -17,11 +17,9 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::make_token_id, - ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - HtlcActivated, OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, + Destination, TokenIssuanceVersion, TxInput, TxOutput, }; -use common::chain::{FrozenTokensValidationVersion, OutPointSourceId, UtxoOutPoint}; +use common::chain::{OutPointSourceId, UtxoOutPoint}; use common::primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}; use randomness::Rng; use rstest::rstest; @@ -30,6 +28,8 @@ use test_utils::{ random::{make_seedable_rng, Seed}, }; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -212,16 +212,9 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), )]) .unwrap(), ) diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index 09de0cc69..af8c1accc 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -22,9 +22,7 @@ use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{is_rfc3986_valid_symbol, make_token_id, Metadata, NftIssuance, NftIssuanceV0}, - Block, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OutPointSourceId, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + Block, Destination, OutPointSourceId, TokenIssuanceVersion, TxInput, TxOutput, }; use common::primitives::{BlockHeight, Idable}; use randomness::{CryptoRng, Rng}; @@ -38,6 +36,8 @@ use test_utils::{ }; use tx_verifier::{error::TokenIssuanceError, CheckTransactionError}; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -1648,16 +1648,9 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), )]) .unwrap(), ) @@ -1715,16 +1708,9 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), )]) .unwrap(), ) diff --git a/chainstate/test-suite/src/tests/nft_transfer.rs b/chainstate/test-suite/src/tests/nft_transfer.rs index 40581fe00..6c2e77f43 100644 --- a/chainstate/test-suite/src/tests/nft_transfer.rs +++ b/chainstate/test-suite/src/tests/nft_transfer.rs @@ -15,24 +15,22 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{get_output_value, TestFramework, TransactionBuilder}; -use common::primitives::Idable; use common::{ chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, NftIssuance, TokenId}, - ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, - FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, - OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, - TxInput, TxOutput, + Destination, NetUpgrades, OutPointSourceId, TokenIssuanceVersion, TxInput, TxOutput, }, - primitives::{Amount, BlockHeight, CoinOrTokenId}, + primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}, }; use randomness::Rng; use rstest::rstest; use test_utils::nft_utils::random_nft_issuance; use test_utils::random::{make_seedable_rng, Seed}; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -367,16 +365,9 @@ fn ensure_nft_cannot_be_printed_from_tokens_op(#[case] seed: Seed) { .chainstate_upgrades( NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V1) + .build(), )]) .unwrap(), ) diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index ce85dd210..d29048d95 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use chainstate::ConnectTransactionError; +use chainstate::{CheckBlockTransactionsError, ConnectTransactionError}; use chainstate_storage::Transactional; use chainstate_test_framework::{output_value_amount, TestFramework, TransactionBuilder}; use common::{ @@ -29,14 +29,14 @@ use common::{ make_token_id, IsTokenFreezable, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, }, - AccountCommand, AccountNonce, ChainstateUpgrade, Destination, OrderData, SignedTransaction, - TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, Destination, OrderAccountCommand, OrderData, OrdersVersion, + SignedTransaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}, }; use crypto::key::{KeyKind, PrivateKey}; use orders_accounting::OrdersAccountingDB; -use randomness::{CryptoRng, Rng}; +use randomness::{CryptoRng, Rng, SliceRandom}; use rstest::rstest; use test_utils::{ nft_utils::random_nft_issuance, @@ -45,7 +45,29 @@ use test_utils::{ }; use tx_verifier::error::{InputCheckError, ScriptError}; -use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; +use crate::tests::helpers::{ + chainstate_upgrade_builder::ChainstateUpgradeBuilder, issue_token_from_block, + mint_tokens_in_block, +}; + +fn create_test_framework_with_orders( + rng: &mut (impl Rng + CryptoRng), + orders_version: OrdersVersion, +) -> TestFramework { + TestFramework::builder(rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest().orders_version(orders_version).build(), + )]) + .unwrap(), + ) + .build(), + ) + .build() +} fn issue_and_mint_token_from_genesis( rng: &mut (impl Rng + CryptoRng), @@ -169,22 +191,21 @@ fn create_two_same_orders_in_tx(#[case] seed: Seed) { OutputValue::TokenV1(token_id, give_amount), )); - let order_id = make_order_id(&tokens_outpoint); let tx = TransactionBuilder::new() .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) .add_output(TxOutput::CreateOrder(order_data.clone())) .add_output(TxOutput::CreateOrder(order_data)) .build(); + let tx_id = tx.transaction().get_id(); let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); assert_eq!( result.unwrap_err(), chainstate::ChainstateError::ProcessBlockError( - chainstate::BlockError::StateUpdateFailed( - chainstate::ConnectTransactionError::OrdersAccountingError( - orders_accounting::Error::OrderAlreadyExists(order_id) - ) - ) + chainstate::BlockError::StateUpdateFailed(ConnectTransactionError::IOPolicyError( + chainstate::IOPolicyError::MultipleOrdersCreated, + tx_id.into() + )) ) ); }); @@ -218,22 +239,21 @@ fn create_two_orders_same_tx(#[case] seed: Seed) { OutputValue::TokenV1(token_id, half_tokens_circulating_supply), ); - let order_id = make_order_id(&tokens_outpoint); let tx = TransactionBuilder::new() .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) .add_output(TxOutput::CreateOrder(Box::new(order_data_1))) .add_output(TxOutput::CreateOrder(Box::new(order_data_2))) .build(); + let tx_id = tx.transaction().get_id(); let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); assert_eq!( result.unwrap_err(), chainstate::ChainstateError::ProcessBlockError( - chainstate::BlockError::StateUpdateFailed( - chainstate::ConnectTransactionError::OrdersAccountingError( - orders_accounting::Error::OrderAlreadyExists(order_id) - ) - ) + chainstate::BlockError::StateUpdateFailed(ConnectTransactionError::IOPolicyError( + chainstate::IOPolicyError::MultipleOrdersCreated, + tx_id.into() + )) ) ); }); @@ -430,11 +450,13 @@ fn create_order_tokens_for_tokens(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn conclude_order_check_storage(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn conclude_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); let tokens_circulating_supply = @@ -456,14 +478,17 @@ fn conclude_order_check_storage(#[case] seed: Seed) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let tx_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(tx_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -485,11 +510,13 @@ fn conclude_order_check_storage(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn conclude_order_multiple_txs(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn conclude_order_multiple_txs(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); let tokens_circulating_supply = @@ -511,27 +538,34 @@ fn conclude_order_multiple_txs(#[case] seed: Seed) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let tx_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx1 = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(tx_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, )) .build(); + + let tx_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx2 = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(tx_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -539,32 +573,49 @@ fn conclude_order_multiple_txs(#[case] seed: Seed) { .build(); let tx2_id = tx2.transaction().get_id(); - let res = tf - .make_block_builder() - .with_transactions(vec![tx1, tx2]) - .build_and_process(&mut rng); - - assert_eq!( - res.unwrap_err(), - chainstate::ChainstateError::ProcessBlockError( - chainstate::BlockError::StateUpdateFailed( - ConnectTransactionError::ConstrainedValueAccumulatorError( - orders_accounting::Error::OrderDataNotFound(order_id).into(), - tx2_id.into() + let block = tf.make_block_builder().with_transactions(vec![tx1, tx2]).build(&mut rng); + let block_id = block.get_id(); + let res = tf.process_block(block, chainstate::BlockSource::Local); + + match version { + OrdersVersion::V0 => { + assert_eq!( + res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + orders_accounting::Error::OrderDataNotFound(order_id).into(), + tx2_id.into() + ) + ) ) - ) - ) - ); + ); + } + OrdersVersion::V1 => { + assert_eq!( + res.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + CheckBlockTransactionsError::DuplicateInputInBlock(block_id) + ) + ) + ) + ); + } + } }); } #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_check_storage(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); @@ -592,19 +643,25 @@ fn fill_order_check_storage(#[case] seed: Seed) { let filled_amount = { let db_tx = tf.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount).unwrap() + orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount, version) + .unwrap() }; let left_to_fill = (ask_amount - fill_amount).unwrap(); + let fill_order_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_order_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -634,7 +691,20 @@ fn fill_order_check_storage(#[case] seed: Seed) { let filled_amount = { let db_tx = tf.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, left_to_fill).unwrap() + orders_accounting::calculate_fill_order(&orders_db, order_id, left_to_fill, version) + .unwrap() + }; + + let fill_order_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::FillOrder(order_id, left_to_fill, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + left_to_fill, + Destination::AnyoneCanSpend, + )), }; let tx = TransactionBuilder::new() @@ -642,13 +712,7 @@ fn fill_order_check_storage(#[case] seed: Seed) { UtxoOutPoint::new(partial_fill_tx_id.into(), 1).into(), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::FillOrder(order_id, left_to_fill, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_order_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -664,20 +728,38 @@ fn fill_order_check_storage(#[case] seed: Seed) { None, tf.chainstate.get_order_ask_balance(&order_id).unwrap() ); - assert_eq!( - None, - tf.chainstate.get_order_give_balance(&order_id).unwrap() - ); + match version { + OrdersVersion::V0 => { + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + } + OrdersVersion::V1 => { + let filled1 = + (give_amount.into_atoms() * fill_amount.into_atoms()) / ask_amount.into_atoms(); + let filled2 = (give_amount.into_atoms() * left_to_fill.into_atoms()) + / ask_amount.into_atoms(); + let remainder = (give_amount - Amount::from_atoms(filled1 + filled2)) + .filter(|v| *v != Amount::ZERO); + assert_eq!( + remainder, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + } + } }); } #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_partially_then_conclude(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_partially_then_conclude(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); @@ -705,18 +787,24 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { let filled_amount = { let db_tx = tf.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount).unwrap() + orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount, version) + .unwrap() }; + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -726,14 +814,17 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { { // Try overspend give in conclude order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1( token_id, @@ -764,14 +855,17 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { { // Try overspend ask in conclude order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, (give_amount - filled_amount).unwrap()), Destination::AnyoneCanSpend, @@ -796,14 +890,17 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { } // conclude the order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, (give_amount - filled_amount).unwrap()), Destination::AnyoneCanSpend, @@ -827,18 +924,23 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { { // Try filling concluded order - let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(2), - AccountCommand::FillOrder( - order_id, - (give_amount - filled_amount).unwrap(), - Destination::AnyoneCanSpend, - ), + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(2), + AccountCommand::FillOrder( + order_id, + (give_amount - filled_amount).unwrap(), + Destination::AnyoneCanSpend, ), - InputWitness::NoSignature(None), - ) + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + (give_amount - filled_amount).unwrap(), + Destination::AnyoneCanSpend, + )), + }; + let tx = TransactionBuilder::new() + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -865,18 +967,20 @@ fn fill_partially_then_conclude(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn try_overbid_order_in_multiple_txs(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn try_overbid_order_in_multiple_txs(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); let tokens_circulating_supply = tf.chainstate.get_token_circulating_supply(&token_id).unwrap().unwrap(); - let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let ask_amount = Amount::from_atoms(rng.gen_range(2u128..1000)); let give_amount = Amount::from_atoms(rng.gen_range(1u128..=tokens_circulating_supply.into_atoms())); let order_data = OrderData::new( @@ -902,36 +1006,50 @@ fn try_overbid_order_in_multiple_txs(#[case] seed: Seed) { let tx_id = tx.transaction().get_id(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let tx1 = TransactionBuilder::new() .add_input( TxInput::from_utxo(tx_id.into(), 1), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, )) .build(); + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::FillOrder( + order_id, + Amount::from_atoms(1), + Destination::AnyoneCanSpend, + ), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + Amount::from_atoms(1), + Destination::AnyoneCanSpend, + )), + }; let tx2 = TransactionBuilder::new() .add_input( TxInput::from_utxo(tx_id.into(), 2), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -949,8 +1067,12 @@ fn try_overbid_order_in_multiple_txs(#[case] seed: Seed) { chainstate::ChainstateError::ProcessBlockError( chainstate::BlockError::StateUpdateFailed( ConnectTransactionError::ConstrainedValueAccumulatorError( - orders_accounting::Error::OrderOverbid(order_id, Amount::ZERO, ask_amount) - .into(), + orders_accounting::Error::OrderOverbid( + order_id, + Amount::ZERO, + Amount::from_atoms(1) + ) + .into(), tx2_id.into() ) ) @@ -961,11 +1083,13 @@ fn try_overbid_order_in_multiple_txs(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_completely_then_conclude(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_completely_then_conclude(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); @@ -990,22 +1114,23 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { { // Try overspend complete fill order + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input( coins_outpoint.clone().into(), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - ask_amount, - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, (give_amount + Amount::from_atoms(1)).unwrap()), Destination::AnyoneCanSpend, @@ -1027,22 +1152,27 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { { // Try overbid complete fill order + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + (ask_amount + Amount::from_atoms(1)).unwrap(), + Destination::AnyoneCanSpend, + ), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + (ask_amount + Amount::from_atoms(1)).unwrap(), + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input( coins_outpoint.clone().into(), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - (ask_amount + Amount::from_atoms(1)).unwrap(), - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -1069,15 +1199,20 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { } // Fill the order completely + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -1087,14 +1222,17 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { { // Try overspend conclude order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::Coin((ask_amount + Amount::from_atoms(1)).unwrap()), Destination::AnyoneCanSpend, @@ -1115,14 +1253,17 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { } // conclude the order + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::Coin(ask_amount), Destination::AnyoneCanSpend, @@ -1144,11 +1285,13 @@ fn fill_completely_then_conclude(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn conclude_order_check_signature(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn conclude_order_check_signature(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (order_sk, order_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); @@ -1174,14 +1317,17 @@ fn conclude_order_check_signature(#[case] seed: Seed) { // try conclude without signature { + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -1204,14 +1350,17 @@ fn conclude_order_check_signature(#[case] seed: Seed) { // try conclude with wrong signature { + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -1256,14 +1405,17 @@ fn conclude_order_check_signature(#[case] seed: Seed) { } // valid case + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; let tx = TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, give_amount), Destination::AnyoneCanSpend, @@ -1296,11 +1448,13 @@ fn conclude_order_check_signature(#[case] seed: Seed) { // Reorg from a point before the order was created, so that after reorg storage has no information on the order #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn reorg_before_create(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn reorg_before_create(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); @@ -1329,19 +1483,25 @@ fn reorg_before_create(#[case] seed: Seed) { let filled_amount = { let db_tx = tf.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount).unwrap() + orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount, version) + .unwrap() }; let left_to_fill = (ask_amount - fill_amount).unwrap(); + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -1386,11 +1546,13 @@ fn reorg_before_create(#[case] seed: Seed) { // Reorg from a point after the order was created, so that after reorg storage has original information on the order #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn reorg_after_create(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn reorg_after_create(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let (token_id, tokens_outpoint, coins_outpoint) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); @@ -1424,25 +1586,27 @@ fn reorg_after_create(#[case] seed: Seed) { let filled_amount = { let db_tx = tf.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount).unwrap() + orders_accounting::calculate_fill_order(&orders_db, order_id, fill_amount, version) + .unwrap() }; let left_to_fill = (ask_amount - fill_amount).unwrap(); + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + Destination::AnyoneCanSpend, + )), + }; tf.make_block_builder() .add_transaction( TransactionBuilder::new() .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - fill_amount, - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, filled_amount), Destination::AnyoneCanSpend, @@ -1456,16 +1620,19 @@ fn reorg_after_create(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(1), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; tf.make_block_builder() .add_transaction( TransactionBuilder::new() - .add_input( - TxInput::AccountCommand( - AccountNonce::new(1), - AccountCommand::ConcludeOrder(order_id), - ), - InputWitness::NoSignature(None), - ) + .add_input(conclude_input, InputWitness::NoSignature(None)) .build(), ) .build_and_process(&mut rng) @@ -1514,29 +1681,15 @@ fn test_activation(#[case] seed: Seed) { common::chain::NetUpgrades::initialize(vec![ ( BlockHeight::zero(), - ChainstateUpgrade::new( - common::chain::TokenIssuanceVersion::V1, - common::chain::RewardDistributionVersion::V1, - common::chain::TokensFeeVersion::V1, - common::chain::DataDepositFeeVersion::V1, - common::chain::ChangeTokenMetadataUriActivated::Yes, - common::chain::FrozenTokensValidationVersion::V1, - common::chain::HtlcActivated::No, - common::chain::OrdersActivated::No, - ), + ChainstateUpgradeBuilder::latest() + .orders_activated(common::chain::OrdersActivated::No) + .build(), ), ( BlockHeight::new(4), - ChainstateUpgrade::new( - common::chain::TokenIssuanceVersion::V1, - common::chain::RewardDistributionVersion::V1, - common::chain::TokensFeeVersion::V1, - common::chain::DataDepositFeeVersion::V1, - common::chain::ChangeTokenMetadataUriActivated::Yes, - common::chain::FrozenTokensValidationVersion::V1, - common::chain::HtlcActivated::No, - common::chain::OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .orders_activated(common::chain::OrdersActivated::Yes) + .build(), ), ]) .unwrap(), @@ -1601,11 +1754,13 @@ fn test_activation(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn create_order_with_nft(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn create_order_with_nft(#[case] seed: Seed, #[case] version: OrdersVersion) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = create_test_framework_with_orders(&mut rng, version); let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); let token_id = make_token_id(&[genesis_input.clone()]).unwrap(); @@ -1670,22 +1825,23 @@ fn create_order_with_nft(#[case] seed: Seed) { // Try get 2 nfts out of order { + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(issue_nft_tx_id.into(), 1), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - ask_amount, - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, Amount::from_atoms(2)), Destination::AnyoneCanSpend, @@ -1706,6 +1862,17 @@ fn create_order_with_nft(#[case] seed: Seed) { } // Fill order + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, ask_amount, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + }; tf.make_block_builder() .add_transaction( TransactionBuilder::new() @@ -1713,17 +1880,7 @@ fn create_order_with_nft(#[case] seed: Seed) { TxInput::from_utxo(issue_nft_tx_id.into(), 1), InputWitness::NoSignature(None), ) - .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - ask_amount, - Destination::AnyoneCanSpend, - ), - ), - InputWitness::NoSignature(None), - ) + .add_input(fill_input, InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::TokenV1(token_id, Amount::from_atoms(1)), Destination::AnyoneCanSpend, @@ -1751,10 +1908,24 @@ fn create_order_with_nft(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn partially_fill_order_with_nft(#[case] seed: Seed) { +fn partially_fill_order_with_nft_v0(#[case] seed: Seed) { utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); - let mut tf = TestFramework::builder(&mut rng).build(); + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V0) + .build(), + )]) + .unwrap(), + ) + .build(), + ) + .build(); let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); let token_id = make_token_id(&[genesis_input.clone()]).unwrap(); @@ -1899,7 +2070,7 @@ fn partially_fill_order_with_nft(#[case] seed: Seed) { tf.chainstate.get_order_give_balance(&order_id).unwrap() ); - // Fill order and receive 1 nft for 1 atom + // Fill order only with proper amount spent tf.make_block_builder() .add_transaction( TransactionBuilder::new() @@ -1941,3 +2112,695 @@ fn partially_fill_order_with_nft(#[case] seed: Seed) { ); }); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn partially_fill_order_with_nft_v1(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V1) + .build(), + )]) + .unwrap(), + ) + .build(), + ) + .build(); + + let genesis_input = TxInput::from_utxo(tf.genesis().get_id().into(), 0); + let token_id = make_token_id(&[genesis_input.clone()]).unwrap(); + let nft_issuance = random_nft_issuance(tf.chain_config(), &mut rng); + let token_min_issuance_fee = + tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); + + let ask_amount = Amount::from_atoms(rng.gen_range(10u128..1000)); + + // Issue an NFT + let issue_nft_tx = TransactionBuilder::new() + .add_input(genesis_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new(nft_issuance.into()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(ask_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) + .build(); + let issue_nft_tx_id = issue_nft_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(issue_nft_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Create order selling NFT for coins + let give_amount = Amount::from_atoms(1); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let nft_outpoint = UtxoOutPoint::new(issue_nft_tx_id.into(), 0); + let order_id = make_order_id(&nft_outpoint); + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(nft_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + assert_eq!( + Some(order_data.clone()), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(ask_amount), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(give_amount), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + + // Try to get nft by filling order with 1 atom less, getting 0 nfts + { + let underbid_amount = (ask_amount - Amount::from_atoms(1)).unwrap(); + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(issue_nft_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + underbid_amount, + Destination::AnyoneCanSpend, + )), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(0)), + Destination::AnyoneCanSpend, + )) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::StateUpdateFailed( + ConnectTransactionError::ConstrainedValueAccumulatorError( + orders_accounting::Error::OrderUnderbid(order_id, underbid_amount) + .into(), + tx_id.into() + ) + ) + ) + ); + } + + // Fill order with proper fill and receive 1 nft + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(issue_nft_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + ask_amount, + Destination::AnyoneCanSpend, + )), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + assert_eq!( + Some(order_data), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_with_zero(#[case] seed: Seed, #[case] version: OrdersVersion) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = create_test_framework_with_orders(&mut rng, version); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + let tokens_circulating_supply = + tf.chainstate.get_token_circulating_supply(&token_id).unwrap().unwrap(); + + let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); + let give_amount = + Amount::from_atoms(rng.gen_range(1u128..=tokens_circulating_supply.into_atoms())); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Fill the order with 0 amount + let fill_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, Amount::ZERO, Destination::AnyoneCanSpend), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + Amount::ZERO, + Destination::AnyoneCanSpend, + )), + }; + let tx = TransactionBuilder::new() + .add_input(fill_input, InputWitness::NoSignature(None)) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + match version { + OrdersVersion::V0 => { + // Check that order has not changed except nonce + assert!(result.is_ok()); + assert_eq!( + Some(AccountNonce::new(0)), + tf.chainstate + .get_account_nonce_count(common::chain::AccountType::Order(order_id)) + .unwrap() + ); + assert_eq!( + Some(order_data), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + Some(ask_amount), + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(give_amount), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + } + OrdersVersion::V1 => { + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::AttemptToFillOrderWithZero( + order_id, tx_id + ) + ) + ) + ) + ) + ); + } + } + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy(), vec![108, 56, 65, 38, 217, 22, 244, 28, 38, 184])] +fn fill_orders_shuffle(#[case] seed: Seed, #[case] fills: Vec) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let mut fill_order_atoms = fills.clone(); + fill_order_atoms.shuffle(&mut rng); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(1000); + let give_amount = Amount::from_atoms(1001); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + assert_eq!(ask_amount.into_atoms(), fill_order_atoms.iter().sum()); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order_data.clone()))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Create a tx with utxos per fill + let mut tx_builder = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)); + for to_fill in &fill_order_atoms { + tx_builder = tx_builder.add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(*to_fill)), + Destination::AnyoneCanSpend, + )); + } + let tx_with_coins_to_fill = tx_builder.build(); + let tx_with_coins_to_fill_id = tx_with_coins_to_fill.transaction().get_id(); + tf.make_block_builder() + .add_transaction(tx_with_coins_to_fill) + .build_and_process(&mut rng) + .unwrap(); + + let mut fill_txs = Vec::new(); + for (i, fill_atoms) in fill_order_atoms.iter().enumerate() { + // Destination of fill order must be unique to avoid duplicating inputs + let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_with_coins_to_fill_id.into(), i as u32), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + Amount::from_atoms(*fill_atoms), + Destination::PublicKey(pk), + )), + InputWitness::NoSignature(None), + ) + // ignore outputs for simplicity + .build(); + fill_txs.push(tx); + } + + tf.make_block_builder() + .with_transactions(fill_txs) + .build_and_process(&mut rng) + .unwrap(); + + assert_eq!( + Some(order_data.clone()), + tf.chainstate.get_order_data(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Some(Amount::from_atoms(1)), + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn orders_v1_activation(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + // activate orders v1 at height 5 (genesis + issue token block + mint block + create order block + empty block) + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V0) + .build(), + ), + ( + BlockHeight::new(5), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + + let (token_id, tokens_outpoint, _) = issue_and_mint_token_from_genesis(&mut rng, &mut tf); + let tokens_circulating_supply = + tf.chainstate.get_token_circulating_supply(&token_id).unwrap().unwrap(); + + let order_id = make_order_id(&tokens_outpoint); + let order_data = Box::new(OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::from_atoms(rng.gen_range(1u128..1000))), + OutputValue::TokenV1( + token_id, + Amount::from_atoms(rng.gen_range(1u128..=tokens_circulating_supply.into_atoms())), + ), + )); + + // Create an order + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(order_data)) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + // Try to fill order before activation, check an error + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + Amount::ZERO, + Destination::AnyoneCanSpend, + )), + InputWitness::NoSignature(None), + ) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::OrdersV1AreNotActivated(tx_id) + ) + ) + ) + ) + ); + } + + // Try to conclude order before activation, check an error + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)), + InputWitness::NoSignature(None), + ) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::OrdersV1AreNotActivated(tx_id) + ) + ) + ) + ) + ); + } + + // produce an empty block and activate fork + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + + // Try to fill order with deprecated command, check an error + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder( + order_id, + Amount::ZERO, + Destination::AnyoneCanSpend, + ), + ), + InputWitness::NoSignature(None), + ) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::DeprecatedOrdersCommands(tx_id) + ) + ) + ) + ) + ); + } + + // Try to conclude order before activation, check an error + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + InputWitness::NoSignature(None), + ) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + chainstate::ChainstateError::ProcessBlockError( + chainstate::BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::DeprecatedOrdersCommands(tx_id) + ) + ) + ) + ) + ); + } + + // now it should be possible to use OrderAccountCommand + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +// Create an order, fill it partially. +// Activate Orders V1 fork. +// Fill partially again and conclude. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn create_order_fill_activate_fork_fill_conclude(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + // activate orders at height 5 (genesis + issue token block + mint + create order + fill) + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V0) + .build(), + ), + ( + BlockHeight::new(5), + ChainstateUpgradeBuilder::latest() + .orders_version(OrdersVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + + let (token_id, tokens_outpoint, coins_outpoint) = + issue_and_mint_token_from_genesis(&mut rng, &mut tf); + + let ask_amount = Amount::from_atoms(1000); + let give_amount = tf.chainstate.get_token_circulating_supply(&token_id).unwrap().unwrap(); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(ask_amount), + OutputValue::TokenV1(token_id, give_amount), + ); + + let order_id = make_order_id(&tokens_outpoint); + let tx = TransactionBuilder::new() + .add_input(tokens_outpoint.into(), InputWitness::NoSignature(None)) + .add_output(TxOutput::CreateOrder(Box::new(order_data))) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + // Fill the order partially + let fill_amount = Amount::from_atoms(100); + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order( + &orders_db, + order_id, + fill_amount, + OrdersVersion::V0, + ) + .unwrap() + }; + + let fill_tx_1 = TransactionBuilder::new() + .add_input(coins_outpoint.into(), InputWitness::NoSignature(None)) + .add_input( + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, fill_amount, Destination::AnyoneCanSpend), + ), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(fill_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let fill_tx_1_id = fill_tx_1.transaction().get_id(); + tf.make_block_builder() + .add_transaction(fill_tx_1) + .build_and_process(&mut rng) + .unwrap(); + + // Next block should activate orders V1 + assert_eq!(BlockHeight::new(4), tf.best_block_index().block_height()); + + // Fill again now with V1 + let filled_amount = { + let db_tx = tf.storage.transaction_ro().unwrap(); + let orders_db = OrdersAccountingDB::new(&db_tx); + orders_accounting::calculate_fill_order( + &orders_db, + order_id, + fill_amount, + OrdersVersion::V1, + ) + .unwrap() + }; + + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(fill_tx_1_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + Destination::AnyoneCanSpend, + )), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_id, filled_amount), + Destination::AnyoneCanSpend, + )) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + // Conclude the order + let tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1( + token_id, + (give_amount - filled_amount).and_then(|v| v - filled_amount).unwrap(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin((fill_amount * 2).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + + assert_eq!(None, tf.chainstate.get_order_data(&order_id).unwrap()); + assert_eq!( + None, + tf.chainstate.get_order_ask_balance(&order_id).unwrap() + ); + assert_eq!( + None, + tf.chainstate.get_order_give_balance(&order_id).unwrap() + ); + }); +} diff --git a/chainstate/test-suite/src/tests/tx_fee.rs b/chainstate/test-suite/src/tests/tx_fee.rs index a4c824ac1..5eeddc452 100644 --- a/chainstate/test-suite/src/tests/tx_fee.rs +++ b/chainstate/test-suite/src/tests/tx_fee.rs @@ -20,22 +20,17 @@ use chainstate_test_framework::{ create_stake_pool_data_with_all_reward_to_staker, empty_witness, TestFramework, TestStore, TransactionBuilder, }; -use common::chain::{ - config::create_unit_test_config, AccountCommand, AccountNonce, AccountSpending, - RewardDistributionVersion, -}; use common::{ chain::{ - config::ChainType, + config::{create_unit_test_config, ChainType}, output_value::OutputValue, timelock::OutputTimeLock, tokens::{ make_token_id, IsTokenFreezable, TokenIssuance, TokenIssuanceV0, TokenIssuanceV1, TokenTotalSupply, }, - ChainConfig, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, - Destination, FrozenTokensValidationVersion, HtlcActivated, NetUpgrades, OrdersActivated, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountSpending, ChainConfig, Destination, NetUpgrades, + TokenIssuanceVersion, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Fee, Idable}, }; @@ -44,6 +39,8 @@ use randomness::CryptoRng; use test_utils::random_ascii_alphanumeric_string; use tx_verifier::transaction_verifier::{TransactionSourceForConnect, TransactionVerifier}; +use crate::tests::helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; + fn setup(rng: &mut (impl Rng + CryptoRng)) -> (ChainConfig, InMemoryStorageWrapper, TestFramework) { let storage = TestStore::new_empty().unwrap(); @@ -573,16 +570,9 @@ fn issue_fungible_token_v0(#[case] seed: Seed) { .chainstate_upgrades( common::chain::NetUpgrades::initialize(vec![( BlockHeight::zero(), - ChainstateUpgrade::new( - TokenIssuanceVersion::V0, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, - DataDepositFeeVersion::V1, - ChangeTokenMetadataUriActivated::Yes, - FrozenTokensValidationVersion::V1, - HtlcActivated::Yes, - OrdersActivated::Yes, - ), + ChainstateUpgradeBuilder::latest() + .token_issuance_version(TokenIssuanceVersion::V0) + .build(), )]) .unwrap(), ) diff --git a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs index 46ba13b42..f1f1d4a7d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs @@ -21,10 +21,11 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{get_tokens_issuance_count, NftIssuance}, - AccountCommand, ChainConfig, ChangeTokenMetadataUriActivated, HtlcActivated, - SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, TxInput, TxOutput, + AccountCommand, ChainConfig, ChangeTokenMetadataUriActivated, HtlcActivated, OrderId, + OrdersVersion, SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, + TxInput, TxOutput, }, - primitives::{BlockHeight, CoinOrTokenId, Id, Idable}, + primitives::{Amount, BlockHeight, CoinOrTokenId, Id, Idable}, }; use thiserror::Error; use utils::ensure; @@ -60,10 +61,16 @@ pub enum CheckTransactionError { HtlcsAreNotActivated, #[error("Orders from tx {0} are not yet activated")] OrdersAreNotActivated(Id), + #[error("Orders V1 from tx {0} are not yet activated")] + OrdersV1AreNotActivated(Id), + #[error("Order inputs from tx {0} are deprecated")] + DeprecatedOrdersCommands(Id), #[error("Orders currencies from tx {0} are the same")] OrdersCurrenciesMustBeDifferent(Id), #[error("Change token metadata uri not activated yet")] ChangeTokenMetadataUriNotActivated, + #[error("Cannot fill order {0} with zero amount in tx {1}")] + AttemptToFillOrderWithZero(OrderId, Id), } pub fn check_transaction( @@ -78,7 +85,7 @@ pub fn check_transaction( check_no_signature_size(chain_config, tx)?; check_data_deposit_outputs(chain_config, block_height, tx)?; check_htlc_outputs(chain_config, block_height, tx)?; - check_order_outputs(chain_config, block_height, tx)?; + check_order_inputs_outputs(chain_config, block_height, tx)?; Ok(()) } @@ -209,7 +216,7 @@ fn check_tokens_tx( // Check token metadata uri change tx.inputs().iter().try_for_each(|input| match input { - TxInput::Utxo(_) | TxInput::Account(_) => Ok(()), + TxInput::Utxo(_) | TxInput::Account(_) | TxInput::OrderAccountCommand(_) => Ok(()), TxInput::AccountCommand(_, command) => match command { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -364,11 +371,66 @@ fn check_htlc_outputs( Ok(()) } -fn check_order_outputs( +fn check_order_inputs_outputs( chain_config: &ChainConfig, block_height: BlockHeight, tx: &SignedTransaction, ) -> Result<(), CheckTransactionError> { + let orders_version = chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .orders_version(); + + for input in tx.inputs() { + match input { + TxInput::Utxo(_) | TxInput::Account(_) => {} + TxInput::AccountCommand(_, account_command) => match account_command { + AccountCommand::MintTokens(..) + | AccountCommand::UnmintTokens(..) + | AccountCommand::LockTokenSupply(..) + | AccountCommand::FreezeToken(..) + | AccountCommand::UnfreezeToken(..) + | AccountCommand::ChangeTokenAuthority(..) + | AccountCommand::ChangeTokenMetadataUri(..) => { /* do nothing */ } + AccountCommand::FillOrder(..) | AccountCommand::ConcludeOrder(..) => { + match orders_version { + OrdersVersion::V0 => {} + OrdersVersion::V1 => { + return Err(CheckTransactionError::DeprecatedOrdersCommands( + tx.transaction().get_id(), + )); + } + }; + } + }, + TxInput::OrderAccountCommand(cmd) => { + match orders_version { + OrdersVersion::V0 => { + return Err(CheckTransactionError::OrdersV1AreNotActivated( + tx.transaction().get_id(), + )); + } + OrdersVersion::V1 => {} + }; + match cmd { + common::chain::OrderAccountCommand::FillOrder(id, fill, _) => { + // Forbidding fills with zero amount ensures that tx has utxo and therefore is unique. + // Unique txs cannot be replayed. + ensure!( + *fill > Amount::ZERO, + CheckTransactionError::AttemptToFillOrderWithZero( + *id, + tx.transaction().get_id() + ) + ); + } + common::chain::OrderAccountCommand::ConcludeOrder { .. } => {} + } + } + } + } + for output in tx.outputs() { match output { TxOutput::Transfer(..) diff --git a/chainstate/tx-verifier/src/transaction_verifier/error.rs b/chainstate/tx-verifier/src/transaction_verifier/error.rs index fefdc08b1..63a99efc8 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/error.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/error.rs @@ -123,6 +123,8 @@ pub enum ConnectTransactionError { OrdersAccountingError(#[from] orders_accounting::Error), #[error(transparent)] InputCheck(#[from] InputCheckError), + #[error("Transaction {0} has conclude order input {1} with amounts that don't match the db")] + ConcludeInputAmountsDontMatch(Id, OrderId), } impl From for ConnectTransactionError { diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs index a34b4e65b..a6f3014f7 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -143,6 +143,7 @@ impl<'a> PerInputData<'a> { } TxInput::Account(outpoint) => InputInfo::Account { outpoint }, TxInput::AccountCommand(_, command) => InputInfo::AccountCommand { command }, + TxInput::OrderAccountCommand(command) => InputInfo::OrderAccountCommand { command }, }; Ok(Self::new(info, witness)) } @@ -400,9 +401,9 @@ impl TimelockContext for InputVerifyContextTim utxo::UtxoSource::Blockchain(height) => Ok(*height), utxo::UtxoSource::Mempool => Ok(self.ctx.spending_height), }, - InputInfo::Account { .. } | InputInfo::AccountCommand { .. } => { - Err(TimelockContextError::TimelockedAccount) - } + InputInfo::Account { .. } + | InputInfo::AccountCommand { .. } + | InputInfo::OrderAccountCommand { .. } => Err(TimelockContextError::TimelockedAccount), } } @@ -431,9 +432,9 @@ impl TimelockContext for InputVerifyContextTim } utxo::UtxoSource::Mempool => Ok(self.ctx.spending_time), }, - InputInfo::Account { .. } | InputInfo::AccountCommand { .. } => { - Err(TimelockContextError::TimelockedAccount) - } + InputInfo::Account { .. } + | InputInfo::AccountCommand { .. } + | InputInfo::OrderAccountCommand { .. } => Err(TimelockContextError::TimelockedAccount), } } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs index 1f4c96c0f..b9e4f8c3d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs @@ -152,6 +152,7 @@ pub fn verify_tx_signature( } TxInput::Account(outpoint) => InputInfo::Account { outpoint }, TxInput::AccountCommand(_, command) => InputInfo::AccountCommand { command }, + TxInput::OrderAccountCommand(command) => InputInfo::OrderAccountCommand { command }, }; let input_witness = tx.signatures()[input_num] .clone() diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs index d1f482d47..41851f347 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs @@ -48,6 +48,8 @@ pub enum IOPolicyError { MultiplePoolCreated, #[error("Attempted to create multiple delegations in a single tx")] MultipleDelegationCreated, + #[error("Attempted to create multiple orders in a single tx")] + MultipleOrdersCreated, #[error("Attempted to produce block in a tx")] ProduceBlockInTx, #[error("Attempted to provide multiple account command inputs in a single tx")] @@ -181,7 +183,9 @@ pub fn check_tx_inputs_outputs_policy( )?; Ok(Some(utxo.take_output())) } - TxInput::Account(..) | TxInput::AccountCommand(..) => Ok(None), + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => Ok(None), }) .collect::, ConnectTransactionError>>()?; @@ -258,7 +262,9 @@ fn collect_inputs_utxos( .iter() .filter_map(|input| match input { TxInput::Utxo(outpoint) => Some(outpoint), - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .map(|outpoint| { utxo_view diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs index 2040d04aa..f5c2fe74d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs @@ -37,12 +37,12 @@ pub fn check_reward_inputs_outputs_purposes( // accounts cannot be used in block reward inputs.iter().try_for_each(|input| match input { TxInput::Utxo(_) => Ok(()), - TxInput::Account(..) | TxInput::AccountCommand(..) => { - Err(ConnectTransactionError::IOPolicyError( - IOPolicyError::AttemptToUseAccountInputInReward, - block_id.into(), - )) - } + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => Err(ConnectTransactionError::IOPolicyError( + IOPolicyError::AttemptToUseAccountInputInReward, + block_id.into(), + )), })?; let inputs_utxos = super::collect_inputs_utxos(&utxo_view, inputs)?; @@ -184,7 +184,7 @@ pub fn check_tx_inputs_outputs_purposes( .iter() .filter(|input| match input { TxInput::Utxo(_) | TxInput::Account(..) => false, - TxInput::AccountCommand(..) => true, + TxInput::AccountCommand(..) | TxInput::OrderAccountCommand(..) => true, }) .count(); @@ -197,6 +197,7 @@ pub fn check_tx_inputs_outputs_purposes( let mut produce_block_outputs_count = 0; let mut stake_pool_outputs_count = 0; let mut create_delegation_output_count = 0; + let mut create_order_output_count = 0; tx.outputs().iter().for_each(|output| match output { TxOutput::Transfer(..) @@ -206,8 +207,7 @@ pub fn check_tx_inputs_outputs_purposes( | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) | TxOutput::DataDeposit(..) - | TxOutput::Htlc(..) - | TxOutput::CreateOrder(..) => { /* do nothing */ } + | TxOutput::Htlc(..) => { /* do nothing */ } TxOutput::CreateStakePool(..) => { stake_pool_outputs_count += 1; } @@ -217,6 +217,9 @@ pub fn check_tx_inputs_outputs_purposes( TxOutput::CreateDelegationId(..) => { create_delegation_output_count += 1; } + TxOutput::CreateOrder(..) => { + create_order_output_count += 1; + } }); ensure!( @@ -231,6 +234,10 @@ pub fn check_tx_inputs_outputs_purposes( create_delegation_output_count <= 1, IOPolicyError::MultipleDelegationCreated ); + ensure!( + create_order_output_count <= 1, + IOPolicyError::MultipleOrdersCreated + ); Ok(()) } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs index 26e54c31e..9b6a3eb47 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs @@ -119,7 +119,6 @@ fn tx_many_to_many_valid(#[case] seed: Seed) { issue_tokens(), issue_nft(), data_deposit(), - create_order(), ]; let (utxo_db, tx) = prepare_utxos_and_tx_with_random_combinations( @@ -152,7 +151,6 @@ fn tx_many_to_many_valid_with_account_input(#[case] seed: Seed) { issue_tokens(), issue_nft(), data_deposit(), - create_order(), ]; let inputs_utxos = get_random_outputs_combination( diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 2a08adbb2..c18423386 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -75,8 +75,8 @@ use common::{ signed_transaction::SignedTransaction, tokens::make_token_id, AccountCommand, AccountNonce, AccountSpending, AccountType, Block, ChainConfig, - DelegationId, FrozenTokensValidationVersion, GenBlock, Transaction, TxInput, TxOutput, - UtxoOutPoint, + DelegationId, FrozenTokensValidationVersion, GenBlock, OrderAccountCommand, OrdersVersion, + Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Fee, Id, Idable}, }; @@ -384,7 +384,7 @@ where } } } - TxInput::AccountCommand(..) => None, + TxInput::AccountCommand(..) | TxInput::OrderAccountCommand(..) => None, }) .collect::, _>>()?; @@ -494,12 +494,12 @@ where // decrement nonce if disconnected input spent from an account for input in tx.inputs() { match input { - TxInput::Utxo(_) => { /* do nothing */ } + TxInput::Utxo(_) | TxInput::OrderAccountCommand(_) => { /* do nothing */ } TxInput::Account(outpoint) => { self.unspend_input_from_account(outpoint.account().clone().into())?; } - TxInput::AccountCommand(_, account_op) => { - self.unspend_input_from_account(account_op.clone().into())?; + TxInput::AccountCommand(_, cmd) => { + self.unspend_input_from_account(cmd.clone().into())?; } }; } @@ -549,7 +549,7 @@ where .inputs() .iter() .filter_map(|input| match input { - TxInput::Utxo(_) | TxInput::Account(_) => None, + TxInput::Utxo(_) | TxInput::Account(_) | TxInput::OrderAccountCommand(_) => None, TxInput::AccountCommand(nonce, account_op) => match account_op { AccountCommand::MintTokens(token_id, amount) => { let res = self @@ -726,6 +726,18 @@ where | TxOutput::DataDeposit(_) => Ok(()), }; + let check_order_doesnt_use_frozen_token = |order_id| { + let order_data = self.get_order_data(order_id)?.ok_or( + ConnectTransactionError::OrdersAccountingError( + orders_accounting::Error::OrderDataNotFound(*order_id), + ), + )?; + [order_data.ask(), order_data.give()].iter().try_for_each(|v| match v { + OutputValue::TokenV0(_) | OutputValue::Coin(_) => Ok(()), + OutputValue::TokenV1(token_id, _) => check_not_frozen(*token_id), + }) + }; + tx.inputs() .iter() .try_for_each(|input| -> Result<(), ConnectTransactionError> { @@ -763,15 +775,13 @@ where | AccountCommand::UnfreezeToken(_) => Ok(()), AccountCommand::ConcludeOrder(order_id) | AccountCommand::FillOrder(order_id, _, _) => { - let order_data = self.get_order_data(order_id)?.ok_or( - ConnectTransactionError::OrdersAccountingError( - orders_accounting::Error::OrderDataNotFound(*order_id), - ), - )?; - [order_data.ask(), order_data.give()].iter().try_for_each(|v| match v { - OutputValue::TokenV0(_) | OutputValue::Coin(_) => Ok(()), - OutputValue::TokenV1(token_id, _) => check_not_frozen(*token_id), - }) + check_order_doesnt_use_frozen_token(order_id) + } + }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + check_order_doesnt_use_frozen_token(order_id) } }, } @@ -844,12 +854,28 @@ where .spend_input_from_account(*nonce, account_op.clone().into()) .and_then(|_| { self.orders_accounting_cache - .fill_order(*order_id, *fill) + .fill_order(*order_id, *fill, OrdersVersion::V0) .map_err(ConnectTransactionError::OrdersAccountingError) }); Some(res) } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, fill, _) => { + let res = self + .orders_accounting_cache + .fill_order(*order_id, *fill, OrdersVersion::V1) + .map_err(ConnectTransactionError::OrdersAccountingError); + Some(res) + } + OrderAccountCommand::ConcludeOrder(order_id) => { + let res = self + .orders_accounting_cache + .conclude_order(*order_id) + .map_err(ConnectTransactionError::OrdersAccountingError); + Some(res) + } + }, }) .collect::, _>>()?; diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index b331ab510..e37b68322 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -30,8 +30,9 @@ use crate::{ pow::PoWChainConfigBuilder, ChainstateUpgrade, ChangeTokenMetadataUriActivated, CoinUnit, ConsensusUpgrade, DataDepositFeeVersion, Destination, FrozenTokensValidationVersion, GenBlock, Genesis, - HtlcActivated, NetUpgrades, OrdersActivated, PoSChainConfig, PoSConsensusVersion, - PoWChainConfig, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + HtlcActivated, NetUpgrades, OrdersActivated, OrdersVersion, PoSChainConfig, + PoSConsensusVersion, PoWChainConfig, RewardDistributionVersion, TokenIssuanceVersion, + TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -55,8 +56,11 @@ const TESTNET_STAKER_REWARD_AND_TOKENS_FEE_FORK_HEIGHT: BlockHeight = BlockHeigh const TESTNET_HTLC_AND_DATA_DEPOSIT_FEE_FORK_HEIGHT: BlockHeight = BlockHeight::new(297_550); // The fork, at which order outputs become valid const TESTNET_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(325_180); +const TESTNET_ORDERS_V1_FORK_HEIGHT: BlockHeight = BlockHeight::new(999_999_999); + // The fork, at which txs with htlc and orders outputs become valid const MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(254_740); +const MAINNET_ORDERS_V1_FORK_HEIGHT: BlockHeight = BlockHeight::new(999_999_999); impl ChainType { fn default_genesis_init(&self) -> GenesisBlockInit { @@ -175,6 +179,7 @@ impl ChainType { FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, + OrdersVersion::V0, ), ), ( @@ -188,6 +193,21 @@ impl ChainType { FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, + OrdersVersion::V0, + ), + ), + ( + MAINNET_ORDERS_V1_FORK_HEIGHT, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, + HtlcActivated::Yes, + OrdersActivated::Yes, + OrdersVersion::V1, ), ), ]; @@ -205,6 +225,7 @@ impl ChainType { FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, + OrdersVersion::V1, ), )]; NetUpgrades::initialize(upgrades).expect("net upgrades") @@ -222,6 +243,7 @@ impl ChainType { FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, + OrdersVersion::V0, ), ), ( @@ -235,6 +257,7 @@ impl ChainType { FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, + OrdersVersion::V0, ), ), ( @@ -248,6 +271,7 @@ impl ChainType { FrozenTokensValidationVersion::V0, HtlcActivated::No, OrdersActivated::No, + OrdersVersion::V0, ), ), ( @@ -261,6 +285,7 @@ impl ChainType { FrozenTokensValidationVersion::V0, HtlcActivated::Yes, OrdersActivated::No, + OrdersVersion::V0, ), ), ( @@ -274,6 +299,21 @@ impl ChainType { FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, + OrdersVersion::V0, + ), + ), + ( + TESTNET_ORDERS_V1_FORK_HEIGHT, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, + HtlcActivated::Yes, + OrdersActivated::Yes, + OrdersVersion::V1, ), ), ]; diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index 86554fc59..31f65d389 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -57,8 +57,8 @@ use crate::{ use super::{ output_value::OutputValue, stakelock::StakePoolData, ChainstateUpgrade, ChangeTokenMetadataUriActivated, ConsensusUpgrade, DataDepositFeeVersion, DestinationTag, - FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RequiredConsensus, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, + RequiredConsensus, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }; use self::{ @@ -915,6 +915,7 @@ pub fn create_unit_test_config_builder() -> Builder { FrozenTokensValidationVersion::V1, HtlcActivated::Yes, OrdersActivated::Yes, + OrdersVersion::V1, ), )]) .expect("cannot fail"), diff --git a/common/src/chain/tokens/tokens_utils.rs b/common/src/chain/tokens/tokens_utils.rs index 04caf8a3d..95c1b75e7 100644 --- a/common/src/chain/tokens/tokens_utils.rs +++ b/common/src/chain/tokens/tokens_utils.rs @@ -53,7 +53,7 @@ pub fn get_token_supply_change_count(inputs: &[TxInput]) -> usize { inputs .iter() .filter(|&input| match input { - TxInput::Utxo(_) | TxInput::Account(_) => false, + TxInput::Utxo(_) | TxInput::Account(_) | TxInput::OrderAccountCommand(_) => false, TxInput::AccountCommand(_, op) => match op { AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) diff --git a/common/src/chain/transaction/account_outpoint.rs b/common/src/chain/transaction/account_outpoint.rs index 5428dce51..4f47390ff 100644 --- a/common/src/chain/transaction/account_outpoint.rs +++ b/common/src/chain/transaction/account_outpoint.rs @@ -61,6 +61,15 @@ impl From for AccountType { } } +impl From for AccountType { + fn from(cmd: OrderAccountCommand) -> Self { + match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => AccountType::Order(order_id), + } + } +} + /// The type represents the amount to withdraw from a particular account. /// Otherwise it's unclear how much should be deducted from an account balance. /// It also helps solving 2 additional problems: calculating fees and providing ability to sign input balance with the witness. @@ -118,10 +127,16 @@ pub enum AccountCommand { ChangeTokenAuthority(TokenId, Destination), // Close an order and withdraw all remaining funds from both give and ask balances. // Only the address specified as `conclude_key` can authorize this command. + // After ChainstateUpgrade::OrdersVersion::V1 is activated this command becomes deprecated. + // TODO: rename this command to ConcludeOrderDeprecated. + // https://github.com/mintlayer/mintlayer-core/issues/1901 #[codec(index = 6)] ConcludeOrder(OrderId), // Satisfy an order completely or partially. // Second parameter is an amount provided to fill an order which corresponds to order's ask currency. + // After ChainstateUpgrade::OrdersVersion::V1 is activated this command becomes deprecated. + // TODO: rename this command to FillOrderDeprecated + // https://github.com/mintlayer/mintlayer-core/issues/1901 #[codec(index = 7)] FillOrder(OrderId, Amount, Destination), // Change token metadata uri @@ -160,3 +175,26 @@ impl AccountOutPoint { &self.account } } + +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Ord, + PartialOrd, + Encode, + Decode, + serde::Serialize, + serde::Deserialize, +)] +pub enum OrderAccountCommand { + // Satisfy an order completely or partially. + // Second parameter is an amount provided to fill an order which corresponds to order's ask currency. + #[codec(index = 0)] + FillOrder(OrderId, Amount, Destination), + // Close an order and withdraw all remaining funds from both give and ask balances. + // Only the address specified as `conclude_key` can authorize this command. + #[codec(index = 1)] + ConcludeOrder(OrderId), +} diff --git a/common/src/chain/transaction/input.rs b/common/src/chain/transaction/input.rs index 072d20c53..03246c2d6 100644 --- a/common/src/chain/transaction/input.rs +++ b/common/src/chain/transaction/input.rs @@ -17,7 +17,10 @@ use serialization::{Decode, Encode}; use crate::{chain::AccountNonce, text_summary::TextSummary}; -use super::{AccountCommand, AccountOutPoint, AccountSpending, OutPointSourceId, UtxoOutPoint}; +use super::{ + AccountCommand, AccountOutPoint, AccountSpending, OrderAccountCommand, OutPointSourceId, + UtxoOutPoint, +}; #[derive( Debug, @@ -39,6 +42,8 @@ pub enum TxInput { Account(AccountOutPoint), #[codec(index = 2)] AccountCommand(AccountNonce, AccountCommand), + #[codec(index = 3)] + OrderAccountCommand(OrderAccountCommand), } impl TxInput { @@ -57,7 +62,9 @@ impl TxInput { pub fn utxo_outpoint(&self) -> Option<&UtxoOutPoint> { match self { TxInput::Utxo(outpoint) => Some(outpoint), - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, } } } @@ -87,6 +94,7 @@ impl TextSummary for TxInput { } TxInput::Account(acc_out) => format!("{acc_out:?}"), TxInput::AccountCommand(nonce, cmd) => format!("AccountCommand({nonce}, {cmd:?})"), + TxInput::OrderAccountCommand(cmd) => format!("OrderAccountCommand({cmd:?})"), } } } diff --git a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs index 105526cd6..40af77440 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -32,7 +32,7 @@ use crate::{ signed_transaction::SignedTransaction, tokens::TokenId, AccountCommand, AccountOutPoint, AccountSpending, ChainConfig, DelegationId, Destination, - OutPointSourceId, Transaction, TxInput, TxOutput, UtxoOutPoint, + OrderAccountCommand, OutPointSourceId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Id, H256}, }; @@ -1059,6 +1059,18 @@ fn mutate_first_input( TxInput::AccountCommand(new_nonce, op.clone()) } } + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(id, amount, destination) => { + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + *id, + Amount::from_atoms(amount.into_atoms() + 1), + destination.clone(), + )) + } + OrderAccountCommand::ConcludeOrder(order_id) => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(*order_id)) + } + }, }; updater.inputs[0] = mutated_input; diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index 80ccce140..617fc58a8 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -69,6 +69,14 @@ pub enum FrozenTokensValidationVersion { V1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum OrdersVersion { + /// Initial orders implementation + V0, + /// Calculate fill amount based on original balances; ignore nonce for order operations + V1, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, @@ -79,6 +87,7 @@ pub struct ChainstateUpgrade { frozen_tokens_validation_version: FrozenTokensValidationVersion, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, + orders_version: OrdersVersion, } impl ChainstateUpgrade { @@ -92,6 +101,7 @@ impl ChainstateUpgrade { frozen_tokens_validation_version: FrozenTokensValidationVersion, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, + orders_version: OrdersVersion, ) -> Self { Self { token_issuance_version, @@ -102,6 +112,7 @@ impl ChainstateUpgrade { frozen_tokens_validation_version, htlc_activated, orders_activated, + orders_version, } } @@ -136,6 +147,10 @@ impl ChainstateUpgrade { pub fn frozen_tokens_validation_version(&self) -> FrozenTokensValidationVersion { self.frozen_tokens_validation_version } + + pub fn orders_version(&self) -> OrdersVersion { + self.orders_version + } } impl Activate for ChainstateUpgrade {} diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index b9df4d5b1..1c1a6c7ad 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -19,8 +19,8 @@ mod netupgrade; pub use chainstate_upgrade::{ ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, - FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, + FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::{Activate, NetUpgrades}; diff --git a/common/src/primitives/mod.rs b/common/src/primitives/mod.rs index 922677854..fc9918f26 100644 --- a/common/src/primitives/mod.rs +++ b/common/src/primitives/mod.rs @@ -58,4 +58,18 @@ impl CoinOrTokenId { OutputValue::TokenV1(id, _) => Some(Self::TokenId(*id)), } } + + pub fn to_output_value(&self, amount: Amount) -> OutputValue { + match self { + CoinOrTokenId::Coin => OutputValue::Coin(amount), + CoinOrTokenId::TokenId(id) => OutputValue::TokenV1(*id, amount), + } + } + + pub fn token_id(&self) -> Option { + match self { + CoinOrTokenId::Coin => None, + CoinOrTokenId::TokenId(id) => Some(*id), + } + } } diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index b19f39574..99d54f0a2 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -144,6 +144,7 @@ impl MempoolBanScore for ConnectTransactionError { ConnectTransactionError::AttemptToCreateOrderFromAccounts => 100, ConnectTransactionError::TotalFeeRequiredOverflow => 100, ConnectTransactionError::InsufficientCoinsFee(_, _) => 100, + ConnectTransactionError::AttemptToSpendFrozenToken(_) => 100, // Need to drill down deeper into the error in these cases ConnectTransactionError::IOPolicyError(err, _) => err.ban_score(), @@ -166,7 +167,7 @@ impl MempoolBanScore for ConnectTransactionError { ConnectTransactionError::MissingTxUndo(_) => 0, ConnectTransactionError::MissingTransactionNonce(_) => 0, ConnectTransactionError::FailedToIncrementAccountNonce => 0, - ConnectTransactionError::AttemptToSpendFrozenToken(_) => 0, + ConnectTransactionError::ConcludeInputAmountsDontMatch(_, _) => 0, } } } @@ -405,6 +406,7 @@ impl MempoolBanScore for IOPolicyError { IOPolicyError::InvalidInputTypeInTx => 100, IOPolicyError::MultiplePoolCreated => 100, IOPolicyError::MultipleDelegationCreated => 100, + IOPolicyError::MultipleOrdersCreated => 100, IOPolicyError::MultipleAccountCommands => 100, IOPolicyError::ProduceBlockInTx => 100, IOPolicyError::AttemptToUseAccountInputInReward => 100, @@ -488,8 +490,11 @@ impl MempoolBanScore for CheckTransactionError { CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, CheckTransactionError::HtlcsAreNotActivated => 100, CheckTransactionError::OrdersAreNotActivated(_) => 100, + CheckTransactionError::AttemptToFillOrderWithZero(_, _) => 100, CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, CheckTransactionError::ChangeTokenMetadataUriNotActivated => 100, + CheckTransactionError::OrdersV1AreNotActivated(_) => 100, + CheckTransactionError::DeprecatedOrdersCommands(_) => 100, } } } @@ -513,7 +518,8 @@ impl MempoolBanScore for orders_accounting::Error { Error::InvariantNonzeroAskBalanceForMissingOrder(_) => 100, Error::InvariantNonzeroGiveBalanceForMissingOrder(_) => 100, Error::OrderOverflow(_) => 100, - Error::OrderOverbid(_, _, _) => 100, + Error::OrderOverbid(_, _, _) => 0, + Error::OrderUnderbid(_, _) => 100, Error::AttemptedConcludeNonexistingOrderData(_) => 0, Error::UnsupportedTokenVersion => 100, Error::ViewFail => 0, diff --git a/mempool/src/pool/entry.rs b/mempool/src/pool/entry.rs index bfbfbdae3..3b1fde3f8 100644 --- a/mempool/src/pool/entry.rs +++ b/mempool/src/pool/entry.rs @@ -17,8 +17,8 @@ use std::num::NonZeroUsize; use common::{ chain::{ - AccountCommand, AccountNonce, AccountSpending, AccountType, SignedTransaction, Transaction, - TxInput, UtxoOutPoint, + AccountCommand, AccountNonce, AccountSpending, AccountType, OrderAccountCommand, + SignedTransaction, Transaction, TxInput, UtxoOutPoint, }, primitives::{Id, Idable}, }; @@ -45,28 +45,31 @@ pub enum TxDependency { DelegationAccount(TxAccountDependency), TokenSupplyAccount(TxAccountDependency), OrderAccount(TxAccountDependency), + // TODO: keep only V1 version after OrdersVersion::V1 is activated + // https://github.com/mintlayer/mintlayer-core/issues/1901 + OrderV1Account(AccountType), TxOutput(Id, u32), // TODO: Block reward? } impl TxDependency { - fn from_utxo(outpt: &UtxoOutPoint) -> Option { - outpt + fn from_utxo(output: &UtxoOutPoint) -> Option { + output .source_id() .get_tx_id() - .map(|id| Self::TxOutput(*id, outpt.output_index())) + .map(|id| Self::TxOutput(*id, output.output_index())) } - fn from_account(acct: &AccountSpending, nonce: AccountNonce) -> Self { - match acct { + fn from_account(account: &AccountSpending, nonce: AccountNonce) -> Self { + match account { AccountSpending::DelegationBalance(_, _) => { - Self::DelegationAccount(TxAccountDependency::new(acct.clone().into(), nonce)) + Self::DelegationAccount(TxAccountDependency::new(account.clone().into(), nonce)) } } } - fn from_account_op(acct: &AccountCommand, nonce: AccountNonce) -> Self { - match acct { + fn from_account_cmd(cmd: &AccountCommand, nonce: AccountNonce) -> Self { + match cmd { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) @@ -74,13 +77,16 @@ impl TxDependency { | AccountCommand::UnfreezeToken(_) | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ChangeTokenAuthority(_, _) => { - Self::TokenSupplyAccount(TxAccountDependency::new(acct.clone().into(), nonce)) + Self::TokenSupplyAccount(TxAccountDependency::new(cmd.clone().into(), nonce)) } AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { - Self::OrderAccount(TxAccountDependency::new(acct.clone().into(), nonce)) + Self::OrderAccount(TxAccountDependency::new(cmd.clone().into(), nonce)) } } } + fn from_order_account_cmd(cmd: &OrderAccountCommand) -> Self { + Self::OrderV1Account(cmd.clone().into()) + } fn from_input_requires(input: &TxInput) -> Option { match input { @@ -89,8 +95,9 @@ impl TxDependency { acct.nonce().decrement().map(|nonce| Self::from_account(acct.account(), nonce)) } TxInput::AccountCommand(nonce, op) => { - nonce.decrement().map(|nonce| Self::from_account_op(op, nonce)) + nonce.decrement().map(|nonce| Self::from_account_cmd(op, nonce)) } + TxInput::OrderAccountCommand(cmd) => Some(Self::from_order_account_cmd(cmd)), } } @@ -98,7 +105,8 @@ impl TxDependency { match input { TxInput::Utxo(_) => None, TxInput::Account(acct) => Some(Self::from_account(acct.account(), acct.nonce())), - TxInput::AccountCommand(nonce, op) => Some(Self::from_account_op(op, *nonce)), + TxInput::AccountCommand(nonce, op) => Some(Self::from_account_cmd(op, *nonce)), + TxInput::OrderAccountCommand(cmd) => Some(Self::from_order_account_cmd(cmd)), } } } diff --git a/mempool/src/pool/orphans/detect.rs b/mempool/src/pool/orphans/detect.rs index b426ba3e2..9701f5de4 100644 --- a/mempool/src/pool/orphans/detect.rs +++ b/mempool/src/pool/orphans/detect.rs @@ -78,6 +78,7 @@ impl OrphanType { | CTE::CheckTransactionError(_) | CTE::OrdersAccountingError(_) | CTE::AttemptToCreateOrderFromAccounts + | CTE::ConcludeInputAmountsDontMatch(_, _) | CTE::IOPolicyError(_, _) => Err(err), } } diff --git a/mempool/src/pool/orphans/mod.rs b/mempool/src/pool/orphans/mod.rs index b6eebbf3e..ad8a3498e 100644 --- a/mempool/src/pool/orphans/mod.rs +++ b/mempool/src/pool/orphans/mod.rs @@ -306,7 +306,8 @@ impl<'p> PoolEntry<'p> { // Always consider account deps. TODO: can be optimized in the future TxDependency::DelegationAccount(_) | TxDependency::TokenSupplyAccount(_) - | TxDependency::OrderAccount(_) => false, + | TxDependency::OrderAccount(_) + | TxDependency::OrderV1Account(_) => false, TxDependency::TxOutput(tx_id, _) => self.pool.maps.by_tx_id.contains_key(&tx_id), }) } diff --git a/mempool/src/pool/tx_pool/mod.rs b/mempool/src/pool/tx_pool/mod.rs index 09f223f94..3867f2aeb 100644 --- a/mempool/src/pool/tx_pool/mod.rs +++ b/mempool/src/pool/tx_pool/mod.rs @@ -341,7 +341,9 @@ impl TxPool { .source_id() .get_tx_id() .is_some_and(|tx_id| self.contains_transaction(tx_id)), - TxInput::Account(..) | TxInput::AccountCommand(..) => false, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => false, } } } diff --git a/mempool/src/pool/tx_pool/store/mod.rs b/mempool/src/pool/tx_pool/store/mod.rs index 9e30db163..2b9a6a316 100644 --- a/mempool/src/pool/tx_pool/store/mod.rs +++ b/mempool/src/pool/tx_pool/store/mod.rs @@ -314,7 +314,9 @@ impl MempoolStore { .iter() .filter_map(|input| match input { TxInput::Utxo(outpoint) => outpoint.source_id().get_tx_id().cloned(), - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .filter(|id| self.txs_by_id.contains_key(id)) .collect::>(); diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index 1a7e4fbe1..47a2ad18d 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -23,8 +23,8 @@ use common::chain::{ DestinationSigError, EvaluatedInputWitness, }, tokens::TokenId, - AccountCommand, AccountOutPoint, AccountSpending, DelegationId, Destination, OrderId, PoolId, - SignedTransaction, TxOutput, UtxoOutPoint, + AccountCommand, AccountOutPoint, AccountSpending, DelegationId, Destination, + OrderAccountCommand, OrderId, PoolId, SignedTransaction, TxOutput, UtxoOutPoint, }; use utxo::UtxoSource; @@ -80,6 +80,9 @@ pub enum InputInfo<'a> { AccountCommand { command: &'a AccountCommand, }, + OrderAccountCommand { + command: &'a OrderAccountCommand, + }, } impl InputInfo<'_> { @@ -90,7 +93,9 @@ impl InputInfo<'_> { utxo, utxo_source: _, } => Some(utxo), - InputInfo::Account { .. } | InputInfo::AccountCommand { .. } => None, + InputInfo::Account { .. } + | InputInfo::AccountCommand { .. } + | InputInfo::OrderAccountCommand { .. } => None, } } } @@ -241,6 +246,15 @@ impl TranslateInput for SignedTransaction { } AccountCommand::FillOrder(_, _, _) => Ok(WitnessScript::TRUE), }, + InputInfo::OrderAccountCommand { command } => match command { + OrderAccountCommand::FillOrder(_, _, _) => Ok(WitnessScript::TRUE), + OrderAccountCommand::ConcludeOrder(order_id) => { + let dest = ctx + .get_orders_conclude_destination(order_id)? + .ok_or(TranslationError::OrderNotFound(*order_id))?; + Ok(to_signature_witness_script(ctx, &dest)) + } + }, } } } @@ -279,9 +293,9 @@ impl TranslateInput for BlockRewardTransactable<'_> } } } - InputInfo::Account { .. } | InputInfo::AccountCommand { .. } => { - Err(TranslationError::IllegalAccountSpend) - } + InputInfo::Account { .. } + | InputInfo::AccountCommand { .. } + | InputInfo::OrderAccountCommand { .. } => Err(TranslationError::IllegalAccountSpend), } } } @@ -342,6 +356,11 @@ impl TranslateInput for TimelockOnly { Ok(WitnessScript::TRUE) } }, + InputInfo::OrderAccountCommand { command } => match command { + OrderAccountCommand::FillOrder(..) | OrderAccountCommand::ConcludeOrder { .. } => { + Ok(WitnessScript::TRUE) + } + }, } } } @@ -447,6 +466,15 @@ impl TranslateInput for SignatureOnlyTx { } AccountCommand::FillOrder(_, _, _) => Ok(WitnessScript::TRUE), }, + InputInfo::OrderAccountCommand { command } => match command { + OrderAccountCommand::FillOrder(_, _, _) => Ok(WitnessScript::TRUE), + OrderAccountCommand::ConcludeOrder(order_id) => { + let dest = ctx + .get_orders_conclude_destination(order_id)? + .ok_or(TranslationError::OrderNotFound(*order_id))?; + Ok(to_signature_witness_script(ctx, &dest)) + } + }, } } } diff --git a/orders-accounting/src/cache.rs b/orders-accounting/src/cache.rs index cc2ed82d7..63c9eea4e 100644 --- a/orders-accounting/src/cache.rs +++ b/orders-accounting/src/cache.rs @@ -15,7 +15,7 @@ use accounting::combine_amount_delta; use common::{ - chain::{output_value::OutputValue, OrderData, OrderId}, + chain::{OrderData, OrderId, OrdersVersion}, primitives::Amount, }; use logging::log; @@ -33,13 +33,6 @@ use crate::{ FlushableOrdersAccountingView, OrdersAccountingDeltaUndoData, }; -fn output_value_amount(value: &OutputValue) -> Result { - match value { - OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => Ok(*amount), - OutputValue::TokenV0(_) => Err(Error::UnsupportedTokenVersion), - } -} - pub struct OrdersAccountingCache

{ parent: P, data: OrdersAccountingDeltaData, @@ -153,8 +146,8 @@ impl OrdersAccountingOperations for OrdersAccountingCac Error::OrderAlreadyExists(id) ); - let ask_amount = output_value_amount(data.ask())?; - let give_amount = output_value_amount(data.give())?; + let ask_amount = crate::output_value_amount(data.ask())?; + let give_amount = crate::output_value_amount(data.give())?; ensure!( ask_amount > Amount::ZERO && give_amount > Amount::ZERO, @@ -206,6 +199,7 @@ impl OrdersAccountingOperations for OrdersAccountingCac &mut self, id: OrderId, fill_amount_in_ask_currency: Amount, + orders_version: OrdersVersion, ) -> Result { log::debug!( "Filling an order: {:?} {:?}", @@ -218,7 +212,8 @@ impl OrdersAccountingOperations for OrdersAccountingCac Error::OrderDataNotFound(id) ); - let filled_amount = calculate_fill_order(self, id, fill_amount_in_ask_currency)?; + let filled_amount = + calculate_fill_order(self, id, fill_amount_in_ask_currency, orders_version)?; self.data.give_balances.sub_unsigned(id, filled_amount)?; self.data.ask_balances.sub_unsigned(id, fill_amount_in_ask_currency)?; diff --git a/orders-accounting/src/error.rs b/orders-accounting/src/error.rs index 962f6e861..3bc4d1de2 100644 --- a/orders-accounting/src/error.rs +++ b/orders-accounting/src/error.rs @@ -47,6 +47,8 @@ pub enum Error { OrderOverflow(OrderId), #[error("Order `{0}` can provide `{1:?}` amount; but attempted to fill `{2:?}`")] OrderOverbid(OrderId, Amount, Amount), + #[error("Order `{0}` provides amount `{1:?}` that is not enough to fill even a single atom")] + OrderUnderbid(OrderId, Amount), #[error("Attempt to conclude non-existing order data `{0}`")] AttemptedConcludeNonexistingOrderData(OrderId), #[error("Unsupported token version")] diff --git a/orders-accounting/src/lib.rs b/orders-accounting/src/lib.rs index 98637660e..3d57e267e 100644 --- a/orders-accounting/src/lib.rs +++ b/orders-accounting/src/lib.rs @@ -34,5 +34,14 @@ pub use { view::{FlushableOrdersAccountingView, OrdersAccountingView}, }; +use common::{chain::output_value::OutputValue, primitives::Amount}; + +fn output_value_amount(value: &OutputValue) -> error::Result { + match value { + OutputValue::Coin(amount) | OutputValue::TokenV1(_, amount) => Ok(*amount), + OutputValue::TokenV0(_) => Err(Error::UnsupportedTokenVersion), + } +} + #[cfg(test)] mod tests; diff --git a/orders-accounting/src/operations.rs b/orders-accounting/src/operations.rs index 5a505721c..f549943d4 100644 --- a/orders-accounting/src/operations.rs +++ b/orders-accounting/src/operations.rs @@ -15,7 +15,7 @@ use accounting::DataDeltaUndo; use common::{ - chain::{OrderData, OrderId}, + chain::{OrderData, OrderId, OrdersVersion}, primitives::Amount, }; use serialization::{Decode, Encode}; @@ -61,6 +61,7 @@ pub trait OrdersAccountingOperations { &mut self, id: OrderId, fill_amount_in_ask_currency: Amount, + orders_version: OrdersVersion, ) -> Result; fn undo(&mut self, undo_data: OrdersAccountingUndo) -> Result<()>; diff --git a/orders-accounting/src/price_calculation.rs b/orders-accounting/src/price_calculation.rs index c31c9f02c..0bb311779 100644 --- a/orders-accounting/src/price_calculation.rs +++ b/orders-accounting/src/price_calculation.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::{chain::OrderId, primitives::Amount, Uint256}; +use common::{ + chain::{OrderId, OrdersVersion}, + primitives::Amount, + Uint256, +}; use utils::ensure; use crate::{error::Result, Error, OrdersAccountingView}; @@ -22,7 +26,28 @@ pub fn calculate_fill_order( view: &impl OrdersAccountingView, order_id: OrderId, fill_amount_in_ask_currency: Amount, + orders_version: OrdersVersion, ) -> Result { + match orders_version { + OrdersVersion::V0 => calculate_fill_order_based_on_remaining_balances( + view, + order_id, + fill_amount_in_ask_currency, + ), + OrdersVersion::V1 => calculate_fill_order_based_on_original_price( + view, + order_id, + fill_amount_in_ask_currency, + ), + } +} + +fn calculate_fill_order_based_on_remaining_balances( + view: &impl OrdersAccountingView, + order_id: OrderId, + fill_amount_in_ask_currency: Amount, +) -> Result { + // Take remaining balances to calculate price let ask_balance = view.get_ask_balance(&order_id).map_err(|_| crate::Error::ViewFail)?; let give_balance = view.get_give_balance(&order_id).map_err(|_| crate::Error::ViewFail)?; @@ -35,6 +60,39 @@ pub fn calculate_fill_order( .ok_or(Error::OrderOverflow(order_id)) } +fn calculate_fill_order_based_on_original_price( + view: &impl OrdersAccountingView, + order_id: OrderId, + fill_amount_in_ask_currency: Amount, +) -> Result { + // Take original balances to calculate price + let order_data = view + .get_order_data(&order_id) + .map_err(|_| crate::Error::ViewFail)? + .ok_or(crate::Error::OrderDataNotFound(order_id))?; + + let original_ask = crate::output_value_amount(order_data.ask())?; + let original_give = crate::output_value_amount(order_data.give())?; + + // Check overbid anyway + let current_ask_balance = + view.get_ask_balance(&order_id).map_err(|_| crate::Error::ViewFail)?; + ensure!( + current_ask_balance >= fill_amount_in_ask_currency, + Error::OrderOverbid(order_id, current_ask_balance, fill_amount_in_ask_currency) + ); + + let result = calculate_filled_amount(original_ask, original_give, fill_amount_in_ask_currency) + .ok_or(Error::OrderOverflow(order_id))?; + + ensure!( + result > Amount::ZERO, + Error::OrderUnderbid(order_id, fill_amount_in_ask_currency) + ); + + Ok(result) +} + pub fn calculate_filled_amount( ask_amount: Amount, give_amount: Amount, @@ -157,17 +215,23 @@ mod tests { #[case(coin!(3), coin!(100), amount!(3), 100)] #[case(coin!(1), token!(u128::MAX), amount!(1), u128::MAX)] #[case(coin!(2), token!(u128::MAX), amount!(2), u128::MAX)] - fn fill_order_valid_values( + fn fill_order_valid_values_v0( #[case] ask: OutputValue, #[case] give: OutputValue, #[case] fill: Amount, #[case] result: u128, ) { let order_id = OrderId::zero(); + + // Original balances are irrelevant for price let orders_store = InMemoryOrdersAccounting::from_values( BTreeMap::from_iter([( order_id, - OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()), + OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::ZERO), + OutputValue::Coin(Amount::ZERO), + ), )]), BTreeMap::from_iter([(order_id, output_value_amount(&ask))]), BTreeMap::from_iter([(order_id, output_value_amount(&give))]), @@ -175,17 +239,97 @@ mod tests { let orders_db = OrdersAccountingDB::new(&orders_store); assert_eq!( - calculate_fill_order(&orders_db, order_id, fill), + calculate_fill_order_based_on_remaining_balances(&orders_db, order_id, fill), Ok(Amount::from_atoms(result)) ); } + #[rstest] + #[case(token!(3), coin!(100), amount!(1), 33)] + #[case(token!(3), coin!(100), amount!(2), 66)] + #[case(token!(3), coin!(100), amount!(3), 100)] + #[case(token!(5), coin!(100), amount!(1), 20)] + #[case(token!(5), coin!(100), amount!(2), 40)] + #[case(token!(5), coin!(100), amount!(3), 60)] + #[case(token!(5), coin!(100), amount!(4), 80)] + #[case(token!(5), coin!(100), amount!(5), 100)] + #[case(coin!(100), token!(3), amount!(34), 1)] + #[case(coin!(100), token!(3), amount!(66), 1)] + #[case(coin!(100), token!(3), amount!(67), 2)] + #[case(coin!(100), token!(3), amount!(99), 2)] + #[case(coin!(100), token!(3), amount!(100), 3)] + #[case(token!(3), token2!(100), amount!(1), 33)] + #[case(token!(3), token2!(100), amount!(2), 66)] + #[case(token!(3), token2!(100), amount!(3), 100)] + #[case(coin!(3), coin!(100), amount!(1), 33)] + #[case(coin!(3), coin!(100), amount!(2), 66)] + #[case(coin!(3), coin!(100), amount!(3), 100)] + #[case(coin!(1), token!(u128::MAX), amount!(1), u128::MAX)] + #[case(coin!(2), token!(u128::MAX), amount!(2), u128::MAX)] + fn fill_order_valid_values_v1( + #[case] ask: OutputValue, + #[case] give: OutputValue, + #[case] fill: Amount, + #[case] result: u128, + ) { + let order_id = OrderId::zero(); + + // Current balances are irrelevant for price + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([( + order_id, + OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()), + )]), + BTreeMap::from_iter([(order_id, fill)]), + BTreeMap::from_iter([(order_id, Amount::ZERO)]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + assert_eq!( + calculate_fill_order_based_on_original_price(&orders_db, order_id, fill), + Ok(Amount::from_atoms(result)) + ); + } + + #[rstest] + #[case(token!(0), coin!(1), amount!(0), Error::OrderOverflow(OrderId::zero()))] + #[case(token!(0), coin!(1), amount!(1), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(0), Amount::from_atoms(1)))] + #[case(coin!(1), token!(1), amount!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] + #[case(coin!(1), token!(u128::MAX), amount!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] + fn fill_order_invalid_values_v0( + #[case] ask: OutputValue, + #[case] give: OutputValue, + #[case] fill: Amount, + #[case] error: Error, + ) { + let order_id = OrderId::zero(); + let orders_store = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([( + order_id, + OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()), + )]), + BTreeMap::from_iter([(order_id, output_value_amount(&ask))]), + BTreeMap::from_iter([(order_id, output_value_amount(&give))]), + ); + let orders_db = OrdersAccountingDB::new(&orders_store); + + assert_eq!( + calculate_fill_order_based_on_remaining_balances(&orders_db, order_id, fill), + Err(error.clone()) + ); + } + #[rstest] #[case(token!(0), coin!(1), amount!(0), Error::OrderOverflow(OrderId::zero()))] #[case(token!(0), coin!(1), amount!(1), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(0), Amount::from_atoms(1)))] #[case(coin!(1), token!(1), amount!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] #[case(coin!(1), token!(u128::MAX), amount!(2), Error::OrderOverbid(OrderId::zero(), Amount::from_atoms(1), Amount::from_atoms(2)))] - fn fill_order_invalid_values( + #[case(token!(3), coin!(1), amount!(0), Error::OrderUnderbid(OrderId::zero(), Amount::from_atoms(0)))] + #[case(token!(3), coin!(1), amount!(1), Error::OrderUnderbid(OrderId::zero(), Amount::from_atoms(1)))] + #[case(token!(3), coin!(1), amount!(2), Error::OrderUnderbid(OrderId::zero(), Amount::from_atoms(2)))] + #[case(token!(1), coin!(0), amount!(0), Error::OrderUnderbid(OrderId::zero(), Amount::from_atoms(0)))] + #[case(token!(1), coin!(0), amount!(1), Error::OrderUnderbid(OrderId::zero(), Amount::from_atoms(1)))] + fn fill_order_invalid_values_v1( #[case] ask: OutputValue, #[case] give: OutputValue, #[case] fill: Amount, @@ -202,6 +346,9 @@ mod tests { ); let orders_db = OrdersAccountingDB::new(&orders_store); - assert_eq!(calculate_fill_order(&orders_db, order_id, fill), Err(error)); + assert_eq!( + calculate_fill_order_based_on_original_price(&orders_db, order_id, fill), + Err(error) + ); } } diff --git a/orders-accounting/src/tests/operations.rs b/orders-accounting/src/tests/operations.rs index 6e6e91982..5ca22b2dc 100644 --- a/orders-accounting/src/tests/operations.rs +++ b/orders-accounting/src/tests/operations.rs @@ -16,7 +16,9 @@ use std::collections::BTreeMap; use common::{ - chain::{output_value::OutputValue, tokens::TokenId, Destination, OrderData, OrderId}, + chain::{ + output_value::OutputValue, tokens::TokenId, Destination, OrderData, OrderId, OrdersVersion, + }, primitives::Amount, }; use randomness::Rng; @@ -253,8 +255,9 @@ fn conclude_order_and_undo(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_entire_order_and_flush(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_entire_order_and_flush(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); let order_id = OrderId::random_using(&mut rng); @@ -271,7 +274,7 @@ fn fill_entire_order_and_flush(#[case] seed: Seed) { // try to fill non-existing order { let random_order = OrderId::random_using(&mut rng); - let result = cache.fill_order(random_order, output_value_amount(order_data.ask())); + let result = cache.fill_order(random_order, output_value_amount(order_data.ask()), version); assert_eq!(result.unwrap_err(), Error::OrderDataNotFound(random_order)); } @@ -279,7 +282,7 @@ fn fill_entire_order_and_flush(#[case] seed: Seed) { { let ask_amount = output_value_amount(order_data.ask()); let fill = (ask_amount + Amount::from_atoms(1)).unwrap(); - let result = cache.fill_order(order_id, fill); + let result = cache.fill_order(order_id, fill, version); assert_eq!( result.unwrap_err(), Error::OrderOverbid( @@ -290,7 +293,9 @@ fn fill_entire_order_and_flush(#[case] seed: Seed) { ); } - let _ = cache.fill_order(order_id, output_value_amount(order_data.ask())).unwrap(); + let _ = cache + .fill_order(order_id, output_value_amount(order_data.ask()), version) + .unwrap(); db.batch_write_orders_data(cache.consume()).unwrap(); @@ -304,8 +309,9 @@ fn fill_entire_order_and_flush(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_partially_and_flush(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_partially_and_flush(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); let order_id = OrderId::random_using(&mut rng); @@ -324,7 +330,7 @@ fn fill_order_partially_and_flush(#[case] seed: Seed) { let mut db = OrdersAccountingDB::new(&mut storage); let mut cache = OrdersAccountingCache::new(&db); - let _ = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let _ = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); assert_eq!( Some(&order_data), @@ -339,7 +345,7 @@ fn fill_order_partially_and_flush(#[case] seed: Seed) { cache.get_give_balance(&order_id).unwrap() ); - let _ = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let _ = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); assert_eq!( Some(&order_data), @@ -354,22 +360,41 @@ fn fill_order_partially_and_flush(#[case] seed: Seed) { cache.get_give_balance(&order_id).unwrap() ); - let _ = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let _ = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!(Amount::ZERO, cache.get_ask_balance(&order_id).unwrap()); + let expected_give_balance = match version { + OrdersVersion::V0 => Amount::ZERO, + OrdersVersion::V1 => Amount::from_atoms(1), + }; + assert_eq!( + expected_give_balance, + cache.get_give_balance(&order_id).unwrap() + ); db.batch_write_orders_data(cache.consume()).unwrap(); + let expected_give_balances = match version { + OrdersVersion::V0 => BTreeMap::new(), + OrdersVersion::V1 => BTreeMap::from_iter([(order_id, Amount::from_atoms(1))]), + }; let expected_storage = InMemoryOrdersAccounting::from_values( BTreeMap::from_iter([(order_id, order_data)]), BTreeMap::new(), - BTreeMap::new(), + expected_give_balances, ); assert_eq!(expected_storage, storage); } #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_partially_and_undo(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_partially_and_undo(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); let order_id = OrderId::random_using(&mut rng); @@ -389,18 +414,25 @@ fn fill_order_partially_and_undo(#[case] seed: Seed) { let mut db = OrdersAccountingDB::new(&mut storage); let mut cache = OrdersAccountingCache::new(&db); - let undo1 = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let undo1 = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); - let undo2 = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let undo2 = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); - let undo3 = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let undo3 = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); assert_eq!( Some(&order_data), cache.get_order_data(&order_id).unwrap().as_ref() ); assert_eq!(Amount::ZERO, cache.get_ask_balance(&order_id).unwrap()); - assert_eq!(Amount::ZERO, cache.get_give_balance(&order_id).unwrap()); + let expected_give_balance = match version { + OrdersVersion::V0 => Amount::ZERO, + OrdersVersion::V1 => Amount::from_atoms(1), + }; + assert_eq!( + expected_give_balance, + cache.get_give_balance(&order_id).unwrap() + ); cache.undo(undo3).unwrap(); @@ -454,8 +486,9 @@ fn fill_order_partially_and_undo(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_partially_and_conclude(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_partially_and_conclude(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); let order_id = OrderId::random_using(&mut rng); @@ -474,7 +507,7 @@ fn fill_order_partially_and_conclude(#[case] seed: Seed) { let mut db = OrdersAccountingDB::new(&mut storage); let mut cache = OrdersAccountingCache::new(&db); - let _ = cache.fill_order(order_id, Amount::from_atoms(1)).unwrap(); + let _ = cache.fill_order(order_id, Amount::from_atoms(1), version).unwrap(); assert_eq!( Some(&order_data), @@ -497,16 +530,25 @@ fn fill_order_partially_and_conclude(#[case] seed: Seed) { } // If total give balance of an order is split into a random number of fill operations -// they must exhaust the order entirely without any change left. +// they must exhaust the order entirely. +// For V0 implementation there should not be any change left. +// For V1 it is allowed for some dust to be left in the give balance. #[rstest] #[trace] -#[case(Seed::from_entropy())] -fn fill_order_must_converge(#[case] seed: Seed) { +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_must_converge(#[case] seed: Seed, #[case] version: OrdersVersion) { let mut rng = make_seedable_rng(seed); - let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); - let give_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); - let fill_orders = test_utils::split_value(&mut rng, ask_amount.into_atoms()); + let ask_atoms = rng.gen_range(1u128..1_000_000_000); + let give_atoms = rng.gen_range(1u128..1_000_000_000); + let ask_amount = Amount::from_atoms(ask_atoms); + let give_amount = Amount::from_atoms(give_atoms); + let fill_orders = test_utils::split_value(&mut rng, ask_atoms) + .into_iter() + .filter(|v| *v > 0) + .collect::>(); let ask = OutputValue::Coin(ask_amount); let give = OutputValue::Coin(give_amount); @@ -521,16 +563,200 @@ fn fill_order_must_converge(#[case] seed: Seed) { let mut db = OrdersAccountingDB::new(&mut storage); let mut cache = OrdersAccountingCache::new(&db); + // Accumulate all fractional parts to later compare with dust balance + let mut remainder = 0f64; + for fill in fill_orders { - let _ = cache.fill_order(order_id, Amount::from_atoms(fill)).unwrap(); + let _ = cache.fill_order(order_id, Amount::from_atoms(fill), version).unwrap(); + + let integer_result = give_atoms * fill / ask_atoms; + let float_result = (give_atoms * fill) as f64 / ask_atoms as f64; + // Collect only positive fractional point. + // This is required to work around the fact that 12./4. can give 2.9999999 + if float_result as u128 >= integer_result { + remainder += float_result.fract(); + } } db.batch_write_orders_data(cache.consume()).unwrap(); + let expected_give_balance = match version { + OrdersVersion::V0 => BTreeMap::new(), + OrdersVersion::V1 => { + let tolerance: f64 = 1e-6; + if remainder < tolerance { + BTreeMap::new() + } else { + BTreeMap::from_iter([(order_id, Amount::from_atoms(remainder.round() as u128))]) + } + } + }; + let expected_storage = InMemoryOrdersAccounting::from_values( BTreeMap::from_iter([(order_id, order_data)]), BTreeMap::new(), + expected_give_balance, + ); + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy(), vec![214, 487, 21, 354, 13, 139, 213, 1319, 112, 461])] +fn fill_order_commutativity(#[case] seed: Seed, #[case] fills: Vec) { + use randomness::SliceRandom; + + let mut rng = make_seedable_rng(seed); + + let ask_atoms = 3333; + let give_atoms = 1_000_000_000; + let ask_amount = Amount::from_atoms(ask_atoms); + let give_amount = Amount::from_atoms(give_atoms); + let mut fill_orders = fills.clone(); + fill_orders.shuffle(&mut rng); + assert_eq!(ask_amount.into_atoms(), fill_orders.iter().sum()); + + let ask = OutputValue::Coin(ask_amount); + let give = OutputValue::Coin(give_amount); + + let order_id = OrderId::random_using(&mut rng); + let order_data = OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()); + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, ask_amount)]), + BTreeMap::from_iter([(order_id, give_amount)]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + for fill in fill_orders { + let _ = cache.fill_order(order_id, Amount::from_atoms(fill), OrdersVersion::V1).unwrap(); + } + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), BTreeMap::new(), + BTreeMap::from_iter([(order_id, Amount::from_atoms(4))]), ); assert_eq!(expected_storage, storage); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V0)] +#[trace] +#[case(Seed::from_entropy(), OrdersVersion::V1)] +fn fill_order_underbid(#[case] seed: Seed, #[case] version: OrdersVersion) { + let mut rng = make_seedable_rng(seed); + + let order_id = OrderId::random_using(&mut rng); + let token_id = TokenId::random_using(&mut rng); + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(Amount::from_atoms(3)), + OutputValue::TokenV1(token_id, Amount::from_atoms(1)), + ); + + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.ask()))]), + BTreeMap::from_iter([(order_id, output_value_amount(order_data.give()))]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let result = cache.fill_order(order_id, Amount::from_atoms(1), version); + + match version { + OrdersVersion::V0 => { + assert!(result.is_ok()); + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!( + Amount::from_atoms(2), + cache.get_ask_balance(&order_id).unwrap() + ); + assert_eq!( + Amount::from_atoms(1), + cache.get_give_balance(&order_id).unwrap() + ); + + let _ = cache.fill_order(order_id, Amount::from_atoms(2), version).unwrap(); + + assert_eq!( + Some(&order_data), + cache.get_order_data(&order_id).unwrap().as_ref() + ); + assert_eq!(Amount::ZERO, cache.get_ask_balance(&order_id).unwrap()); + assert_eq!(Amount::ZERO, cache.get_give_balance(&order_id).unwrap()); + } + OrdersVersion::V1 => { + assert_eq!( + result.unwrap_err(), + Error::OrderUnderbid(order_id, Amount::from_atoms(1)) + ); + + assert_eq!( + cache.fill_order(order_id, Amount::from_atoms(2), version).unwrap_err(), + Error::OrderUnderbid(order_id, Amount::from_atoms(2)) + ); + + let _ = cache.fill_order(order_id, Amount::from_atoms(3), version).unwrap(); + } + } + + db.batch_write_orders_data(cache.consume()).unwrap(); + + let expected_storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data)]), + BTreeMap::new(), + BTreeMap::new(), + ); + assert_eq!(expected_storage, storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fill_orders_interrupted_by_v0_to_v1_fork(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let ask_atoms = rng.gen_range(1u128..1_000_000_000); + let give_atoms = rng.gen_range(1u128..1_000_000_000); + let ask_amount = Amount::from_atoms(ask_atoms); + let give_amount = Amount::from_atoms(give_atoms); + let fill_orders = test_utils::split_value(&mut rng, ask_atoms) + .into_iter() + .filter(|v| *v > 0) + .collect::>(); + let (fill_orders_v0, fill_orders_v1) = + fill_orders.split_at(rng.gen_range(1..=fill_orders.len())); + + let ask = OutputValue::Coin(ask_amount); + let give = OutputValue::Coin(give_amount); + + let order_id = OrderId::random_using(&mut rng); + let order_data = OrderData::new(Destination::AnyoneCanSpend, ask.clone(), give.clone()); + let mut storage = InMemoryOrdersAccounting::from_values( + BTreeMap::from_iter([(order_id, order_data.clone())]), + BTreeMap::from_iter([(order_id, ask_amount)]), + BTreeMap::from_iter([(order_id, give_amount)]), + ); + let mut db = OrdersAccountingDB::new(&mut storage); + let mut cache = OrdersAccountingCache::new(&db); + + let mut fill_orders = |fills: &[u128], version| { + for fill in fills { + let _ = cache.fill_order(order_id, Amount::from_atoms(*fill), version).unwrap(); + } + }; + + fill_orders(fill_orders_v0, OrdersVersion::V0); + fill_orders(fill_orders_v1, OrdersVersion::V1); + + db.batch_write_orders_data(cache.consume()).unwrap(); +} diff --git a/p2p/src/sync/tests/no_discouragement_after_tx_reorg.rs b/p2p/src/sync/tests/no_discouragement_after_tx_reorg.rs index 38eca3f3d..73a8fc67a 100644 --- a/p2p/src/sync/tests/no_discouragement_after_tx_reorg.rs +++ b/p2p/src/sync/tests/no_discouragement_after_tx_reorg.rs @@ -35,8 +35,8 @@ use common::{ TokenIssuanceV1, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, CoinUnit, DelegationId, - Destination, Genesis, OrderData, OrderId, OutPointSourceId, PoolId, SignedTransaction, - TxInput, TxOutput, UtxoOutPoint, + Destination, Genesis, OrderAccountCommand, OrderData, OrderId, OutPointSourceId, PoolId, + SignedTransaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Idable as _}, }; @@ -265,7 +265,7 @@ async fn no_discouragement_after_tx_reorg(#[case] seed: Seed) { token_id, another_order1_ask_amount_to_fill, ), - tfxt.make_tx_to_conclude_order(order_give_amount, order_id, token_id), + tfxt.make_tx_to_conclude_order(order_id, token_id, order_give_amount), ]; let future_block = tfxt @@ -831,8 +831,13 @@ impl TestFixture { let filled_amount = { let db_tx = self.tfrm.storage.transaction_ro().unwrap(); let orders_db = OrdersAccountingDB::new(&db_tx); - orders_accounting::calculate_fill_order(&orders_db, order_id, coin_amount_to_fill) - .unwrap() + orders_accounting::calculate_fill_order( + &orders_db, + order_id, + coin_amount_to_fill, + common::chain::OrdersVersion::V1, + ) + .unwrap() }; TransactionBuilder::new() @@ -841,14 +846,11 @@ impl TestFixture { InputWitness::NoSignature(None), ) .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::FillOrder( - order_id, - coin_amount_to_fill, - Destination::AnyoneCanSpend, - ), - ), + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + coin_amount_to_fill, + Destination::AnyoneCanSpend, + )), InputWitness::NoSignature(None), ) .add_output(TxOutput::Transfer( @@ -860,20 +862,17 @@ impl TestFixture { fn make_tx_to_conclude_order( &mut self, - give_amount: Amount, order_id: OrderId, token_id: TokenId, + remaining_give_amount: Amount, ) -> SignedTransaction { TransactionBuilder::new() .add_input( - TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)), InputWitness::NoSignature(None), ) .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, give_amount), + OutputValue::TokenV1(token_id, remaining_give_amount), Destination::AnyoneCanSpend, )) .build() diff --git a/test-rpc-functions/src/rpc.rs b/test-rpc-functions/src/rpc.rs index fa3fb1e7c..a8c74c19b 100644 --- a/test-rpc-functions/src/rpc.rs +++ b/test-rpc-functions/src/rpc.rs @@ -513,7 +513,9 @@ impl RpcTestFunctionsRpcServer for super::RpcTestFunctionsHandle { let htlc_outpoint = htlc_outpoint.into_outpoint(); let htlc_position = tx.transaction().inputs().iter().position(|input| match input { TxInput::Utxo(outpoint) => *outpoint == htlc_outpoint, - TxInput::Account(_) | TxInput::AccountCommand(_, _) => false, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => false, }); match htlc_position { diff --git a/trezor-common/src/tests/mod.rs b/trezor-common/src/tests/mod.rs index 363b26259..14be73460 100644 --- a/trezor-common/src/tests/mod.rs +++ b/trezor-common/src/tests/mod.rs @@ -146,6 +146,11 @@ impl From for crate::TxInput { chain::TxInput::AccountCommand(nonce, command) => { Self::AccountCommand(nonce.value(), command.into()) } + chain::TxInput::OrderAccountCommand(_) => { + //TODO: support OrdersVersion::V1 + // https://github.com/mintlayer/mintlayer-core/issues/1902 + unimplemented!(); + } } } } diff --git a/utxo/src/cache.rs b/utxo/src/cache.rs index 4e64b30fb..15645604b 100644 --- a/utxo/src/cache.rs +++ b/utxo/src/cache.rs @@ -154,7 +154,9 @@ impl UtxosCache

{ .iter() .filter_map(|input| match input { TxInput::Utxo(outpoint) => Some(outpoint.source_id()), - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .collect(); @@ -163,7 +165,9 @@ impl UtxosCache

{ .iter() .map(|input| match input { TxInput::Utxo(outpoint) => self.spend_utxo(outpoint).map(Some), - TxInput::Account(..) | TxInput::AccountCommand(..) => Ok(None), + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => Ok(None), }) .collect::, Error>>()?; @@ -194,7 +198,9 @@ impl UtxosCache

{ .filter_map(|(input, undo)| { undo.map(|utxo| match input { TxInput::Utxo(outpoint) => Ok((outpoint, utxo)), - TxInput::Account(..) | TxInput::AccountCommand(..) => { + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => { Err(Error::TxInputAndUndoMismatch(tx.get_id())) } }) @@ -220,7 +226,9 @@ impl UtxosCache

{ .iter() .filter_map(|input| match input { TxInput::Utxo(outpoint) => Some(self.spend_utxo(outpoint)), - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .collect::, _>>()?; (!utxos.is_empty()).then(|| UtxosBlockRewardUndo::new(utxos)) @@ -263,7 +271,9 @@ impl UtxosCache

{ .zip(block_undo.into_inner().into_iter()) .filter_map(|(tx_in, utxo)| match tx_in { TxInput::Utxo(outpoint) => Some((outpoint, utxo)), - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) + | TxInput::AccountCommand(..) + | TxInput::OrderAccountCommand(..) => None, }) .try_for_each(|(outpoint, utxo)| self.add_utxo(outpoint, utxo, false))?; } diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index fe7e49c37..dfe1738d0 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -22,7 +22,10 @@ use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; use common::chain::htlc::HashedTimelockContract; -use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, OrderId, RpcOrderInfo}; +use common::chain::{ + AccountCommand, AccountOutPoint, AccountSpending, OrderAccountCommand, OrderId, OrdersVersion, + RpcOrderInfo, +}; use common::primitives::id::WithId; use common::primitives::{Idable, H256}; use common::size_estimation::{ @@ -981,14 +984,31 @@ impl Account { outputs.push(TxOutput::Transfer(output_value, output_destination)); } - let nonce = order_info - .nonce - .map_or(Some(AccountNonce::new(0)), |n| n.increment()) - .ok_or(WalletError::OrderNonceOverflow(order_id))?; - let request = SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( - TxInput::AccountCommand(nonce, AccountCommand::ConcludeOrder(order_id)), - order_info.conclude_key.clone(), - )]); + let version = self + .chain_config + .chainstate_upgrades() + .version_at_height(self.account_info.best_block_height().next_height()) + .1 + .orders_version(); + + let request = match version { + common::chain::OrdersVersion::V0 => { + let nonce = order_info + .nonce + .map_or(Some(AccountNonce::new(0)), |n| n.increment()) + .ok_or(WalletError::OrderNonceOverflow(order_id))?; + SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( + TxInput::AccountCommand(nonce, AccountCommand::ConcludeOrder(order_id)), + order_info.conclude_key.clone(), + )]) + } + common::chain::OrdersVersion::V1 => { + SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)), + order_info.conclude_key.clone(), + )]) + } + }; self.select_inputs_for_send_request( request, @@ -1019,33 +1039,66 @@ impl Account { self.get_new_address(db_tx, KeyPurpose::ReceiveFunds)?.1.into_object() }; - let filled_amount = orders_accounting::calculate_filled_amount( - order_info.ask_balance, - order_info.give_balance, - fill_amount_in_ask_currency, - ) - .ok_or(WalletError::CalculateOrderFilledAmountFailed(order_id))?; - let output_value = match order_info.initially_given { - RpcOutputValue::Coin { .. } => OutputValue::Coin(filled_amount), - RpcOutputValue::Token { id, .. } => OutputValue::TokenV1(id, filled_amount), - }; - let outputs = vec![TxOutput::Transfer(output_value, output_destination.clone())]; - - let nonce = order_info - .nonce - .map_or(Some(AccountNonce::new(0)), |n| n.increment()) - .ok_or(WalletError::OrderNonceOverflow(order_id))?; - let request = SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( - TxInput::AccountCommand( - nonce, - AccountCommand::FillOrder( - order_id, + let version = self + .chain_config + .chainstate_upgrades() + .version_at_height(self.account_info.best_block_height().next_height()) + .1 + .orders_version(); + + let request = match version { + common::chain::OrdersVersion::V0 => { + let filled_amount = orders_accounting::calculate_filled_amount( + order_info.ask_balance, + order_info.give_balance, fill_amount_in_ask_currency, - output_destination.clone(), - ), - ), - output_destination, - )]); + ) + .ok_or(WalletError::CalculateOrderFilledAmountFailed(order_id))?; + let output_value = match order_info.initially_given { + RpcOutputValue::Coin { .. } => OutputValue::Coin(filled_amount), + RpcOutputValue::Token { id, .. } => OutputValue::TokenV1(id, filled_amount), + }; + let outputs = vec![TxOutput::Transfer(output_value, output_destination.clone())]; + + let nonce = order_info + .nonce + .map_or(Some(AccountNonce::new(0)), |n| n.increment()) + .ok_or(WalletError::OrderNonceOverflow(order_id))?; + SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( + TxInput::AccountCommand( + nonce, + AccountCommand::FillOrder( + order_id, + fill_amount_in_ask_currency, + output_destination.clone(), + ), + ), + output_destination, + )]) + } + common::chain::OrdersVersion::V1 => { + let filled_amount = orders_accounting::calculate_filled_amount( + order_info.initially_asked.amount(), + order_info.initially_given.amount(), + fill_amount_in_ask_currency, + ) + .ok_or(WalletError::CalculateOrderFilledAmountFailed(order_id))?; + let output_value = match order_info.initially_given { + RpcOutputValue::Coin { .. } => OutputValue::Coin(filled_amount), + RpcOutputValue::Token { id, .. } => OutputValue::TokenV1(id, filled_amount), + }; + let outputs = vec![TxOutput::Transfer(output_value, output_destination.clone())]; + + SendRequest::new().with_outputs(outputs).with_inputs_and_destinations([( + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount_in_ask_currency, + output_destination.clone(), + )), + output_destination, + )]) + } + }; self.select_inputs_for_send_request( request, @@ -1470,6 +1523,20 @@ impl Account { } } + pub fn find_order_account_command_destination( + &self, + cmd: &OrderAccountCommand, + ) -> WalletResult { + match cmd { + OrderAccountCommand::FillOrder(_, _, destination) => Ok(destination.clone()), + OrderAccountCommand::ConcludeOrder(order_id) => self + .output_cache + .order_data(order_id) + .map(|data| data.conclude_key.clone()) + .ok_or(WalletError::UnknownOrderId(*order_id)), + } + } + pub fn find_unspent_utxo_and_destination( &self, outpoint: &UtxoOutPoint, @@ -1902,6 +1969,12 @@ impl Account { self.find_delegation(delegation_id).is_ok() } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, dest) => { + self.find_order(order_id).is_ok() || self.is_destination_mine_or_watched(dest) + } + OrderAccountCommand::ConcludeOrder(order_id) => self.find_order(order_id).is_ok(), + }, TxInput::AccountCommand(_, op) => match op { AccountCommand::MintTokens(token_id, _) | AccountCommand::UnmintTokens(token_id) @@ -2453,59 +2526,41 @@ fn group_preselected_inputs( )?; } AccountCommand::ConcludeOrder(order_id) => { - let order_info = order_info - .as_ref() - .and_then(|info| info.get(order_id)) - .ok_or(WalletError::OrderInfoMissing(*order_id))?; - - let given_currency = - Currency::from_rpc_output_value(&order_info.initially_given); - update_preselected_inputs( - given_currency, - order_info.give_balance, - Amount::ZERO, - Amount::ZERO, - )?; - - let asked_currency = - Currency::from_rpc_output_value(&order_info.initially_asked); - let filled_amount = (order_info.initially_asked.amount() - - order_info.ask_balance) - .ok_or(WalletError::OutputAmountOverflow)?; - update_preselected_inputs( - asked_currency, - filled_amount, - Amount::ZERO, - Amount::ZERO, + handle_conclude_order( + *order_id, + order_info.as_ref(), + *fee, + &mut update_preselected_inputs, )?; - - // add fee - update_preselected_inputs(Currency::Coin, Amount::ZERO, *fee, Amount::ZERO)?; } AccountCommand::FillOrder(order_id, fill_amount_in_ask_currency, _) => { - let order_info = order_info - .as_ref() - .and_then(|info| info.get(order_id)) - .ok_or(WalletError::OrderInfoMissing(*order_id))?; - - let filled_amount = orders_accounting::calculate_filled_amount( - order_info.ask_balance, - order_info.give_balance, + handle_fill_order_op( + *order_id, *fill_amount_in_ask_currency, - ) - .ok_or(WalletError::CalculateOrderFilledAmountFailed(*order_id))?; - - let given_currency = - Currency::from_rpc_output_value(&order_info.initially_given); - update_preselected_inputs(given_currency, filled_amount, *fee, Amount::ZERO)?; - - let asked_currency = - Currency::from_rpc_output_value(&order_info.initially_asked); - update_preselected_inputs( - asked_currency, - Amount::ZERO, - Amount::ZERO, + order_info.as_ref(), + *fee, + &mut update_preselected_inputs, + OrdersVersion::V0, + )?; + } + }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(id, fill_amount_in_ask_currency, _) => { + handle_fill_order_op( + *id, *fill_amount_in_ask_currency, + order_info.as_ref(), + *fee, + &mut update_preselected_inputs, + OrdersVersion::V1, + )?; + } + OrderAccountCommand::ConcludeOrder(order_id) => { + handle_conclude_order( + *order_id, + order_info.as_ref(), + *fee, + &mut update_preselected_inputs, )?; } }, @@ -2514,6 +2569,75 @@ fn group_preselected_inputs( Ok(preselected_inputs) } +fn handle_fill_order_op( + order_id: OrderId, + fill_amount_in_ask_currency: Amount, + order_info: Option<&BTreeMap>, + fee: Amount, + update_preselected_inputs: &mut impl FnMut(Currency, Amount, Amount, Amount) -> WalletResult<()>, + version: OrdersVersion, +) -> WalletResult<()> { + let order_info = order_info + .as_ref() + .and_then(|info| info.get(&order_id)) + .ok_or(WalletError::OrderInfoMissing(order_id))?; + + let filled_amount = match version { + OrdersVersion::V0 => orders_accounting::calculate_filled_amount( + order_info.ask_balance, + order_info.give_balance, + fill_amount_in_ask_currency, + ), + OrdersVersion::V1 => orders_accounting::calculate_filled_amount( + order_info.initially_asked.amount(), + order_info.initially_given.amount(), + fill_amount_in_ask_currency, + ), + } + .ok_or(WalletError::CalculateOrderFilledAmountFailed(order_id))?; + + let given_currency = Currency::from_rpc_output_value(&order_info.initially_given); + update_preselected_inputs(given_currency, filled_amount, fee, Amount::ZERO)?; + + let asked_currency = Currency::from_rpc_output_value(&order_info.initially_asked); + update_preselected_inputs( + asked_currency, + Amount::ZERO, + Amount::ZERO, + fill_amount_in_ask_currency, + )?; + Ok(()) +} + +fn handle_conclude_order( + order_id: OrderId, + order_info: Option<&BTreeMap>, + fee: Amount, + update_preselected_inputs: &mut impl FnMut(Currency, Amount, Amount, Amount) -> WalletResult<()>, +) -> WalletResult<()> { + let order_info = order_info + .as_ref() + .and_then(|info| info.get(&order_id)) + .ok_or(WalletError::OrderInfoMissing(order_id))?; + + let given_currency = Currency::from_rpc_output_value(&order_info.initially_given); + update_preselected_inputs( + given_currency, + order_info.give_balance, + Amount::ZERO, + Amount::ZERO, + )?; + + let asked_currency = Currency::from_rpc_output_value(&order_info.initially_asked); + let filled_amount = (order_info.initially_asked.amount() - order_info.ask_balance) + .ok_or(WalletError::OutputAmountOverflow)?; + update_preselected_inputs(asked_currency, filled_amount, Amount::ZERO, Amount::ZERO)?; + + // add fee + update_preselected_inputs(Currency::Coin, Amount::ZERO, fee, Amount::ZERO)?; + Ok(()) +} + /// Calculate the amount of fee that needs to be paid to add a change output /// Returns the Amounts for Coin output and Token output fn coin_and_token_output_change_fees( diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 3105169de..d673ca5c5 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -31,7 +31,8 @@ use common::{ TokenId, TokenIssuance, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountSpending, DelegationId, Destination, GenBlock, - OrderId, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, + OrderAccountCommand, OrderId, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, + UtxoOutPoint, }, primitives::{id::WithId, per_thousand::PerThousand, Amount, BlockHeight, Id, Idable}, }; @@ -746,7 +747,7 @@ impl OutputCache { } let frozen_token_id = tx.inputs().iter().find_map(|inp| match inp { - TxInput::Utxo(_) | TxInput::Account(_) => None, + TxInput::Utxo(_) | TxInput::Account(_) | TxInput::OrderAccountCommand(_) => None, TxInput::AccountCommand(_, cmd) => match cmd { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -831,6 +832,17 @@ impl OutputCache { }) } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + self.order_data(order_id).is_some_and(|data| { + [data.ask_currency, data.give_currency].iter().any(|v| match v { + Currency::Coin => false, + Currency::Token(token_id) => frozen_token_id == token_id, + }) + }) + } + }, TxInput::Account(_) => false, }) } @@ -1039,7 +1051,23 @@ impl OutputCache { &mut self.unconfirmed_descendants, data, order_id, - *nonce, + Some(*nonce), + tx_id, + )?; + } + } + } + }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + if !already_present { + if let Some(data) = self.orders.get_mut(order_id) { + Self::update_order_state( + &mut self.unconfirmed_descendants, + data, + order_id, + None, tx_id, )?; } @@ -1118,20 +1146,23 @@ impl OutputCache { unconfirmed_descendants: &mut BTreeMap>, data: &mut OrderData, order_id: &OrderId, - nonce: AccountNonce, + nonce: Option, tx_id: &OutPointSourceId, ) -> Result<(), WalletError> { - let next_nonce = data - .last_nonce - .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) - .ok_or(WalletError::OrderNonceOverflow(*order_id))?; + if let Some(nonce) = nonce { + let next_nonce = data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::OrderNonceOverflow(*order_id))?; - ensure!( - nonce == next_nonce, - WalletError::InconsistentOrderDuplicateNonce(*order_id, nonce) - ); + ensure!( + nonce == next_nonce, + WalletError::InconsistentOrderDuplicateNonce(*order_id, nonce) + ); + + data.last_nonce = Some(nonce); + } - data.last_nonce = Some(nonce); // update unconfirmed descendants if let Some(descendants) = data .last_parent @@ -1186,6 +1217,15 @@ impl OutputCache { } } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + if let Some(data) = self.orders.get_mut(order_id) { + data.last_parent = + find_parent(&self.unconfirmed_descendants, tx_id.clone()); + } + } + }, } } for output in tx.outputs() { @@ -1389,7 +1429,9 @@ impl OutputCache { get_all_tx_output_destinations(txo, &|pool_id| self.pools.get(pool_id)) }) .is_some_and(|output_dest| output_dest.contains(dest)), - TxInput::Account(_) | TxInput::AccountCommand(_, _) => false, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => false, }) } @@ -1473,6 +1515,17 @@ impl OutputCache { } } }, + TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + if let Some(data) = self.orders.get_mut(order_id) { + data.last_parent = find_parent( + &self.unconfirmed_descendants, + tx_id.into(), + ); + } + } + }, } } Ok(()) @@ -1517,7 +1570,9 @@ impl OutputCache { is_mine: &F, ) -> Option { match inp { - TxInput::Account(_) | TxInput::AccountCommand(_, _) => None, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => None, TxInput::Utxo(outpoint) => self .txs .get(&outpoint.source_id()) @@ -1682,6 +1737,7 @@ fn apply_freeze_mutations_from_tx( | AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => {} }, + TxInput::OrderAccountCommand(..) => {} } } @@ -1724,6 +1780,7 @@ fn apply_total_supply_mutations_from_tx( | AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => {} }, + TxInput::OrderAccountCommand(..) => {} } } diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index 0d4089f2f..a2b47be0b 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -141,7 +141,9 @@ fn own_input<'a>( .filter(|&output| own_output(key_chain, output)), None => None, }, - TxInput::Account(..) | TxInput::AccountCommand(..) => None, + TxInput::Account(..) | TxInput::AccountCommand(..) | TxInput::OrderAccountCommand(..) => { + None + } } } diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 007b0922d..68fde28cf 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -563,6 +563,10 @@ fn to_trezor_input_msgs( ptx.additional_info(), ) } + (TxInput::OrderAccountCommand(_), _, _) => { + //TODO: support OrdersVersion::V1 + unimplemented!(); + } (_, _, None) => Err(SignerError::MissingDestinationInTransaction), (TxInput::Utxo(_), _, _) => Err(SignerError::MissingUtxo), }) @@ -852,7 +856,9 @@ fn to_trezor_utxo_msgs( let out = to_trezor_output_msg(chain_config, utxo, ptx.additional_info())?; utxos.entry(id).or_default().insert(outpoint.output_index(), out); } - TxInput::Account(_) | TxInput::AccountCommand(_, _) => {} + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => {} } } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 55920e1aa..7f12fad1d 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -47,9 +47,9 @@ use common::chain::tokens::{ }; use common::chain::{ make_order_id, AccountCommand, AccountNonce, AccountOutPoint, Block, ChainConfig, DelegationId, - Destination, GenBlock, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, SignedTransaction, - SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, TxOutput, - UtxoOutPoint, + Destination, GenBlock, OrderAccountCommand, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, + SignedTransaction, SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, + TxOutput, UtxoOutPoint, }; use common::primitives::id::{hash_encoded, WithId}; use common::primitives::{Amount, BlockHeight, Id, H256}; @@ -1210,6 +1210,15 @@ where .find_map(|acc| acc.find_account_command_destination(cmd).ok()) } + pub fn find_order_account_command_destination( + &self, + cmd: &OrderAccountCommand, + ) -> Option { + self.accounts + .values() + .find_map(|acc| acc.find_order_account_command_destination(cmd).ok()) + } + pub fn find_unspent_utxo_and_destination( &self, outpoint: &UtxoOutPoint, diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 6e2ce0c24..d0334d1f2 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -6454,4 +6454,3 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { ); } } -// create order, fill partially, conclude diff --git a/wallet/wallet-controller/src/helpers.rs b/wallet/wallet-controller/src/helpers.rs index 3878379a6..5c8f20fa7 100644 --- a/wallet/wallet-controller/src/helpers.rs +++ b/wallet/wallet-controller/src/helpers.rs @@ -22,8 +22,8 @@ use common::{ chain::{ output_value::OutputValue, tokens::{RPCTokenInfo, TokenId}, - AccountCommand, ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, - UtxoOutPoint, + AccountCommand, ChainConfig, Destination, OrderAccountCommand, OrderId, PoolId, + Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{amount::RpcAmountOut, Amount}, }; @@ -286,47 +286,16 @@ async fn into_utxo_and_destination( (Some(utxo), additional_infos, Some(dest)) } TxInput::Account(acc_outpoint) => { - // find delegation destination let dest = wallet.find_account_destination(acc_outpoint); (None, TxAdditionalInfo::new(), dest) } TxInput::AccountCommand(_, cmd) => { - // find authority of the token let dest = wallet.find_account_command_destination(cmd); let additional_infos = match cmd { AccountCommand::FillOrder(order_id, _, _) | AccountCommand::ConcludeOrder(order_id) => { - let order_info = rpc_client - .get_order_info(*order_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::OrderInfoMissing( - *order_id, - )))?; - - let ask_token_info = fetch_token_extra_info( - rpc_client, - &Currency::from_rpc_output_value(&order_info.initially_asked) - .into_output_value(order_info.ask_balance), - ) - .await?; - let give_token_info = fetch_token_extra_info( - rpc_client, - &Currency::from_rpc_output_value(&order_info.initially_given) - .into_output_value(order_info.give_balance), - ) - .await?; - - ask_token_info.join(give_token_info).join(TxAdditionalInfo::with_order_info( - *order_id, - OrderAdditionalInfo { - initially_asked: order_info.initially_asked.into(), - initially_given: order_info.initially_given.into(), - ask_balance: order_info.ask_balance, - give_balance: order_info.give_balance, - }, - )) + fetch_order_additional_info(rpc_client, *order_id).await? } AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -338,5 +307,54 @@ async fn into_utxo_and_destination( }; (None, additional_infos, dest) } + TxInput::OrderAccountCommand(cmd) => { + let dest = wallet.find_order_account_command_destination(cmd); + + let additional_infos = match cmd { + OrderAccountCommand::FillOrder(order_id, _, _) + | OrderAccountCommand::ConcludeOrder(order_id) => { + fetch_order_additional_info(rpc_client, *order_id).await? + } + }; + + (None, additional_infos, dest) + } }) } + +async fn fetch_order_additional_info( + rpc_client: &T, + order_id: OrderId, +) -> Result> { + let order_info = rpc_client + .get_order_info(order_id) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError(WalletError::OrderInfoMissing( + order_id, + )))?; + + let ask_token_info = fetch_token_extra_info( + rpc_client, + &Currency::from_rpc_output_value(&order_info.initially_asked) + .into_output_value(order_info.ask_balance), + ) + .await?; + let give_token_info = fetch_token_extra_info( + rpc_client, + &Currency::from_rpc_output_value(&order_info.initially_given) + .into_output_value(order_info.give_balance), + ) + .await?; + + let result = ask_token_info.join(give_token_info).join(TxAdditionalInfo::with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.into(), + initially_given: order_info.initially_given.into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + )); + Ok(result) +} diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 51fddc667..0a2b5b880 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -955,7 +955,7 @@ where .filter_map(|inp| match inp { TxInput::Utxo(utxo) => Some(utxo.clone()), TxInput::Account(_) => None, - TxInput::AccountCommand(_, _) => None, + TxInput::AccountCommand(_, _) | TxInput::OrderAccountCommand(_) => None, }) .collect(); let fees = match self.fetch_utxos(&inputs).await { @@ -1172,8 +1172,9 @@ where ) -> Result, ControllerError> { match input { TxInput::Utxo(utxo) => fetch_utxo(&self.rpc_client, utxo, &self.wallet).await.map(Some), - TxInput::Account(_) => Ok(None), - TxInput::AccountCommand(_, _) => Ok(None), + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => Ok(None), } } diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index d4f3577f1..5df471bba 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -23,9 +23,9 @@ use common::{ output_value::OutputValue, signature::inputsig::arbitrary_message::ArbitraryMessageSignature, tokens::{IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance}, - AccountCommand, AccountOutPoint, DelegationId, Destination, GenBlock, OrderId, PoolId, - RpcOrderInfo, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, - UtxoOutPoint, + AccountCommand, AccountOutPoint, DelegationId, Destination, GenBlock, OrderAccountCommand, + OrderId, PoolId, RpcOrderInfo, SignedTransaction, SignedTransactionIntent, Transaction, + TxOutput, UtxoOutPoint, }, primitives::{id::WithId, Amount, BlockHeight, Id, H256}, }; @@ -96,6 +96,17 @@ impl RuntimeWallet { } } + pub fn find_order_account_command_destination( + &self, + cmd: &OrderAccountCommand, + ) -> Option { + match self { + RuntimeWallet::Software(w) => w.find_order_account_command_destination(cmd), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_order_account_command_destination(cmd), + } + } + pub fn seed_phrase(&self) -> Result, WalletError> { match self { RuntimeWallet::Software(w) => w.seed_phrase(), diff --git a/wasm-wrappers/src/lib.rs b/wasm-wrappers/src/lib.rs index d9147703d..f16215a96 100644 --- a/wasm-wrappers/src/lib.rs +++ b/wasm-wrappers/src/lib.rs @@ -1006,7 +1006,9 @@ pub fn extract_htlc_secret( .iter() .position(|input| match input { TxInput::Utxo(outpoint) => *outpoint == htlc_utxo_outpoint, - TxInput::Account(_) | TxInput::AccountCommand(_, _) => false, + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => false, }) .ok_or(Error::NoInputOutpointFound)?;