diff --git a/core/src/box_kind.rs b/core/src/box_kind.rs index 179f963f..4714abab 100644 --- a/core/src/box_kind.rs +++ b/core/src/box_kind.rs @@ -2,8 +2,10 @@ mod ballot_box; mod oracle_box; mod pool_box; mod refresh_box; +mod update_box; pub use ballot_box::*; pub use oracle_box::*; pub use pool_box::*; pub use refresh_box::*; +pub use update_box::*; diff --git a/core/src/box_kind/ballot_box.rs b/core/src/box_kind/ballot_box.rs index 801a3d8d..ac7f3890 100644 --- a/core/src/box_kind/ballot_box.rs +++ b/core/src/box_kind/ballot_box.rs @@ -1,5 +1,3 @@ -use std::convert::TryInto; - use crate::{ contracts::ballot::{BallotContract, BallotContractError}, oracle_config::{BallotBoxWrapperParameters, CastBallotBoxVoteParameters}, @@ -59,7 +57,7 @@ pub trait BallotBox { fn get_box(&self) -> &ErgoBox; } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct BallotBoxWrapper { ergo_box: ErgoBox, contract: BallotContract, @@ -67,15 +65,6 @@ pub struct BallotBoxWrapper { impl BallotBoxWrapper { pub fn new(ergo_box: ErgoBox, inputs: BallotBoxWrapperInputs) -> Result { - let CastBallotBoxVoteParameters { - reward_token_id, - reward_token_quantity, - pool_box_address_hash, - } = inputs - .parameters - .vote_parameters - .as_ref() - .ok_or(BallotBoxError::ExpectedVoteCast)?; let ballot_token_id = &ergo_box .tokens .as_ref() @@ -98,42 +87,54 @@ impl BallotBoxWrapper { return Err(BallotBoxError::UnexpectedGroupElementInR4); } - if ergo_box - .get_register(NonMandatoryRegisterId::R5.into()) - .ok_or(BallotBoxError::NoUpdateBoxCreationHeightInR5)? - .try_extract_into::() - .is_err() + if let Some(CastBallotBoxVoteParameters { + reward_token_id, + reward_token_quantity, + pool_box_address_hash, + update_box_creation_height, + }) = inputs.parameters.vote_parameters.as_ref() { - return Err(BallotBoxError::NoUpdateBoxCreationHeightInR5); - } + let register_update_box_creation_height = ergo_box + .get_register(NonMandatoryRegisterId::R5.into()) + .ok_or(BallotBoxError::NoUpdateBoxCreationHeightInR5)? + .try_extract_into::()?; - let register_pool_box_address_hash = ergo_box - .get_register(NonMandatoryRegisterId::R6.into()) - .ok_or(BallotBoxError::NoPoolBoxAddressInR6)? - .try_extract_into::()?; - let pb: Digest32 = base16::decode(pool_box_address_hash) - .unwrap() - .try_into() - .unwrap(); - if pb != register_pool_box_address_hash { - warn!("Pool box address in R6 register differs to config. Could be due to vote."); - } + if register_update_box_creation_height != *update_box_creation_height { + warn!("Update box creation height in R5 register differs to config. Could be due to vote."); + } - let register_reward_token_id = ergo_box - .get_register(NonMandatoryRegisterId::R7.into()) - .ok_or(BallotBoxError::NoRewardTokenIdInR7)? - .try_extract_into::()?; - if register_reward_token_id != *reward_token_id { - warn!("Reward token id in R7 register differs to config. Could be due to vote."); - } + let register_pool_box_address_hash = ergo_box + .get_register(NonMandatoryRegisterId::R6.into()) + .ok_or(BallotBoxError::NoPoolBoxAddressInR6)? + .try_extract_into::()?; - let register_reward_token_quantity = ergo_box - .get_register(NonMandatoryRegisterId::R8.into()) - .ok_or(BallotBoxError::NoRewardTokenQuantityInR8)? - .try_extract_into::()? as u32; + if *pool_box_address_hash != register_pool_box_address_hash { + warn!("Pool box address in R6 register differs to config. Could be due to vote."); + } + + let register_reward_token_id = ergo_box + .get_register(NonMandatoryRegisterId::R7.into()) + .ok_or(BallotBoxError::NoRewardTokenIdInR7)? + .try_extract_into::()?; + + if register_reward_token_id != *reward_token_id { + warn!("Reward token id in R7 register differs to config. Could be due to vote."); + } - if register_reward_token_quantity != *reward_token_quantity { - warn!("Reward token quantity in R8 register differs to config. Could be due to vote."); + let register_reward_token_quantity: u64 = ergo_box + .get_register(NonMandatoryRegisterId::R8.into()) + .ok_or(BallotBoxError::NoRewardTokenQuantityInR8)? + .try_extract_into::()? + as u64; + + if register_reward_token_quantity != *reward_token_quantity { + warn!( + "Reward token quantity in R8 register differs to config. Could be due to vote." + ); + } + } else if ergo_box.additional_registers.len() > 1 { + // If no vote parameter is provided, then the box must only have R4 (owner public key) defined + return Err(BallotBoxError::ExpectedVoteCast); } let contract = BallotContract::from_ergo_tree(ergo_box.ergo_tree.clone(), inputs.into())?; @@ -150,6 +151,73 @@ pub struct BallotBoxWrapperInputs<'a> { pub update_nft_token_id: &'a TokenId, } +/// A Ballot Box with vote parameters guaranteed to be set +#[derive(Clone, Debug)] +pub struct VoteBallotBoxWrapper { + ergo_box: ErgoBox, + vote_parameters: CastBallotBoxVoteParameters, + contract: BallotContract, +} + +impl VoteBallotBoxWrapper { + pub fn new(ergo_box: ErgoBox, inputs: BallotBoxWrapperInputs) -> Result { + let ballot_token_id = &ergo_box + .tokens + .as_ref() + .ok_or(BallotBoxError::NoBallotToken)? + .get(0) + .ok_or(BallotBoxError::NoBallotToken)? + .token_id; + if *ballot_token_id != *inputs.ballot_token_id { + return Err(BallotBoxError::UnknownBallotTokenId); + } + + if ergo_box + .get_register(NonMandatoryRegisterId::R4.into()) + .ok_or(BallotBoxError::NoGroupElementInR4)? + .try_extract_into::() + .is_err() + { + return Err(BallotBoxError::NoGroupElementInR4); + } + let update_box_creation_height = ergo_box + .get_register(NonMandatoryRegisterId::R5.into()) + .ok_or(BallotBoxError::NoUpdateBoxCreationHeightInR5)? + .try_extract_into::()?; + + let pool_box_address_hash = ergo_box + .get_register(NonMandatoryRegisterId::R6.into()) + .ok_or(BallotBoxError::NoPoolBoxAddressInR6)? + .try_extract_into::()?; + + let reward_token_id = ergo_box + .get_register(NonMandatoryRegisterId::R7.into()) + .ok_or(BallotBoxError::NoRewardTokenIdInR7)? + .try_extract_into::()?; + let reward_token_quantity = ergo_box + .get_register(NonMandatoryRegisterId::R8.into()) + .ok_or(BallotBoxError::NoRewardTokenQuantityInR8)? + .try_extract_into::()? as u64; + + let contract = BallotContract::from_ergo_tree(ergo_box.ergo_tree.clone(), inputs.into())?; + let vote_parameters = CastBallotBoxVoteParameters { + pool_box_address_hash, + reward_token_id, + reward_token_quantity, + update_box_creation_height, + }; + Ok(Self { + ergo_box, + contract, + vote_parameters, + }) + } + + pub fn vote_parameters(&self) -> &CastBallotBoxVoteParameters { + &self.vote_parameters + } +} + impl BallotBox for BallotBoxWrapper { fn contract(&self) -> &BallotContract { &self.contract @@ -183,6 +251,39 @@ impl BallotBox for BallotBoxWrapper { } } +impl BallotBox for VoteBallotBoxWrapper { + fn contract(&self) -> &BallotContract { + &self.contract + } + + fn ballot_token(&self) -> Token { + self.ergo_box + .tokens + .as_ref() + .unwrap() + .get(0) + .unwrap() + .clone() + } + + fn min_storage_rent(&self) -> u64 { + self.contract.min_storage_rent() + } + + fn ballot_token_owner(&self) -> ProveDlog { + self.ergo_box + .get_register(NonMandatoryRegisterId::R4.into()) + .unwrap() + .try_extract_into::() + .unwrap() + .into() + } + + fn get_box(&self) -> &ErgoBox { + &self.ergo_box + } +} + #[allow(clippy::too_many_arguments)] pub fn make_local_ballot_box_candidate( contract: &BallotContract, @@ -210,7 +311,7 @@ pub fn make_local_ballot_box_candidate( ); builder.set_register_value( NonMandatoryRegisterId::R8, - (*reward_tokens.amount.as_u64() as i32).into(), + (*reward_tokens.amount.as_u64() as i64).into(), ); builder.add_token(ballot_token); builder.build() diff --git a/core/src/box_kind/update_box.rs b/core/src/box_kind/update_box.rs new file mode 100644 index 00000000..7be39e85 --- /dev/null +++ b/core/src/box_kind/update_box.rs @@ -0,0 +1,70 @@ +use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox; +use ergo_lib::ergotree_ir::chain::token::Token; +use ergo_lib::ergotree_ir::chain::token::TokenId; +use ergo_lib::ergotree_ir::ergo_tree::ErgoTree; +use thiserror::Error; + +use crate::contracts::update::UpdateContract; +use crate::contracts::update::UpdateContractError; +use crate::contracts::update::UpdateContractParameters; + +#[derive(Debug, Error)] +pub enum UpdateBoxError { + #[error("oracle box: no tokens found")] + NoTokens, + #[error("update contract: {0:?}")] + UpdateContractError(#[from] UpdateContractError), + #[error("update contract: {0:?}")] + IncorrectUpdateTokenId(TokenId), +} + +#[derive(Clone)] +pub struct UpdateBoxWrapper(ErgoBox, UpdateContract); + +impl UpdateBoxWrapper { + pub fn new(b: ErgoBox, inputs: UpdateBoxWrapperInputs) -> Result { + let update_token_id = b + .tokens + .as_ref() + .ok_or(UpdateBoxError::NoTokens)? + .get(0) + .ok_or(UpdateBoxError::NoTokens)? + .token_id + .clone(); + if update_token_id != *inputs.update_nft_token_id { + return Err(UpdateBoxError::IncorrectUpdateTokenId(update_token_id)); + } + let contract = UpdateContract::from_ergo_tree(b.ergo_tree.clone(), inputs.into())?; + + Ok(Self(b, contract)) + } + pub fn ergo_tree(&self) -> ErgoTree { + self.1.ergo_tree() + } + pub fn update_nft(&self) -> Token { + self.0.tokens.as_ref().unwrap().get(0).unwrap().clone() + } + pub fn ballot_token_id(&self) -> TokenId { + self.1.ballot_token_id().clone() + } + pub fn get_box(&self) -> &ErgoBox { + &self.0 + } + pub fn min_votes(&self) -> u32 { + self.1.min_votes() as u32 + } +} + +#[derive(Debug, Copy, Clone)] +pub struct UpdateBoxWrapperInputs<'a> { + pub contract_parameters: &'a UpdateContractParameters, + pub update_nft_token_id: &'a TokenId, + pub ballot_token_id: &'a TokenId, + pub pool_nft_token_id: &'a TokenId, +} + +impl From for ErgoBox { + fn from(w: UpdateBoxWrapper) -> Self { + w.0.clone() + } +} diff --git a/core/src/cli_commands.rs b/core/src/cli_commands.rs index f718ede7..831c033b 100644 --- a/core/src/cli_commands.rs +++ b/core/src/cli_commands.rs @@ -2,8 +2,10 @@ use ergo_lib::ergotree_ir::chain::address::NetworkPrefix; pub mod bootstrap; pub mod extract_reward_tokens; +pub mod prepare_update; pub mod print_reward_tokens; pub mod transfer_oracle_token; +pub mod update_pool; pub mod vote_update_pool; pub(crate) fn ergo_explorer_transaction_link(tx_id_str: String, prefix: NetworkPrefix) -> String { @@ -11,6 +13,7 @@ pub(crate) fn ergo_explorer_transaction_link(tx_id_str: String, prefix: NetworkP NetworkPrefix::Mainnet => "explorer", NetworkPrefix::Testnet => "testnet", }; + let tx_id_str = tx_id_str.replace('"', ""); // Node interface returns Tx Id as a JSON string "TxId" format!( "https://{}.ergoplatform.com/en/transactions/{}", prefix_str, tx_id_str diff --git a/core/src/cli_commands/bootstrap.rs b/core/src/cli_commands/bootstrap.rs index 7bbea98b..77e36b4c 100644 --- a/core/src/cli_commands/bootstrap.rs +++ b/core/src/cli_commands/bootstrap.rs @@ -9,7 +9,9 @@ use ergo_lib::{ }, ergotree_ir::{ chain::{ - address::{Address, AddressEncoder, AddressEncoderError, NetworkPrefix}, + address::{ + Address, AddressEncoder, AddressEncoderError, NetworkAddress, NetworkPrefix, + }, ergo_box::{ box_value::{BoxValue, BoxValueError}, ErgoBox, @@ -89,12 +91,52 @@ pub fn bootstrap(config_file_name: String) -> Result<(), BootstrapError> { Ok(()) } -pub fn generate_bootstrap_config_template(config_file_name: String) -> Result<(), BootstrapError> { +pub fn generate_bootstrap_config_template( + config_file_name: String, + testnet: bool, +) -> Result<(), BootstrapError> { if Path::new(&config_file_name).exists() { return Err(BootstrapError::ConfigFilenameAlreadyExists); } let address = AddressEncoder::new(NetworkPrefix::Mainnet) .parse_address_from_str("9hEQHEMyY1K1vs79vJXFtNjr2dbQbtWXF99oVWGJ5c4xbcLdBsw")?; + + let refresh_contract_parameters = if !testnet { + RefreshContractParameters::default() + } else { + let default = RefreshContractParameters::default(); + RefreshContractParameters { + p2s: NetworkAddress::new(NetworkPrefix::Testnet, &default.p2s.address()), + ..default + } + }; + let pool_contract_parameters = if !testnet { + PoolContractParameters::default() + } else { + let default = PoolContractParameters::default(); + PoolContractParameters { + p2s: NetworkAddress::new(NetworkPrefix::Testnet, &default.p2s.address()), + ..default + } + }; + let update_contract_parameters = if !testnet { + UpdateContractParameters::default() + } else { + let default = UpdateContractParameters::default(); + UpdateContractParameters { + p2s: NetworkAddress::new(NetworkPrefix::Testnet, &default.p2s.address()), + ..default + } + }; + let ballot_contract_parameters = if !testnet { + BallotContractParameters::default() + } else { + let default = BallotContractParameters::default(); + BallotContractParameters { + p2s: NetworkAddress::new(NetworkPrefix::Testnet, &default.p2s.address()), + ..default + } + }; let config = BootstrapConfig { tokens_to_mint: TokensToMint { pool_nft: NftMintDetails { @@ -132,11 +174,11 @@ pub fn generate_bootstrap_config_template(config_file_name: String) -> Result<() node_ip: "127.0.0.1".into(), node_port: "9053".into(), node_api_key: "hello".into(), - on_mainnet: true, - refresh_contract_parameters: RefreshContractParameters::default(), - pool_contract_parameters: PoolContractParameters::default(), - update_contract_parameters: UpdateContractParameters::default(), - ballot_contract_parameters: BallotContractParameters::default(), + on_mainnet: !testnet, + refresh_contract_parameters, + pool_contract_parameters, + update_contract_parameters, + ballot_contract_parameters, }; let s = serde_yaml::to_string(&config)?; @@ -665,7 +707,7 @@ pub enum BootstrapError { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use ergo_lib::{ chain::{ergo_state_context::ErgoStateContext, transaction::TxId}, ergotree_interpreter::sigma_protocol::private_input::DlogProverInput, @@ -682,7 +724,7 @@ mod tests { use crate::pool_commands::test_utils::{LocalTxSigner, WalletDataMock}; use std::cell::RefCell; #[derive(Default)] - struct SubmitTxMock { + pub(crate) struct SubmitTxMock { transactions: RefCell>, } diff --git a/core/src/cli_commands/prepare_update.rs b/core/src/cli_commands/prepare_update.rs new file mode 100644 index 00000000..68ad2995 --- /dev/null +++ b/core/src/cli_commands/prepare_update.rs @@ -0,0 +1,538 @@ +use std::{convert::TryInto, io::Write}; + +use derive_more::From; +use ergo_lib::{ + chain::{ + ergo_box::box_builder::{ErgoBoxCandidateBuilder, ErgoBoxCandidateBuilderError}, + transaction::Transaction, + }, + ergotree_ir::{ + chain::{ + address::{Address, AddressEncoder, AddressEncoderError, NetworkPrefix}, + ergo_box::{ + box_value::{BoxValue, BoxValueError}, + ErgoBox, + }, + token::Token, + }, + ergo_tree::ErgoTree, + serialization::SigmaParsingError, + }, + wallet::{ + box_selector::{BoxSelector, BoxSelectorError, SimpleBoxSelector}, + tx_builder::{TxBuilder, TxBuilderError}, + }, +}; +use ergo_node_interface::node_interface::NodeError; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{ + contracts::{ + pool::PoolContractParameters, + refresh::{ + RefreshContract, RefreshContractError, RefreshContractInputs, RefreshContractParameters, + }, + update::{ + UpdateContract, UpdateContractError, UpdateContractInputs, UpdateContractParameters, + }, + }, + node_interface::{new_node_interface, SignTransaction, SubmitTransaction}, + oracle_config::{OracleConfig, ORACLE_CONFIG}, + wallet::WalletDataSource, +}; + +use super::bootstrap::{Addresses, NftMintDetails, TokenMintDetails}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct UpdateTokensToMint { + pub refresh_nft: Option, + pub update_nft: Option, + pub oracle_tokens: Option, + pub ballot_tokens: Option, + pub reward_tokens: Option, +} + +#[derive(Clone, Deserialize)] +#[serde(try_from = "crate::serde::UpdateBootstrapConfigSerde")] +pub struct UpdateBootstrapConfig { + pub pool_contract_parameters: Option, // New pool script, etc. Note that we don't actually mint any new pool NFT in the update step, instead this is simply passed to the new oracle config for convenience + pub refresh_contract_parameters: Option, + pub update_contract_parameters: Option, + pub tokens_to_mint: UpdateTokensToMint, + pub addresses: Addresses, +} + +pub fn prepare_update(config_file_name: String) -> Result<(), PrepareUpdateError> { + let s = std::fs::read_to_string(config_file_name)?; + let config: UpdateBootstrapConfig = serde_yaml::from_str(&s)?; + + let node_interface = new_node_interface(); + let prefix = if ORACLE_CONFIG.on_mainnet { + NetworkPrefix::Mainnet + } else { + NetworkPrefix::Testnet + }; + let change_address = AddressEncoder::new(prefix).parse_address_from_str( + &node_interface + .wallet_status()? + .change_address + .ok_or(PrepareUpdateError::NoChangeAddressSetInNode)?, + )?; + let update_bootstrap_input = PrepareUpdateInput { + config: config.clone(), + wallet: &node_interface, + tx_signer: &node_interface, + submit_tx: &node_interface, + tx_fee: BoxValue::SAFE_USER_MIN, + erg_value_per_box: BoxValue::SAFE_USER_MIN, + change_address, + height: node_interface + .current_block_height() + .unwrap() + .try_into() + .unwrap(), + old_config: ORACLE_CONFIG.clone(), + }; + + let new_config = perform_update_chained_transaction(update_bootstrap_input)?; + + info!("Update chain-transaction complete"); + info!("Writing new config file to oracle_config_updated.yaml"); + let s = serde_yaml::to_string(&new_config)?; + let mut file = std::fs::File::create("oracle_config_updated.yaml")?; + file.write_all(s.as_bytes())?; + info!("Updated oracle configuration file oracle_config_updated.yaml"); + Ok(()) +} + +pub struct PrepareUpdateInput<'a> { + pub config: UpdateBootstrapConfig, + pub wallet: &'a dyn WalletDataSource, + pub tx_signer: &'a dyn SignTransaction, + pub submit_tx: &'a dyn SubmitTransaction, + pub tx_fee: BoxValue, + pub erg_value_per_box: BoxValue, + pub change_address: Address, + pub height: u32, + pub old_config: OracleConfig, +} + +pub(crate) fn perform_update_chained_transaction( + input: PrepareUpdateInput, +) -> Result { + let PrepareUpdateInput { + config, + wallet, + tx_signer: wallet_sign, + submit_tx, + tx_fee, + erg_value_per_box, + change_address, + height, + old_config, + .. + } = input; + + let mut num_transactions_left = 1; + + if config.refresh_contract_parameters.is_some() { + num_transactions_left += 1; + } + if config.update_contract_parameters.is_some() { + num_transactions_left += 1; + } + if config.tokens_to_mint.oracle_tokens.is_some() { + num_transactions_left += 1; + } + if config.tokens_to_mint.ballot_tokens.is_some() { + num_transactions_left += 1; + } + if config.tokens_to_mint.reward_tokens.is_some() { + num_transactions_left += 1; + } + + let wallet_pk_ergo_tree = config + .addresses + .wallet_address_for_chain_transaction + .script()?; + let guard = wallet_pk_ergo_tree.clone(); + + // Since we're building a chain of transactions, we need to filter the output boxes of each + // constituent transaction to be only those that are guarded by our wallet's key. + let filter_tx_outputs = move |outputs: Vec| -> Vec { + outputs + .clone() + .into_iter() + .filter(|b| b.ergo_tree == guard) + .collect() + }; + + // This closure computes `E_{num_transactions_left}`. + let calc_target_balance = |num_transactions_left| { + let b = erg_value_per_box.checked_mul_u32(num_transactions_left)?; + let fees = tx_fee.checked_mul_u32(num_transactions_left)?; + b.checked_add(&fees) + }; + + // Effect a single transaction that mints a token with given details, as described in comments + // at the beginning. By default it uses `wallet_pk_ergo_tree` as the guard for the token box, + // but this can be overriden with `different_token_box_guard`. + let mint_token = |input_boxes: Vec, + num_transactions_left: &mut u32, + token_name, + token_desc, + token_amount, + different_token_box_guard: Option| + -> Result<(Token, Transaction), PrepareUpdateError> { + let target_balance = calc_target_balance(*num_transactions_left)?; + let box_selector = SimpleBoxSelector::new(); + let box_selection = box_selector.select(input_boxes, target_balance, &[])?; + let token = Token { + token_id: box_selection.boxes.first().box_id().into(), + amount: token_amount, + }; + let token_box_guard = + different_token_box_guard.unwrap_or_else(|| wallet_pk_ergo_tree.clone()); + let mut builder = ErgoBoxCandidateBuilder::new(erg_value_per_box, token_box_guard, height); + builder.mint_token(token.clone(), token_name, token_desc, 1); + let mut output_candidates = vec![builder.build()?]; + + let remaining_funds = ErgoBoxCandidateBuilder::new( + calc_target_balance(*num_transactions_left - 1)?, + wallet_pk_ergo_tree.clone(), + height, + ) + .build()?; + output_candidates.push(remaining_funds.clone()); + + let inputs = box_selection.boxes.clone(); + let tx_builder = TxBuilder::new( + box_selection, + output_candidates, + height, + tx_fee, + change_address.clone(), + BoxValue::MIN, + ); + let mint_token_tx = tx_builder.build()?; + debug!("Mint token unsigned transaction: {:?}", mint_token_tx); + let signed_tx = wallet_sign.sign_transaction_with_inputs(&mint_token_tx, inputs, None)?; + *num_transactions_left -= 1; + Ok((token, signed_tx)) + }; + + let unspent_boxes = wallet.get_unspent_wallet_boxes()?; + debug!("unspent boxes: {:?}", unspent_boxes); + let target_balance = calc_target_balance(num_transactions_left)?; + debug!("target_balance: {:?}", target_balance); + let box_selector = SimpleBoxSelector::new(); + let box_selection = box_selector.select(unspent_boxes.clone(), target_balance, &[])?; + debug!("box selection: {:?}", box_selection); + + let mut new_oracle_config = old_config.clone(); + let mut transactions = vec![]; + let mut inputs = box_selection.boxes.clone(); // Inputs for each transaction in chained tx, updated after each mint step + + if let Some(ref token_mint_details) = config.tokens_to_mint.oracle_tokens { + info!("Minting oracle tokens"); + let (token, tx) = mint_token( + inputs.into(), + &mut num_transactions_left, + token_mint_details.name.clone(), + token_mint_details.description.clone(), + token_mint_details.quantity.try_into().unwrap(), + None, + )?; + new_oracle_config.token_ids.oracle_token_id = token.token_id; + inputs = filter_tx_outputs(tx.outputs.clone()).try_into().unwrap(); + transactions.push(tx); + } + if let Some(ref token_mint_details) = config.tokens_to_mint.ballot_tokens { + info!("Minting ballot tokens"); + let (token, tx) = mint_token( + inputs.into(), + &mut num_transactions_left, + token_mint_details.name.clone(), + token_mint_details.description.clone(), + token_mint_details.quantity.try_into().unwrap(), + None, + )?; + new_oracle_config.token_ids.ballot_token_id = token.token_id; + inputs = filter_tx_outputs(tx.outputs.clone()).try_into().unwrap(); + } + if let Some(ref token_mint_details) = config.tokens_to_mint.reward_tokens { + info!("Minting reward tokens"); + let (token, tx) = mint_token( + inputs.into(), + &mut num_transactions_left, + token_mint_details.name.clone(), + token_mint_details.description.clone(), + token_mint_details.quantity.try_into().unwrap(), + None, + )?; + new_oracle_config.token_ids.reward_token_id = token.token_id; + inputs = filter_tx_outputs(tx.outputs.clone()).try_into().unwrap(); + transactions.push(tx); + } + if let Some(ref contract_parameters) = config.refresh_contract_parameters { + info!("Creating new refresh NFT"); + let refresh_contract_inputs = RefreshContractInputs { + contract_parameters, + oracle_token_id: &new_oracle_config.token_ids.oracle_token_id, + pool_nft_token_id: &old_config.token_ids.pool_nft_token_id, + }; + let refresh_contract = RefreshContract::new(refresh_contract_inputs)?; + let refresh_nft_details = config + .tokens_to_mint + .refresh_nft + .ok_or(PrepareUpdateError::NoMintDetails)?; + let (token, tx) = mint_token( + inputs.into(), + &mut num_transactions_left, + refresh_nft_details.name.clone(), + refresh_nft_details.description.clone(), + 1.try_into().unwrap(), + Some(refresh_contract.ergo_tree()), + )?; + new_oracle_config.token_ids.refresh_nft_token_id = token.token_id; + new_oracle_config.refresh_contract_parameters = contract_parameters.clone(); + inputs = filter_tx_outputs(tx.outputs.clone()).try_into().unwrap(); + info!("Refresh contract tx id: {:?}", tx.id()); + transactions.push(tx); + } + if let Some(ref contract_parameters) = config.update_contract_parameters { + info!("Creating new update NFT"); + let update_contract_inputs = UpdateContractInputs { + contract_parameters, + ballot_token_id: &new_oracle_config.token_ids.ballot_token_id, + pool_nft_token_id: &new_oracle_config.token_ids.pool_nft_token_id, + }; + let update_contract = UpdateContract::new(update_contract_inputs)?; + let update_nft_details = config + .tokens_to_mint + .update_nft + .ok_or(PrepareUpdateError::NoMintDetails)?; + let (token, tx) = mint_token( + inputs.into(), + &mut num_transactions_left, + update_nft_details.name.clone(), + update_nft_details.description.clone(), + 1.try_into().unwrap(), + Some(update_contract.ergo_tree()), + )?; + new_oracle_config.token_ids.update_nft_token_id = token.token_id; + new_oracle_config.update_contract_parameters = contract_parameters.clone(); + info!("Update contract tx id: {:?}", tx.id()); + transactions.push(tx); + } + + if let Some(new_pool_contract_parameters) = config.pool_contract_parameters { + new_oracle_config.pool_contract_parameters = new_pool_contract_parameters; + } + + for tx in transactions { + let tx_id = submit_tx.submit_transaction(&tx)?; + info!("Tx submitted {}", tx_id); + } + Ok(new_oracle_config) +} + +#[derive(Debug, Error, From)] +pub enum PrepareUpdateError { + #[error("tx builder error: {0}")] + TxBuilder(TxBuilderError), + #[error("box builder error: {0}")] + ErgoBoxCandidateBuilder(ErgoBoxCandidateBuilderError), + #[error("node error: {0}")] + Node(NodeError), + #[error("box selector error: {0}")] + BoxSelector(BoxSelectorError), + #[error("box value error: {0}")] + BoxValue(BoxValueError), + #[error("IO error: {0}")] + Io(std::io::Error), + #[error("serde-yaml error: {0}")] + SerdeYaml(serde_yaml::Error), + #[error("yaml-rust error: {0}")] + YamlRust(String), + #[error("AddressEncoder error: {0}")] + AddressEncoder(AddressEncoderError), + #[error("SigmaParsing error: {0}")] + SigmaParse(SigmaParsingError), + #[error("Node doesn't have a change address set")] + NoChangeAddressSetInNode, + #[error("Refresh contract failed: {0}")] + RefreshContract(RefreshContractError), + #[error("Update contract error: {0}")] + UpdateContract(UpdateContractError), + #[error("Bootstrap config file already exists")] + ConfigFilenameAlreadyExists, + #[error("No parameters were added for update")] + NoOpUpgrade, + #[error("No mint details were provided for update/refresh contract in tokens_to_mint")] + NoMintDetails, +} + +#[cfg(test)] +mod test { + use ergo_lib::{ + chain::{ergo_state_context::ErgoStateContext, transaction::TxId}, + ergotree_interpreter::sigma_protocol::private_input::DlogProverInput, + ergotree_ir::chain::{ + address::AddressEncoder, + ergo_box::{ErgoBox, NonMandatoryRegisters}, + }, + wallet::Wallet, + }; + use sigma_test_util::force_any_val; + + use super::*; + use crate::cli_commands::bootstrap::tests::SubmitTxMock; + use crate::pool_commands::test_utils::{LocalTxSigner, WalletDataMock}; + + #[test] + fn test_prepare_update_transaction() { + let old_config: OracleConfig = serde_yaml::from_str( + "--- +token_ids: + pool_nft_token_id: FHF/kXzbGVH8N44x/8Cgp3i92xDWUJwgHLTtJVDvn4M= + refresh_nft_token_id: L5ERlF2PBfXBJzJ0PmAbHegC/nQcOAZeamNy4TKclvo= + update_nft_token_id: SOAloePnia3O3cXSElkLDx9iETxIgnlXEtVCqGbRF+g= + oracle_token_id: blxak+JLo73NK1ENhiOpxudO/n4ObSDBFMR6GbKZ9X8= + reward_token_id: ZdF48OHW1UjygIE8bxhjjUlZ3sHu/MsNKPpNH2EsWu8= + ballot_token_id: sfLzXXJ78hxZnH6hURhNkd91Z8SxQj5Ut/uTz9x3+BA= + +node_ip: 127.0.0.1 +node_port: 9052 +node_api_key: hello +base_fee: 10000 +core_api_port: 9053 +oracle_address: 3WzD3VNSK4RtDCZe8njzLzRnWbxcfpCneUcQncAVV9JBDE37nLxR +on_mainnet: false + +data_point_source: NanoErgUsd +addresses: + address_for_oracle_tokens: 3WzD3VNSK4RtDCZe8njzLzRnWbxcfpCneUcQncAVV9JBDE37nLxR + wallet_address_for_chain_transaction: 3WzD3VNSK4RtDCZe8njzLzRnWbxcfpCneUcQncAVV9JBDE37nLxR + +refresh_contract_parameters: + p2s: 62TTAg5ZqAM7HwjB169cSSn844L2ZmMq4xjen6QcGt9Bb6ssYBgg3SNHuRiDqJmHYXbdgU179G5WiYPMq5VgUHDPbjJ814wuUgCARX1TiSavG2ycFFbJzqdfj7cmZHhNziVg5f7YsU365P49oCpVxmQoeVUnomDzKMrsn2M1VeAGrrnBdTBcjuSm6M1NURRNmfir3TsazHZbyPPrr6BajikkwxLQ9GiSWnY841qU8ZRHvhqVqiVZa13NiXqfTa36LBGQmsyYQuY4ThdgLkfgsNH24snGcx2UUyYsRSQfK8SWtVjbsjwRF4fDdST4St4ermWXP8JfPmXLFeg3m5NHEyd6W3WrCR2i14tenGKqU6aHzJ3ZCZCfKinaUFqr81NQ4kmH9kzpzs5KfXhyTKRd43jRuU2gP2hDLZ2N4isS1EYCsySp86yrM1VAP91f3sF9MHjUXXAXZHp4EvtnJdtkTgQxCYmV95XuEnBk1Kx2zNvqtERsRnyqbtvv78qY63DMCMiNNkPJeg1YbNk4Li7FzPokiNE4YfPZ3uQVEKzNdrgUCPuMh5ShcBmDV4v35AbbwWWQpfh6j7dLsXzDKMG5i4fuoctfthSBC8ipsbNo563zorHd6iyoFofLeAxNW9eTZC4oZdskhep1wu3BYyYCEdrmtUTXKueK3JTpnzb1uRKYNFWdQKMv2UwfxrxWwaDv7BxFbrRstAc4wznZkjtQ1C2VmKrjU3UuVv235r9tFwojEoCuRbGqtg2DzW + pool_nft_index: 17 + oracle_token_id_index: 3 + min_data_points_index: 13 + min_data_points: 4 + buffer_index: 21 + buffer_length: 4 + max_deviation_percent_index: 15 + max_deviation_percent: 5 + epoch_length_index: 0 + epoch_length: 30 +pool_contract_parameters: + p2s: 3R221rmtBS5mwaKUTfwXpmGoUk4PjKLXuD6aRd1xyuCaRLNtzinwTGsZnhs4Fen3Rz46GYcgLezUe3Aunm7gZYWgYyEWCGfPYsmTptYtd8U2o2pp8NvNL7yQ79vJUamqqEtVt1i7eVHKAZarHP51n + refresh_nft_index: 2 + update_nft_index: 3 +update_contract_parameters: + p2s: 3c2tfyDoE3VryrPkj4f6Drw4QCf4Cx2rHRwbMy3rp9JZwArEB7L18ePfXVpwxgPJ21E4vu5SnKzumuCSwgjUMxL6LTYncGWozPf71Xz1Bx8X2aUuNnkTxrJK9NJwuEyBdbnCiYv18a6Cbib5T9wovLuhDdcrdTsXbyyWy9iojqgvrApU4Ge31Xgxmir4sgVFX8pR6po1VCpSwbtvbP9pJyApVmkYGuWT7vMpoapPcsD4qRdn9cetGZL2Nz8dNvJyE4LRaE97VokTYFCpvM2NbAa8GkwXhBc4SsnFQKchR7PUch1CjZi4sVfBZL4Zma94UrpgP5oMNujzJx865mQsBm1h2dL87Dgfba81npafZxzDT9EU82UgWwhTYcnPiqZft5T9sQTsWiNtvMwTvXFVXAvfcPBr5meEkut2fx1p8ZmPLYoRGvDi9eLucRhpuhpAsLdBSS3iuHVn7bU4MAUYaquHD97CDFoTL2FZkfnELJHfqZMt3rtrjcC1VpMq2TgXPK5PsbsD9nEQYJTSex + pool_nft_index: 5 + ballot_token_index: 9 + min_votes_index: 13 + min_votes: 6 +ballot_contract_parameters: + p2s: KKTr5Kf9nPN9o2FAhMHorL6oucAzavWXyqqzDhVVBPcbmtcSzCAWHXN4qeFJh58jbinfZcMCxHqrHp5GBVffxNxV2D1o91NimDxZVsgNjiGd1B5y5j9LsAixoU3GbmMeJKiXBvahu2emyLWQva3oWQaAPRGaSMY8fUeqvPNZcFqd2zgUTZ2gYWdDrsKZGK36mnTtio4F9kBkquPBt5VyQfGjjTjU3MhCRrKtg5UesyndY4mA + min_storage_rent_index: 0 + min_storage_rent: 10000000 + update_nft_index: 6 +oracle_contract_parameters: + p2s: CxiAhQ241nmgPp39VgGTWPjKDNauyuu8p9Rc2x5pWf5znRwmFC3TbNqGBmBZmAhE6SUG2xMBpdLp3GZUQs9VEcrnHtCzpMqXcdsbtELqoPRAmHtNarK8VnfyhHzZijDGggbwEZzQRhw1U45N75BFYWrZSdgrgZLnpr5pC1Rs979hB8UTYHU2p1vPqPbJe44tvZ7E1mJJqqKFBdCR2hW8Ka7dCTWEv4yviUWRtHjpFnBCsFanMQ8R7YdHxh1Z36JFCscdvHvHCQsCZo7Z3AAdq8F5cwoWrZMY9eBP + pool_nft_index: 5 +ballot_parameters: + contract_parameters: + p2s: KKTr5Kf9nPN9o2FAhMHorL6oucAzavWXyqqzDhVVBPcbmtcSzCAWHXN4qeFJh58jbinfZcMCxHqrHp5GBVffxNxV2D1o91NimDxZVsgNjiGd1B5y5j9LsAixoU3GbmMeJKiXBvahu2emyLWQva3oWQaAPRGaSMY8fUeqvPNZcFqd2zgUTZ2gYWdDrsKZGK36mnTtio4F9kBkquPBt5VyQfGjjTjU3MhCRrKtg5UesyndY4mA + min_storage_rent_index: 0 + min_storage_rent: 10000000 + update_nft_index: 6 + vote_parameters: ~ + ballot_token_owner_address: 3WzD3VNSK4RtDCZe8njzLzRnWbxcfpCneUcQncAVV9JBDE37nLxR").unwrap(); + let ctx = force_any_val::(); + let height = ctx.pre_header.height; + let secret = force_any_val::(); + let address = Address::P2Pk(secret.public_image()); + let wallet = Wallet::from_secrets(vec![secret.clone().into()]); + let ergo_tree = address.script().unwrap(); + + let value = BoxValue::SAFE_USER_MIN.checked_mul_u32(10000).unwrap(); + let unspent_boxes = vec![ErgoBox::new( + value, + ergo_tree.clone(), + None, + NonMandatoryRegisters::empty(), + height - 9, + force_any_val::(), + 0, + ) + .unwrap()]; + let change_address = + AddressEncoder::new(ergo_lib::ergotree_ir::chain::address::NetworkPrefix::Mainnet) + .parse_address_from_str("9iHyKxXs2ZNLMp9N9gbUT9V8gTbsV7HED1C1VhttMfBUMPDyF7r") + .unwrap(); + + let state = UpdateBootstrapConfig { + tokens_to_mint: UpdateTokensToMint { + refresh_nft: Some(NftMintDetails { + name: "refresh NFT".into(), + description: "refresh NFT".into(), + }), + update_nft: Some(NftMintDetails { + name: "update NFT".into(), + description: "update NFT".into(), + }), + oracle_tokens: Some(TokenMintDetails { + name: "oracle token".into(), + description: "oracle token".into(), + quantity: 15, + }), + ballot_tokens: Some(TokenMintDetails { + name: "ballot token".into(), + description: "ballot token".into(), + quantity: 15, + }), + reward_tokens: Some(TokenMintDetails { + name: "reward token".into(), + description: "reward token".into(), + quantity: 100_000_000, + }), + }, + refresh_contract_parameters: Some(RefreshContractParameters::default()), + pool_contract_parameters: Some(PoolContractParameters::default()), + update_contract_parameters: Some(UpdateContractParameters::default()), + addresses: Addresses { + address_for_oracle_tokens: address.clone(), + wallet_address_for_chain_transaction: address.clone(), + }, + }; + + let height = ctx.pre_header.height; + let submit_tx = SubmitTxMock::default(); + let oracle_config = perform_update_chained_transaction(PrepareUpdateInput { + config: state.clone(), + wallet: &WalletDataMock { + unspent_boxes: unspent_boxes.clone(), + }, + tx_signer: &mut LocalTxSigner { + ctx: &ctx, + wallet: &wallet, + }, + submit_tx: &submit_tx, + tx_fee: BoxValue::SAFE_USER_MIN, + erg_value_per_box: BoxValue::SAFE_USER_MIN, + change_address, + height, + old_config: old_config.clone(), + }) + .unwrap(); + + assert!(oracle_config.token_ids != old_config.token_ids); + } +} diff --git a/core/src/cli_commands/update_pool.rs b/core/src/cli_commands/update_pool.rs new file mode 100644 index 00000000..a15b9665 --- /dev/null +++ b/core/src/cli_commands/update_pool.rs @@ -0,0 +1,583 @@ +use ergo_lib::{ + chain::{ + ergo_box::box_builder::ErgoBoxCandidateBuilder, + ergo_box::box_builder::ErgoBoxCandidateBuilderError, + transaction::unsigned::UnsignedTransaction, + }, + ergo_chain_types::blake2b256_hash, + ergotree_interpreter::sigma_protocol::prover::ContextExtension, + ergotree_ir::chain::{ + address::{Address, AddressEncoder, AddressEncoderError, NetworkPrefix}, + ergo_box::{box_value::BoxValue, ErgoBox, NonMandatoryRegisterId}, + token::Token, + }, + ergotree_ir::serialization::SigmaSerializable, + wallet::{ + box_selector::{BoxSelection, BoxSelector, BoxSelectorError, SimpleBoxSelector}, + signing::{TransactionContext, TxSigningError}, + tx_builder::{TxBuilder, TxBuilderError}, + }, +}; +use ergo_node_interface::node_interface::NodeError; +use log::{error, info}; +use std::convert::TryInto; + +use crate::{ + box_kind::{make_pool_box_candidate, BallotBox, PoolBox, PoolBoxWrapper, VoteBallotBoxWrapper}, + cli_commands::ergo_explorer_transaction_link, + contracts::pool::PoolContract, + contracts::pool::PoolContractInputs, + node_interface::{current_block_height, get_wallet_status, sign_and_submit_transaction}, + oracle_config::{CastBallotBoxVoteParameters, OracleConfig, ORACLE_CONFIG}, + oracle_state::{OraclePool, PoolBoxSource, StageError, UpdateBoxSource, VoteBallotBoxesSource}, + wallet::WalletDataSource, +}; +use derive_more::From; +use thiserror::Error; + +#[derive(Debug, Error, From)] +pub enum UpdatePoolError { + #[error("Update pool: Not enough votes, expected {0}, found {1}")] + NotEnoughVotes(usize, usize), + #[error("Update pool: Pool parameters (refresh NFT, update NFT) unchanged")] + PoolUnchanged, + #[error("Update pool: ErgoBoxCandidateBuilderError {0}")] + ErgoBoxCandidateBuilder(ErgoBoxCandidateBuilderError), + #[error("Update pool: box selector error {0}")] + BoxSelector(BoxSelectorError), + #[error("Update pool: tx builder error {0}")] + TxBuilder(TxBuilderError), + #[error("Update pool: tx context error {0}")] + TxSigningError(TxSigningError), + #[error("Update pool: stage error {0}")] + StageError(StageError), + #[error("Update pool: node error {0}")] + Node(NodeError), + #[error("No change address in node")] + NoChangeAddressSetInNode, + #[error("Update pool: address encoder error {0}")] + AddressEncoderError(AddressEncoderError), + #[error("Update pool: pool contract error {0}")] + PoolContractError(crate::contracts::pool::PoolContractError), + #[error("Update pool: io error {0}")] + IoError(std::io::Error), + #[error("Update pool: yaml error {0}")] + YamlError(serde_yaml::Error), + #[error("Update pool: could not find unspent wallot boxes that do not contain ballot tokens")] + NoUsableWalletBoxes, +} + +pub fn update_pool( + new_pool_box_hash_str: Option, + new_reward_tokens: Option, +) -> Result<(), UpdatePoolError> { + info!("Opening oracle_config_updated.yaml"); + let s = std::fs::read_to_string("oracle_config_updated.yaml")?; + let new_oracle_config: OracleConfig = serde_yaml::from_str(&s)?; + let wallet = crate::wallet::WalletData {}; + let op = OraclePool::new().unwrap(); + let change_address_str = get_wallet_status()? + .change_address + .ok_or(UpdatePoolError::NoChangeAddressSetInNode)?; + + let network_prefix = if ORACLE_CONFIG.on_mainnet { + NetworkPrefix::Mainnet + } else { + NetworkPrefix::Testnet + }; + let change_address = + AddressEncoder::new(network_prefix).parse_address_from_str(&change_address_str)?; + + let pool_contract_inputs = PoolContractInputs::from(( + &new_oracle_config.pool_contract_parameters, + &new_oracle_config.token_ids, + )); + + let new_pool_contract = PoolContract::new(pool_contract_inputs)?; + let new_pool_box_hash = blake2b256_hash( + &new_pool_contract + .ergo_tree() + .sigma_serialize_bytes() + .unwrap(), + ); + + display_update_diff( + &ORACLE_CONFIG, + &new_oracle_config, + op.get_pool_box_source().get_pool_box()?, + new_reward_tokens.clone(), + ); + + if new_pool_box_hash_str.is_none() { + println!( + "Run ./oracle-core --new_pool_box_hash {} to update pool", + String::from(new_pool_box_hash) + ); + return Ok(()); + } + + let tx = build_update_pool_box_tx( + op.get_pool_box_source(), + op.get_ballot_boxes_source(), + &wallet, + op.get_update_box_source(), + new_pool_contract, + new_reward_tokens, + current_block_height()? as u32, + change_address, + )?; + + let tx_id_str = sign_and_submit_transaction(&tx.spending_tx)?; + println!( + "Update pool box transaction submitted: view here, {}", + ergo_explorer_transaction_link(tx_id_str, network_prefix) + ); + Ok(()) +} + +fn display_update_diff( + old_oracle_config: &OracleConfig, + new_oracle_config: &OracleConfig, + old_pool_box: PoolBoxWrapper, + new_reward_tokens: Option, +) { + let new_tokens = new_reward_tokens.unwrap_or_else(|| old_pool_box.reward_token()); + let new_pool_contract = PoolContract::new(PoolContractInputs::from(( + &new_oracle_config.pool_contract_parameters, + &new_oracle_config.token_ids, + ))) + .unwrap(); + println!("Pool Parameters: "); + let pool_box_hash = blake2b256_hash( + &new_pool_contract + .ergo_tree() + .sigma_serialize_bytes() + .unwrap(), + ); + println!("Pool Box Hash (new): {}", String::from(pool_box_hash)); + println!( + "Reward Token ID (old): {}", + String::from(old_oracle_config.token_ids.reward_token_id.clone()) + ); + println!( + "Reward Token ID (new): {}", + String::from(new_oracle_config.token_ids.reward_token_id.clone()) + ); + println!( + "Reward Token Amount (old): {}", + old_pool_box.reward_token().amount.as_u64() + ); + println!("Reward Token Amount (new): {}", new_tokens.amount.as_u64()); + println!( + "Update NFT ID (old): {}", + String::from(old_pool_box.contract().update_nft_token_id().clone()) + ); + println!( + "Update NFT ID (new): {}", + String::from(new_pool_contract.update_nft_token_id().clone()) + ); + println!( + "Refresh NFT ID (old): {}", + String::from(old_pool_box.contract().refresh_nft_token_id().clone()) + ); + println!( + "Refresh NFT ID (new): {}", + String::from(new_pool_contract.refresh_nft_token_id().clone()) + ); +} + +#[allow(clippy::too_many_arguments)] +fn build_update_pool_box_tx( + pool_box_source: &dyn PoolBoxSource, + ballot_boxes: &dyn VoteBallotBoxesSource, + wallet: &dyn WalletDataSource, + update_box: &dyn UpdateBoxSource, + new_pool_contract: PoolContract, + new_reward_tokens: Option, + height: u32, + change_address: Address, +) -> Result, UpdatePoolError> { + let update_box = update_box.get_update_box()?; + let min_votes = update_box.min_votes(); + let old_pool_box = pool_box_source.get_pool_box()?; + let pool_box_hash = blake2b256_hash( + &new_pool_contract + .ergo_tree() + .sigma_serialize_bytes() + .unwrap(), + ); + let reward_tokens = new_reward_tokens.unwrap_or_else(|| old_pool_box.reward_token()); + let vote_parameters = CastBallotBoxVoteParameters { + pool_box_address_hash: pool_box_hash, + reward_token_id: reward_tokens.token_id.clone(), + reward_token_quantity: *reward_tokens.amount.as_u64(), + update_box_creation_height: update_box.get_box().creation_info().0, + }; + // Find ballot boxes that are voting for the new pool hash + let mut sorted_ballot_boxes = ballot_boxes.get_ballot_boxes()?; + // Sort in descending order of ballot token amounts. If two boxes have the same amount of ballot tokens, also compare box value, in case some boxes were incorrectly created below minStorageRent + sorted_ballot_boxes.sort_by(|b1, b2| { + ( + *b1.ballot_token().amount.as_u64(), + *b1.get_box().value.as_u64(), + ) + .cmp(&( + *b2.ballot_token().amount.as_u64(), + *b2.get_box().value.as_u64(), + )) + }); + sorted_ballot_boxes.reverse(); + + let mut votes_cast = 0; + let vote_ballot_boxes: Vec = ballot_boxes + .get_ballot_boxes()? + .into_iter() + .filter(|ballot_box| *ballot_box.vote_parameters() == vote_parameters) + .scan(&mut votes_cast, |votes_cast, ballot_box| { + **votes_cast += *ballot_box.ballot_token().amount.as_u64(); + Some(ballot_box) + }) + .collect(); + if votes_cast < min_votes as u64 { + return Err(UpdatePoolError::NotEnoughVotes( + min_votes as usize, + vote_ballot_boxes.len(), + )); + } + + let pool_box_candidate = make_pool_box_candidate( + &new_pool_contract, + old_pool_box.rate() as i64, + old_pool_box.epoch_counter() as i32, + old_pool_box.pool_nft_token(), + reward_tokens.clone(), + old_pool_box.get_box().value, + old_pool_box.get_box().creation_height, // creation info must be preserved + )?; + let mut update_box_candidate = + ErgoBoxCandidateBuilder::new(update_box.get_box().value, update_box.ergo_tree(), height); + update_box_candidate.add_token(update_box.update_nft()); + let update_box_candidate = update_box_candidate.build()?; + + // Find unspent boxes without ballot token, see: https://github.com/ergoplatform/oracle-core/pull/80#issuecomment-1200258458 + let unspent_boxes: Vec = wallet + .get_unspent_wallet_boxes()? + .into_iter() + .filter(|wallet_box| { + wallet_box + .tokens + .as_ref() + .and_then(|tokens| { + tokens + .iter() + .find(|token| token.token_id == update_box.ballot_token_id()) + }) + .is_none() + }) + .collect(); + if unspent_boxes.is_empty() { + error!("Could not find unspent wallet boxes that do not contain ballot token. Please move ballot tokens to another address"); + return Err(UpdatePoolError::NoUsableWalletBoxes); + } + + let target_balance = BoxValue::SAFE_USER_MIN; + let target_tokens = if reward_tokens.token_id != old_pool_box.reward_token().token_id { + vec![reward_tokens.clone()] + } else { + vec![] + }; + let box_selector = SimpleBoxSelector::new(); + let selection = box_selector.select(unspent_boxes, target_balance, &target_tokens)?; + let mut input_boxes = vec![old_pool_box.get_box().clone(), update_box.get_box().clone()]; + input_boxes.extend( + vote_ballot_boxes + .iter() + .map(|ballot_box| ballot_box.get_box()) + .cloned(), + ); + input_boxes.extend_from_slice(selection.boxes.as_vec()); + let box_selection = BoxSelection { + boxes: input_boxes.try_into().unwrap(), + change_boxes: selection.change_boxes, + }; + + let mut outputs = vec![pool_box_candidate, update_box_candidate]; + for ballot_box in vote_ballot_boxes.iter() { + let mut ballot_box_candidate = ErgoBoxCandidateBuilder::new( + ballot_box.get_box().value, // value must be preserved or increased + ballot_box.contract().ergo_tree(), + height, + ); + ballot_box_candidate.add_token(ballot_box.ballot_token()); + ballot_box_candidate.set_register_value( + NonMandatoryRegisterId::R4, + (*ballot_box.ballot_token_owner().h).clone().into(), + ); + outputs.push(ballot_box_candidate.build()?) + } + + let mut tx_builder = TxBuilder::new( + box_selection.clone(), + outputs.clone(), + height, + BoxValue::SAFE_USER_MIN, + change_address, + BoxValue::MIN, + ); + + for (i, input_ballot) in vote_ballot_boxes.iter().enumerate() { + tx_builder.set_context_extension( + input_ballot.get_box().box_id(), + ContextExtension { + values: IntoIterator::into_iter([(0, ((i + 2) as i32).into())]).collect(), // first 2 outputs are pool and update box, ballot indexes start at 2 + }, + ) + } + let unsigned_tx = tx_builder.build()?; + Ok(TransactionContext::new( + unsigned_tx, + box_selection.boxes.into(), + vec![], + )?) +} + +#[cfg(test)] +mod tests { + use ergo_lib::{ + chain::{ + ergo_box::box_builder::ErgoBoxCandidateBuilder, ergo_state_context::ErgoStateContext, + transaction::TxId, + }, + ergo_chain_types::blake2b256_hash, + ergotree_interpreter::sigma_protocol::private_input::DlogProverInput, + ergotree_ir::{ + chain::{ + address::AddressEncoder, + ergo_box::{box_value::BoxValue, ErgoBox}, + token::{Token, TokenId}, + }, + serialization::SigmaSerializable, + }, + wallet::Wallet, + }; + use sigma_test_util::force_any_val; + use std::convert::TryInto; + + use crate::{ + box_kind::{ + make_local_ballot_box_candidate, make_pool_box_candidate, PoolBoxWrapper, + PoolBoxWrapperInputs, UpdateBoxWrapper, UpdateBoxWrapperInputs, VoteBallotBoxWrapper, + }, + contracts::{ + ballot::{BallotContract, BallotContractInputs}, + pool::{PoolContract, PoolContractInputs}, + update::{UpdateContract, UpdateContractInputs, UpdateContractParameters}, + }, + oracle_config::{BallotBoxWrapperParameters, TokenIds}, + pool_commands::test_utils::{ + make_wallet_unspent_box, BallotBoxesMock, PoolBoxMock, UpdateBoxMock, WalletDataMock, + }, + }; + + use super::build_update_pool_box_tx; + + fn force_any_tokenid() -> TokenId { + use proptest::strategy::Strategy; + proptest::arbitrary::any_with::( + ergo_lib::ergotree_ir::chain::token::arbitrary::ArbTokenIdParam::Arbitrary, + ) + .new_tree(&mut Default::default()) + .unwrap() + .current() + } + #[test] + fn test_update_pool_box() { + let ctx = force_any_val::(); + let height = ctx.pre_header.height; + + let token_ids = TokenIds { + pool_nft_token_id: force_any_tokenid(), + update_nft_token_id: force_any_tokenid(), + refresh_nft_token_id: force_any_tokenid(), + reward_token_id: force_any_tokenid(), + oracle_token_id: force_any_tokenid(), + ballot_token_id: force_any_tokenid(), + }; + let reward_tokens = Token { + token_id: token_ids.reward_token_id.clone(), + amount: 1500.try_into().unwrap(), + }; + let new_reward_tokens = Token { + token_id: force_any_tokenid(), + amount: force_any_val(), + }; + + let update_contract_parameters = UpdateContractParameters { + min_votes: 6, + ..Default::default() + }; + let update_contract_inputs = UpdateContractInputs { + contract_parameters: &update_contract_parameters, + pool_nft_token_id: &token_ids.pool_nft_token_id, + ballot_token_id: &token_ids.ballot_token_id, + }; + let update_contract = UpdateContract::new(update_contract_inputs).unwrap(); + let mut update_box_candidate = ErgoBoxCandidateBuilder::new( + BoxValue::SAFE_USER_MIN, + update_contract.ergo_tree(), + height, + ); + update_box_candidate.add_token(Token { + token_id: token_ids.update_nft_token_id.clone(), + amount: 1.try_into().unwrap(), + }); + let update_box = ErgoBox::from_box_candidate( + &update_box_candidate.build().unwrap(), + force_any_val::(), + 0, + ) + .unwrap(); + + let pool_contract_parameters = Default::default(); + let pool_contract_inputs = + PoolContractInputs::from((&pool_contract_parameters, &token_ids)); + + let pool_contract = PoolContract::new(pool_contract_inputs).unwrap(); + let pool_box_candidate = make_pool_box_candidate( + &pool_contract, + 0, + 0, + Token { + token_id: token_ids.pool_nft_token_id.clone(), + amount: 1.try_into().unwrap(), + }, + reward_tokens.clone(), + BoxValue::SAFE_USER_MIN, + height, + ) + .unwrap(); + let pool_box = + ErgoBox::from_box_candidate(&pool_box_candidate, force_any_val::(), 0).unwrap(); + + let new_refresh_token_id = force_any_tokenid(); + let mut new_pool_contract_inputs = pool_contract_inputs; + new_pool_contract_inputs.refresh_nft_token_id = &new_refresh_token_id; + let new_pool_contract = PoolContract::new(new_pool_contract_inputs).unwrap(); + + let pool_box_bytes = new_pool_contract + .ergo_tree() + .sigma_serialize_bytes() + .unwrap(); + let pool_box_hash = blake2b256_hash(&pool_box_bytes); + + let ballot_contract_parameters = Default::default(); + let ballot_contract_inputs = BallotContractInputs { + contract_parameters: &ballot_contract_parameters, + update_nft_token_id: &token_ids.update_nft_token_id, + }; + let ballot_contract = BallotContract::new(ballot_contract_inputs).unwrap(); + + let mut ballot_boxes = vec![]; + + for _ in 0..6 { + let secret = DlogProverInput::random(); + let ballot_box_parameters = BallotBoxWrapperParameters { + contract_parameters: ballot_contract_parameters.clone(), + vote_parameters: None, + ballot_token_owner_address: AddressEncoder::new( + ballot_contract_parameters.p2s.network(), + ) + .address_to_str( + &ergo_lib::ergotree_ir::chain::address::Address::P2Pk(secret.public_image()), + ), + }; + let ballot_box_candidate = make_local_ballot_box_candidate( + &ballot_contract, + secret.public_image(), + update_box.creation_height, + Token { + token_id: token_ids.ballot_token_id.clone(), + amount: 1.try_into().unwrap(), + }, + pool_box_hash.clone(), + new_reward_tokens.clone(), + ballot_contract.min_storage_rent().try_into().unwrap(), + height, + ) + .unwrap(); + let ballot_box = + ErgoBox::from_box_candidate(&ballot_box_candidate, force_any_val::(), 0) + .unwrap(); + ballot_boxes.push( + VoteBallotBoxWrapper::new( + ballot_box, + crate::box_kind::BallotBoxWrapperInputs { + parameters: &ballot_box_parameters, + ballot_token_id: &token_ids.ballot_token_id, + update_nft_token_id: &token_ids.update_nft_token_id, + }, + ) + .unwrap(), + ); + } + let ballot_boxes_mock = BallotBoxesMock { ballot_boxes }; + + let secret = DlogProverInput::random(); + let wallet_unspent_box = make_wallet_unspent_box( + // create a wallet box with new reward tokens + secret.public_image(), + BoxValue::SAFE_USER_MIN + .checked_mul_u32(4_000_000_000) + .unwrap(), + Some(vec![new_reward_tokens.clone()].try_into().unwrap()), + ); + let wallet_mock = WalletDataMock { + unspent_boxes: vec![wallet_unspent_box], + }; + let wallet = Wallet::from_secrets(vec![secret.clone().into()]); + let update_mock = UpdateBoxMock { + update_box: UpdateBoxWrapper::new( + update_box, + UpdateBoxWrapperInputs { + contract_parameters: &update_contract_parameters, + update_nft_token_id: &token_ids.update_nft_token_id, + ballot_token_id: &token_ids.ballot_token_id, + pool_nft_token_id: &token_ids.pool_nft_token_id, + }, + ) + .unwrap(), + }; + let pool_mock = PoolBoxMock { + pool_box: PoolBoxWrapper::new( + pool_box, + PoolBoxWrapperInputs { + contract_parameters: &pool_contract_parameters, + pool_nft_token_id: &token_ids.pool_nft_token_id, + reward_token_id: &token_ids.reward_token_id, + refresh_nft_token_id: &token_ids.refresh_nft_token_id, + update_nft_token_id: &token_ids.update_nft_token_id, + }, + ) + .unwrap(), + }; + + let change_address = + AddressEncoder::new(ergo_lib::ergotree_ir::chain::address::NetworkPrefix::Mainnet) + .parse_address_from_str("9iHyKxXs2ZNLMp9N9gbUT9V8gTbsV7HED1C1VhttMfBUMPDyF7r") + .unwrap(); + + let update_tx = build_update_pool_box_tx( + &pool_mock, + &ballot_boxes_mock, + &wallet_mock, + &update_mock, + new_pool_contract, + Some(new_reward_tokens), + height + 1, + change_address, + ) + .unwrap(); + + wallet.sign_transaction(update_tx, &ctx, None).unwrap(); + } +} diff --git a/core/src/cli_commands/vote_update_pool.rs b/core/src/cli_commands/vote_update_pool.rs index b8166b8a..5abee836 100644 --- a/core/src/cli_commands/vote_update_pool.rs +++ b/core/src/cli_commands/vote_update_pool.rs @@ -124,7 +124,7 @@ pub fn vote_update_pool( ); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; - if input == "YES" { + if input.trim_end() == "YES" { let tx_id_str = sign_and_submit_transaction(&unsigned_tx)?; println!( "Transaction made. Check status here: {}", @@ -375,7 +375,8 @@ mod tests { vote_parameters: Some(CastBallotBoxVoteParameters { reward_token_id: force_any_val::(), reward_token_quantity: 100000, - pool_box_address_hash: force_any_val::().into(), + pool_box_address_hash: force_any_val::(), + update_box_creation_height: force_any_val::().abs(), }), }; let inputs = BallotBoxWrapperInputs { diff --git a/core/src/contracts/ballot.rs b/core/src/contracts/ballot.rs index 6ece322e..837474fe 100644 --- a/core/src/contracts/ballot.rs +++ b/core/src/contracts/ballot.rs @@ -10,7 +10,7 @@ use thiserror::Error; use crate::box_kind::BallotBoxWrapperInputs; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct BallotContract { ergo_tree: ErgoTree, min_storage_rent_index: usize, diff --git a/core/src/contracts/oracle.rs b/core/src/contracts/oracle.rs index 743dfb5a..73722243 100644 --- a/core/src/contracts/oracle.rs +++ b/core/src/contracts/oracle.rs @@ -6,6 +6,7 @@ use ergo_lib::ergotree_ir::ergo_tree::ErgoTreeConstantError; use ergo_lib::ergotree_ir::mir::constant::TryExtractFromError; use ergo_lib::ergotree_ir::mir::constant::TryExtractInto; use ergo_lib::ergotree_ir::serialization::SigmaParsingError; +use serde::Serialize; use thiserror::Error; use crate::box_kind::OracleBoxWrapperInputs; @@ -95,7 +96,8 @@ impl OracleContract { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] +#[serde(into = "crate::serde::OracleContractParametersSerde")] /// Parameters for the oracle contract pub struct OracleContractParameters { pub p2s: NetworkAddress, diff --git a/core/src/contracts/pool.rs b/core/src/contracts/pool.rs index fa3dcf81..cd211396 100644 --- a/core/src/contracts/pool.rs +++ b/core/src/contracts/pool.rs @@ -37,6 +37,7 @@ pub enum PoolContractError { TryExtractFrom(TryExtractFromError), } +#[derive(Copy, Clone)] pub struct PoolContractInputs<'a> { pub contract_parameters: &'a PoolContractParameters, pub refresh_nft_token_id: &'a TokenId, diff --git a/core/src/contracts/update.rs b/core/src/contracts/update.rs index a26f7c18..8b017ec5 100644 --- a/core/src/contracts/update.rs +++ b/core/src/contracts/update.rs @@ -9,6 +9,8 @@ use ergo_lib::ergotree_ir::serialization::SigmaParsingError; use thiserror::Error; +use crate::box_kind::UpdateBoxWrapperInputs; + #[derive(Clone)] pub struct UpdateContract { ergo_tree: ErgoTree, @@ -47,6 +49,16 @@ pub struct UpdateContractInputs<'a> { pub ballot_token_id: &'a TokenId, } +impl<'a> From> for UpdateContractInputs<'a> { + fn from(wrapper_inputs: UpdateBoxWrapperInputs) -> UpdateContractInputs { + UpdateContractInputs { + contract_parameters: wrapper_inputs.contract_parameters, + pool_nft_token_id: wrapper_inputs.pool_nft_token_id, + ballot_token_id: wrapper_inputs.ballot_token_id, + } + } +} + impl UpdateContract { pub fn new(inputs: UpdateContractInputs) -> Result { let ergo_tree = inputs @@ -61,6 +73,10 @@ impl UpdateContract { .with_constant( inputs.contract_parameters.ballot_token_index, inputs.ballot_token_id.clone().into(), + )? + .with_constant( + inputs.contract_parameters.min_votes_index, + (inputs.contract_parameters.min_votes as i32).into(), )?; let contract = Self::from_ergo_tree(ergo_tree, inputs)?; Ok(contract) diff --git a/core/src/default_parameters.rs b/core/src/default_parameters.rs index 37c9e005..9ca006f7 100644 --- a/core/src/default_parameters.rs +++ b/core/src/default_parameters.rs @@ -72,8 +72,8 @@ impl Default for RefreshContractParameters { impl Default for UpdateContractParameters { fn default() -> Self { - // from https://wallet.plutomonkey.com/p2s/?source=eyAvLyBUaGlzIGJveCAodXBkYXRlIGJveCk6CiAgICAgICAgIC8vIFJlZ2lzdGVycyBlbXB0eSAKICAgICAgICAgLy8gCiAgICAgICAgIC8vIGJhbGxvdCBib3hlcyAoSW5wdXRzKQogICAgICAgICAvLyBSNCB0aGUgcHViIGtleSBvZiB2b3RlciBbR3JvdXBFbGVtZW50XSAobm90IHVzZWQgaGVyZSkKICAgICAgICAgLy8gUjUgdGhlIGNyZWF0aW9uIGhlaWdodCBvZiB0aGlzIGJveCBbSW50XQogICAgICAgICAvLyBSNiB0aGUgdmFsdWUgdm90ZWQgZm9yIFtDb2xsW0J5dGVdXSAoaGFzaCBvZiB0aGUgbmV3IHBvb2wgYm94IHNjcmlwdCkKICAgICAgICAgLy8gUjcgdGhlIHJld2FyZCB0b2tlbiBpZCBpbiBuZXcgYm94IAogICAgICAgICAvLyBSOCB0aGUgbnVtYmVyIG9mIHJld2FyZCB0b2tlbnMgaW4gbmV3IGJveCAKICAgICAgIAogICAgICAgICB2YWwgcG9vbE5GVCA9IGZyb21CYXNlNjQoIlJ5dExZbEJsVTJoV2JWbHhNM1EyZHpsNkpFTW1SaWxLUUUxalVXWlVhbGM9IikgLy8gVE9ETyByZXBsYWNlIHdpdGggYWN0dWFsIAogICAgICAKICAgICAgICAgdmFsIGJhbGxvdFRva2VuSWQgPSBmcm9tQmFzZTY0KCJQMFFvUnkxTFlWQmtVMmRXYTFsd00zTTJkamw1SkVJbVJTbElRRTFpVVdVPSIpIC8vIFRPRE8gcmVwbGFjZSB3aXRoIGFjdHVhbCAKICAgICAgIAogICAgICAgICB2YWwgbWluVm90ZXMgPSA2IC8vIFRPRE8gcmVwbGFjZSB3aXRoIGFjdHVhbAogICAgICAgICAKICAgICAgICAgdmFsIHBvb2xJbiA9IElOUFVUUygwKSAvLyBwb29sIGJveCBpcyAxc3QgaW5wdXQKICAgICAgICAgdmFsIHBvb2xPdXQgPSBPVVRQVVRTKDApIC8vIGNvcHkgb2YgcG9vbCBib3ggaXMgdGhlIDFzdCBvdXRwdXQKICAgICAgIAogICAgICAgICB2YWwgdXBkYXRlQm94T3V0ID0gT1VUUFVUUygxKSAvLyBjb3B5IG9mIHRoaXMgYm94IGlzIHRoZSAybmQgb3V0cHV0CiAgICAgICAKICAgICAgICAgLy8gY29tcHV0ZSB0aGUgaGFzaCBvZiB0aGUgcG9vbCBvdXRwdXQgYm94LiBUaGlzIHNob3VsZCBiZSB0aGUgdmFsdWUgdm90ZWQgZm9yCiAgICAgICAgIHZhbCBwb29sT3V0SGFzaCA9IGJsYWtlMmIyNTYocG9vbE91dC5wcm9wb3NpdGlvbkJ5dGVzKQogICAgICAgICB2YWwgcmV3YXJkVG9rZW5JZCA9IHBvb2xPdXQudG9rZW5zKDEpLl8xCiAgICAgICAgIHZhbCByZXdhcmRBbXQgPSBwb29sT3V0LnRva2VucygxKS5fMgogICAgICAgICAKICAgICAgICAgdmFsIHZhbGlkUG9vbEluID0gcG9vbEluLnRva2VucygwKS5fMSA9PSBwb29sTkZUCiAgICAgICAgIAogICAgICAgICB2YWwgdmFsaWRQb29sT3V0ID0gcG9vbEluLnByb3Bvc2l0aW9uQnl0ZXMgIT0gcG9vbE91dC5wcm9wb3NpdGlvbkJ5dGVzICAmJiAvLyBzY3JpcHQgc2hvdWxkIG5vdCBiZSBwcmVzZXJ2ZWQKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvb2xJbi50b2tlbnMoMCkgPT0gcG9vbE91dC50b2tlbnMoMCkgICAgICAgICAgICAgICAgJiYgLy8gTkZUIHByZXNlcnZlZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9vbEluLmNyZWF0aW9uSW5mby5fMSA9PSBwb29sT3V0LmNyZWF0aW9uSW5mby5fMSAgICAmJiAvLyBjcmVhdGlvbiBoZWlnaHQgcHJlc2VydmVkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb29sSW4udmFsdWUgPT0gcG9vbE91dC52YWx1ZSAgICAgICAgICAgICAgICAgICAgICAgICYmIC8vIHZhbHVlIHByZXNlcnZlZCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvb2xJbi5SNFtMb25nXSA9PSBwb29sT3V0LlI0W0xvbmddICAgICAgICAgICAgICAgICAgJiYgLy8gcmF0ZSBwcmVzZXJ2ZWQgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9vbEluLlI1W0ludF0gPT0gcG9vbE91dC5SNVtJbnRdICAgICAgICAgICAgICAgICAgICAmJiAvLyBjb3VudGVyIHByZXNlcnZlZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgISAocG9vbE91dC5SNltBbnldLmlzRGVmaW5lZCkKICAgICAgIAogICAgICAgICAKICAgICAgICAgdmFsIHZhbGlkVXBkYXRlT3V0ID0gdXBkYXRlQm94T3V0LnRva2VucyA9PSBTRUxGLnRva2VucyAgICAgICAgICAgICAgICAgICAgICYmCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHVwZGF0ZUJveE91dC5wcm9wb3NpdGlvbkJ5dGVzID09IFNFTEYucHJvcG9zaXRpb25CeXRlcyAmJgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB1cGRhdGVCb3hPdXQudmFsdWUgPj0gU0VMRi52YWx1ZSAgICAgICAgICAgICAgICAgICAgICAgJiYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdXBkYXRlQm94T3V0LmNyZWF0aW9uSW5mby5fMSA+IFNFTEYuY3JlYXRpb25JbmZvLl8xICAgICYmCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICEgKHVwZGF0ZUJveE91dC5SNFtBbnldLmlzRGVmaW5lZCkgCiAgICAgICAKICAgICAgICAgZGVmIGlzVmFsaWRCYWxsb3QoYjpCb3gpID0gaWYgKGIudG9rZW5zLnNpemUgPiAwKSB7CiAgICAgICAgICAgYi50b2tlbnMoMCkuXzEgPT0gYmFsbG90VG9rZW5JZCAgICAgICAmJgogICAgICAgICAgIGIuUjVbSW50XS5nZXQgPT0gU0VMRi5jcmVhdGlvbkluZm8uXzEgJiYgLy8gZW5zdXJlIHZvdGUgY29ycmVzcG9uZHMgdG8gdGhpcyBib3ggYnkgY2hlY2tpbmcgY3JlYXRpb24gaGVpZ2h0CiAgICAgICAgICAgYi5SNltDb2xsW0J5dGVdXS5nZXQgPT0gcG9vbE91dEhhc2ggICAmJiAvLyBjaGVjayBwcm9wb3NpdGlvbiB2b3RlZCBmb3IKICAgICAgICAgICBiLlI3W0NvbGxbQnl0ZV1dLmdldCA9PSByZXdhcmRUb2tlbklkICYmIC8vIGNoZWNrIHJld2FyZFRva2VuSWQgdm90ZWQgZm9yCiAgICAgICAgICAgYi5SOFtMb25nXS5nZXQgPT0gcmV3YXJkQW10ICAgICAgICAgICAgICAvLyBjaGVjayByZXdhcmRUb2tlbkFtdCB2b3RlZCBmb3IKICAgICAgICAgfSBlbHNlIGZhbHNlCiAgICAgICAgIAogICAgICAgICB2YWwgYmFsbG90Qm94ZXMgPSBJTlBVVFMuZmlsdGVyKGlzVmFsaWRCYWxsb3QpCiAgICAgICAgIAogICAgICAgICB2YWwgdm90ZXNDb3VudCA9IGJhbGxvdEJveGVzLmZvbGQoMEwsIHsoYWNjdW06IExvbmcsIGI6IEJveCkgPT4gYWNjdW0gKyBiLnRva2VucygwKS5fMn0pCiAgICAgICAgIAogICAgICAgICBzaWdtYVByb3AodmFsaWRQb29sSW4gJiYgdmFsaWRQb29sT3V0ICYmIHZhbGlkVXBkYXRlT3V0ICYmIHZvdGVzQ291bnQgPj0gbWluVm90ZXMpICAKICAgICAgIH0KICAgICAgIA== - let address = AddressEncoder::new(NetworkPrefix::Mainnet).parse_address_from_str("RGQjcwtwcPBVwTFZMaGyo471kgwcwtMjrUy41RqWhAtY2ovdKAQ2Ce3cUaF6S7LGMrV3boM5rGKR5K2vjyheDXtVuEoUpZefQ2qa7H8MPBaYfAWqttNpyp5A1GfYviWfSbbEsbUSptgUMHH9MTLCnkvQdfxtC9HvKX8gJdaJBhEF4KHUBDVcsuMX33vcqi7Y5voEjunnmgvbpcYBG6HAkZtz15uXh1TskFpumFDgqwMbExapeRRXbq3EjuVAqEeoibastYMLrZ1evAq1bZP9mFoQRd15kUgBHvRQLwHJzdcRSz1pCM6UXTsna599VQBCiqKRZ9iCDffeUGuvjJBgzm5gouMCpaEc6LJn5Z2ta5MFAvQpd1MhtvTBL6X6NFKbYJxNWFK7igqbf9nDtbkcrUjRD2LKeqEapNRbLnxyMd6Dd5nMKZLuthkgsK3BSmN4YKh2S94wNE5PRDM1FULTg1RC7tFvRV5aKmcKD25M7qYwXwLqWoRPCk7C8CqCdSHT2cJTM3RAx6xSbt5Cq").unwrap(); + // from https://wallet.plutomonkey.com/p2s/?source=eyAvLyBUaGlzIGJveCAodXBkYXRlIGJveCk6CiAgLy8gUmVnaXN0ZXJzIGVtcHR5IAogIC8vIAogIC8vIGJhbGxvdCBib3hlcyAoSW5wdXRzKQogIC8vIFI0IHRoZSBwdWIga2V5IG9mIHZvdGVyIFtHcm91cEVsZW1lbnRdIChub3QgdXNlZCBoZXJlKQogIC8vIFI1IHRoZSBjcmVhdGlvbiBoZWlnaHQgb2YgdGhpcyBib3ggW0ludF0KICAvLyBSNiB0aGUgdmFsdWUgdm90ZWQgZm9yIFtDb2xsW0J5dGVdXSAoaGFzaCBvZiB0aGUgbmV3IHBvb2wgYm94IHNjcmlwdCkKICAvLyBSNyB0aGUgcmV3YXJkIHRva2VuIGlkIGluIG5ldyBib3ggCiAgLy8gUjggdGhlIG51bWJlciBvZiByZXdhcmQgdG9rZW5zIGluIG5ldyBib3ggCgogIHZhbCBwb29sTkZUID0gZnJvbUJhc2U2NCgiUnl0TFlsQmxVMmhXYlZseE0zUTJkemw2SkVNbVJpbEtRRTFqVVdaVWFsYz0iKSAvLyBUT0RPIHJlcGxhY2Ugd2l0aCBhY3R1YWwgCgogIHZhbCBiYWxsb3RUb2tlbklkID0gZnJvbUJhc2U2NCgiUDBRb1J5MUxZVkJrVTJkV2ExbHdNM00yZGpsNUpFSW1SU2xJUUUxaVVXVT0iKSAvLyBUT0RPIHJlcGxhY2Ugd2l0aCBhY3R1YWwgCgogIHZhbCBtaW5Wb3RlcyA9IDYgLy8gVE9ETyByZXBsYWNlIHdpdGggYWN0dWFsCiAgCiAgdmFsIHBvb2xJbiA9IElOUFVUUygwKSAvLyBwb29sIGJveCBpcyAxc3QgaW5wdXQKICB2YWwgcG9vbE91dCA9IE9VVFBVVFMoMCkgLy8gY29weSBvZiBwb29sIGJveCBpcyB0aGUgMXN0IG91dHB1dAoKICB2YWwgdXBkYXRlQm94T3V0ID0gT1VUUFVUUygxKSAvLyBjb3B5IG9mIHRoaXMgYm94IGlzIHRoZSAybmQgb3V0cHV0CgogIC8vIGNvbXB1dGUgdGhlIGhhc2ggb2YgdGhlIHBvb2wgb3V0cHV0IGJveC4gVGhpcyBzaG91bGQgYmUgdGhlIHZhbHVlIHZvdGVkIGZvcgogIHZhbCBwb29sT3V0SGFzaCA9IGJsYWtlMmIyNTYocG9vbE91dC5wcm9wb3NpdGlvbkJ5dGVzKQogIHZhbCByZXdhcmRUb2tlbklkID0gcG9vbE91dC50b2tlbnMoMSkuXzEKICB2YWwgcmV3YXJkQW10ID0gcG9vbE91dC50b2tlbnMoMSkuXzIKICAKICB2YWwgdmFsaWRQb29sSW4gPSBwb29sSW4udG9rZW5zKDApLl8xID09IHBvb2xORlQKICAKICB2YWwgdmFsaWRQb29sT3V0ID0gcG9vbEluLnRva2VucygwKSA9PSBwb29sT3V0LnRva2VucygwKSAgICAgICAgICAgICAgICAmJiAvLyBORlQgcHJlc2VydmVkCiAgICAgICAgICAgICAgICAgICAgIHBvb2xJbi5jcmVhdGlvbkluZm8uXzEgPT0gcG9vbE91dC5jcmVhdGlvbkluZm8uXzEgICAgJiYgLy8gY3JlYXRpb24gaGVpZ2h0IHByZXNlcnZlZAogICAgICAgICAgICAgICAgICAgICBwb29sSW4udmFsdWUgPT0gcG9vbE91dC52YWx1ZSAgICAgICAgICAgICAgICAgICAgICAgICYmIC8vIHZhbHVlIHByZXNlcnZlZCAKICAgICAgICAgICAgICAgICAgICAgcG9vbEluLlI0W0xvbmddID09IHBvb2xPdXQuUjRbTG9uZ10gICAgICAgICAgICAgICAgICAmJiAvLyByYXRlIHByZXNlcnZlZCAgCiAgICAgICAgICAgICAgICAgICAgIHBvb2xJbi5SNVtJbnRdID09IHBvb2xPdXQuUjVbSW50XSAgICAgICAgICAgICAgICAgICAgJiYgLy8gY291bnRlciBwcmVzZXJ2ZWQKICAgICAgICAgICAgICAgICAgICAgISAocG9vbE91dC5SNltBbnldLmlzRGVmaW5lZCkKCiAgCiAgdmFsIHZhbGlkVXBkYXRlT3V0ID0gdXBkYXRlQm94T3V0LnRva2VucyA9PSBTRUxGLnRva2VucyAgICAgICAgICAgICAgICAgICAgICYmCiAgICAgICAgICAgICAgICAgICAgICAgdXBkYXRlQm94T3V0LnByb3Bvc2l0aW9uQnl0ZXMgPT0gU0VMRi5wcm9wb3NpdGlvbkJ5dGVzICYmCiAgICAgICAgICAgICAgICAgICAgICAgdXBkYXRlQm94T3V0LnZhbHVlID49IFNFTEYudmFsdWUgICAgICAgICAgICAgICAgICAgICAgICYmCiAgICAgICAgICAgICAgICAgICAgICAgdXBkYXRlQm94T3V0LmNyZWF0aW9uSW5mby5fMSA+IFNFTEYuY3JlYXRpb25JbmZvLl8xICAgICYmCiAgICAgICAgICAgICAgICAgICAgICAgISAodXBkYXRlQm94T3V0LlI0W0FueV0uaXNEZWZpbmVkKSAKCiAgZGVmIGlzVmFsaWRCYWxsb3QoYjpCb3gpID0gaWYgKGIudG9rZW5zLnNpemUgPiAwKSB7CiAgICBiLnRva2VucygwKS5fMSA9PSBiYWxsb3RUb2tlbklkICAgICAgICYmCiAgICBiLlI1W0ludF0uZ2V0ID09IFNFTEYuY3JlYXRpb25JbmZvLl8xICYmIC8vIGVuc3VyZSB2b3RlIGNvcnJlc3BvbmRzIHRvIHRoaXMgYm94IGJ5IGNoZWNraW5nIGNyZWF0aW9uIGhlaWdodAogICAgYi5SNltDb2xsW0J5dGVdXS5nZXQgPT0gcG9vbE91dEhhc2ggICAmJiAvLyBjaGVjayBwcm9wb3NpdGlvbiB2b3RlZCBmb3IKICAgIGIuUjdbQ29sbFtCeXRlXV0uZ2V0ID09IHJld2FyZFRva2VuSWQgJiYgLy8gY2hlY2sgcmV3YXJkVG9rZW5JZCB2b3RlZCBmb3IKICAgIGIuUjhbTG9uZ10uZ2V0ID09IHJld2FyZEFtdCAgICAgICAgICAgICAgLy8gY2hlY2sgcmV3YXJkVG9rZW5BbXQgdm90ZWQgZm9yCiAgfSBlbHNlIGZhbHNlCiAgCiAgdmFsIGJhbGxvdEJveGVzID0gSU5QVVRTLmZpbHRlcihpc1ZhbGlkQmFsbG90KQogIAogIHZhbCB2b3Rlc0NvdW50ID0gYmFsbG90Qm94ZXMuZm9sZCgwTCwgeyhhY2N1bTogTG9uZywgYjogQm94KSA9PiBhY2N1bSArIGIudG9rZW5zKDApLl8yfSkKICAKICBzaWdtYVByb3AodmFsaWRQb29sSW4gJiYgdmFsaWRQb29sT3V0ICYmIHZhbGlkVXBkYXRlT3V0ICYmIHZvdGVzQ291bnQgPj0gbWluVm90ZXMpICAKfQ== + let address = AddressEncoder::new(NetworkPrefix::Mainnet).parse_address_from_str("PAt5ff3qB8f3aFze1UP7EJPCbqqREvYkP6sbm4nRrsaSUVh6GLSyxm98sSRh6nMvSyp8i5Pt4ZipCLfLwh25uayaymmmXkEyAYV41TJkh9wqg9mdaKa4zCwiB7js1DXZ347jMJWfXS8s2eW4JP1gz2fCi4vw8AdiHvaitaZtr668SA5j5p2XkfegvTNHJV3b7Guiyr49sBkorxaxUfLQWk9KCXLvSE5p4UCtufkiV6B8SuP9NjeCevUcaZac19cBDBDQ4FxtN42pnBKdDBnEfFGB53NsBPY1cjbU9x9JKJPkFs4k8zYG1EAS4SD7cnn3isUQFnfvVdMe4dbb4hhjHPYDZWjS99qyg9tfjbGmGLExovdU2ZkZxiQ3LzrSwVPbdfCZzUhnTpLKEyGtDMEumXHMaoqLaUGYVoTbu64YNNyPCex6H3QRt2RFQxoRuRuEjawZzFVV9cQBesuYzpLsXKWTMxeVVVy2ffv8Ei4BzPTsDNgxvhFTddJMXSBtfv99yZ").unwrap(); UpdateContractParameters { p2s: NetworkAddress::new(NetworkPrefix::Mainnet, &address), pool_nft_index: 5, diff --git a/core/src/main.rs b/core/src/main.rs index 269916ae..3b133588 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -44,6 +44,8 @@ use crossbeam::channel::bounded; use ergo_lib::ergotree_ir::chain::address::Address; use ergo_lib::ergotree_ir::chain::address::AddressEncoder; use ergo_lib::ergotree_ir::chain::address::NetworkPrefix; +use ergo_lib::ergotree_ir::chain::token::Token; +use ergo_lib::ergotree_ir::chain::token::TokenId; use log::debug; use log::error; use log::LevelFilter; @@ -55,6 +57,7 @@ use oracle_state::OraclePool; use pool_commands::build_action; use state::process; use state::PoolState; +use std::convert::TryInto; use std::thread; use std::time::Duration; use wallet::WalletData; @@ -102,6 +105,9 @@ enum Command { /// Set this flag to output a bootstrap config template file to the given filename. If /// filename already exists, return error. generate_config_template: bool, + #[clap(short, long)] + /// Set this flag to use testnet prefix for bootstrap config template file + testnet: bool, }, /// Run the oracle-pool @@ -130,6 +136,22 @@ enum Command { /// The creation height of the update box. update_box_creation_height: u32, }, + /// Initiate the Update Pool transaction. + /// Run with no arguments to show diff between oracle_config.yaml and oracle_config_updated.yaml + /// Updated config file must be created using --prepare-update command first + UpdatePool { + /// New pool box hash. Must match hash of updated pool contract + new_pool_box_hash: Option, + /// New reward token id (optional, base64) + reward_token_id: Option, + /// New reward token amount, required if new token id was voted for + reward_token_amount: Option, + }, + /// Prepare updating oracle pool with new contracts/parameters. + PrepareUpdate { + /// Name of update parameters file (.yaml) + update_file: String, + }, } fn main() { @@ -149,10 +171,14 @@ fn main() { Command::Bootstrap { yaml_config_name, generate_config_template, + testnet, } => { if let Err(e) = (|| -> Result<(), anyhow::Error> { if generate_config_template { - cli_commands::bootstrap::generate_bootstrap_config_template(yaml_config_name)?; + cli_commands::bootstrap::generate_bootstrap_config_template( + yaml_config_name, + testnet, + )?; } else { cli_commands::bootstrap::bootstrap(yaml_config_name)?; } @@ -242,6 +268,33 @@ fn main() { std::process::exit(exitcode::SOFTWARE); } } + Command::UpdatePool { + new_pool_box_hash, + reward_token_id, + reward_token_amount, + } => { + assert_wallet_unlocked(&new_node_interface()); + let new_reward_tokens = + reward_token_id + .zip(reward_token_amount) + .map(|(token_id, amount)| Token { + token_id: TokenId::from_base64(&token_id).unwrap(), + amount: amount.try_into().unwrap(), + }); + if let Err(e) = + cli_commands::update_pool::update_pool(new_pool_box_hash, new_reward_tokens) + { + error!("Fatal update-pool error: {}", e); + std::process::exit(exitcode::SOFTWARE); + } + } + Command::PrepareUpdate { update_file } => { + assert_wallet_unlocked(&new_node_interface()); + if let Err(e) = cli_commands::prepare_update::prepare_update(update_file) { + error!("Fatal update error : {}", e); + std::process::exit(exitcode::SOFTWARE); + } + } } } diff --git a/core/src/oracle_config.rs b/core/src/oracle_config.rs index 31a7e4b4..9ffb1ec1 100644 --- a/core/src/oracle_config.rs +++ b/core/src/oracle_config.rs @@ -3,11 +3,15 @@ use crate::{ contracts::{ ballot::BallotContractParameters, oracle::OracleContractParameters, pool::PoolContractParameters, refresh::RefreshContractParameters, + update::UpdateContractParameters, }, datapoint_source::{DataPointSource, ExternalScript, PredefinedDataPointSource}, }; use anyhow::anyhow; -use ergo_lib::ergotree_ir::chain::{address::NetworkPrefix, token::TokenId}; +use ergo_lib::{ + ergo_chain_types::Digest32, + ergotree_ir::chain::{address::NetworkPrefix, token::TokenId}, +}; use log::LevelFilter; use serde::{Deserialize, Serialize}; @@ -32,8 +36,8 @@ pub struct OracleConfig { pub oracle_contract_parameters: OracleContractParameters, pub pool_contract_parameters: PoolContractParameters, pub refresh_contract_parameters: RefreshContractParameters, + pub update_contract_parameters: UpdateContractParameters, pub ballot_parameters: BallotBoxWrapperParameters, - // TODO: update_parameters (https://github.com/ergoplatform/oracle-core/issues/49) pub token_ids: TokenIds, pub addresses: Addresses, } @@ -47,11 +51,12 @@ pub struct BallotBoxWrapperParameters { pub ballot_token_owner_address: String, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct CastBallotBoxVoteParameters { - pub pool_box_address_hash: String, + pub pool_box_address_hash: Digest32, pub reward_token_id: TokenId, - pub reward_token_quantity: u32, + pub reward_token_quantity: u64, + pub update_box_creation_height: i32, } /// Holds the token ids of every important token used by the oracle pool. diff --git a/core/src/oracle_state.rs b/core/src/oracle_state.rs index 076c33b7..990dc3a5 100644 --- a/core/src/oracle_state.rs +++ b/core/src/oracle_state.rs @@ -3,21 +3,26 @@ use crate::box_kind::{ BallotBoxError, BallotBoxWrapper, BallotBoxWrapperInputs, OracleBox, OracleBoxError, OracleBoxWrapper, OracleBoxWrapperInputs, PoolBox, PoolBoxError, PoolBoxWrapper, PoolBoxWrapperInputs, RefreshBoxError, RefreshBoxWrapper, RefreshBoxWrapperInputs, + UpdateBoxError, UpdateBoxWrapper, UpdateBoxWrapperInputs, VoteBallotBoxWrapper, }; use crate::contracts::ballot::BallotContract; use crate::contracts::oracle::OracleContract; use crate::datapoint_source::{DataPointSource, DataPointSourceError}; -use crate::oracle_config::ORACLE_CONFIG; +use crate::oracle_config::{BallotBoxWrapperParameters, ORACLE_CONFIG}; use crate::scans::{ - register_datapoint_scan, register_local_ballot_box_scan, register_local_oracle_datapoint_scan, - register_pool_box_scan, register_refresh_box_scan, save_scan_ids_locally, Scan, ScanError, + register_ballot_box_scan, register_datapoint_scan, register_local_ballot_box_scan, + register_local_oracle_datapoint_scan, register_pool_box_scan, register_refresh_box_scan, + register_update_box_scan, save_scan_ids_locally, Scan, ScanError, }; use crate::state::PoolState; use crate::{BlockHeight, EpochID, NanoErg}; use anyhow::Error; use derive_more::From; -use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox; -use ergo_lib::ergotree_ir::mir::constant::TryExtractFromError; + +use ergo_lib::ergotree_ir::chain::address::{Address, AddressEncoder}; +use ergo_lib::ergotree_ir::chain::ergo_box::{ErgoBox, NonMandatoryRegisterId}; +use ergo_lib::ergotree_ir::mir::constant::{TryExtractFromError, TryExtractInto}; +use ergo_lib::ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog; use ergo_node_interface::node_interface::NodeError; use std::path::Path; use thiserror::Error; @@ -42,6 +47,8 @@ pub enum StageError { OracleBoxError(OracleBoxError), #[error("datapoint source error: {0}")] DataPointSource(DataPointSourceError), + #[error("update box error: {0}")] + UpdateBoxError(UpdateBoxError), } pub trait StageDataSource { @@ -83,6 +90,14 @@ pub trait LocalDatapointBoxSource { fn get_local_oracle_datapoint_box(&self) -> Result; } +pub trait VoteBallotBoxesSource { + fn get_ballot_boxes(&self) -> Result>; +} + +pub trait UpdateBoxSource { + fn get_update_box(&self) -> Result; +} + /// A `Stage` in the multi-stage smart contract protocol. Is defined here by it's contract address & it's scan_id #[derive(Debug, Clone)] pub struct Stage { @@ -100,6 +115,8 @@ pub struct OraclePool<'a> { local_ballot_box_scan: Option>, pool_box_scan: PoolBoxScan<'a>, refresh_box_scan: RefreshBoxScan<'a>, + ballot_boxes_scan: BallotBoxesScan<'a>, + update_box_scan: UpdateBoxScan<'a>, } #[derive(Debug)] @@ -132,6 +149,17 @@ pub struct RefreshBoxScan<'a> { refresh_box_wrapper_inputs: RefreshBoxWrapperInputs<'a>, } +#[derive(Debug)] +pub struct BallotBoxesScan<'a> { + scan: Scan, + ballot_box_wrapper_inputs: BallotBoxWrapperInputs<'a>, +} +#[derive(Debug)] +pub struct UpdateBoxScan<'a> { + scan: Scan, + update_box_wrapper_inputs: UpdateBoxWrapperInputs<'a>, +} + /// The state of the oracle pool when it is in the Live Epoch stage #[derive(Debug, Clone)] pub struct LiveEpochState { @@ -171,6 +199,7 @@ impl<'a> OraclePool<'a> { pub fn new() -> std::result::Result, Error> { let config = &ORACLE_CONFIG; let local_oracle_address = config.oracle_address.clone(); + let oracle_pool_participant_token_id = config.token_ids.oracle_token_id.clone(); let data_point_source = config.data_point_source()?; @@ -199,6 +228,12 @@ impl<'a> OraclePool<'a> { oracle_token_id: &config.token_ids.oracle_token_id, pool_nft_token_id: &config.token_ids.pool_nft_token_id, }; + let update_box_wrapper_inputs = UpdateBoxWrapperInputs { + contract_parameters: &config.update_contract_parameters, + pool_nft_token_id: &config.token_ids.pool_nft_token_id, + update_nft_token_id: &config.token_ids.update_nft_token_id, + ballot_token_id: &config.token_ids.ballot_token_id, + }; // If scanIDs.json exists, skip registering scans & saving generated ids if !Path::new("scanIDs.json").exists() { @@ -208,6 +243,7 @@ impl<'a> OraclePool<'a> { &datapoint_contract_address, ) .unwrap(), + register_update_box_scan(&config.token_ids.update_nft_token_id).unwrap(), register_pool_box_scan(pool_box_wrapper_inputs).unwrap(), register_refresh_box_scan(refresh_box_scan_name, refresh_box_wrapper_inputs) .unwrap(), @@ -232,6 +268,13 @@ impl<'a> OraclePool<'a> { ) { scans.push(local_scan); } + scans.push( + register_ballot_box_scan( + &ballot_contract_address, + &config.token_ids.ballot_token_id, + ) + .unwrap(), + ); let res = save_scan_ids_locally(scans); if res.is_ok() { @@ -281,6 +324,11 @@ impl<'a> OraclePool<'a> { }); } + let ballot_boxes_scan = BallotBoxesScan { + scan: Scan::new("Ballot Box Scan", &scan_json["Ballot Box Scan"].to_string()), + ballot_box_wrapper_inputs, + }; + let pool_box_scan = PoolBoxScan { scan: Scan::new("Pool Box Scan", &scan_json["Pool Box Scan"].to_string()), pool_box_wrapper_inputs, @@ -294,6 +342,11 @@ impl<'a> OraclePool<'a> { refresh_box_wrapper_inputs, }; + let update_box_scan = UpdateBoxScan { + scan: Scan::new("Update Box Scan", &scan_json["Update Box Scan"].to_string()), + update_box_wrapper_inputs, + }; + // Create `OraclePool` struct Ok(OraclePool { data_point_source, @@ -306,8 +359,10 @@ impl<'a> OraclePool<'a> { }, local_oracle_datapoint_scan, local_ballot_box_scan, + ballot_boxes_scan, pool_box_scan, refresh_box_scan, + update_box_scan, }) } @@ -416,6 +471,10 @@ impl<'a> OraclePool<'a> { .map(|s| s as &dyn LocalBallotBoxSource) } + pub fn get_ballot_boxes_source(&self) -> &dyn VoteBallotBoxesSource { + &self.ballot_boxes_scan as &dyn VoteBallotBoxesSource + } + pub fn get_refresh_box_source(&self) -> &dyn RefreshBoxSource { &self.refresh_box_scan as &dyn RefreshBoxSource } @@ -429,6 +488,10 @@ impl<'a> OraclePool<'a> { .as_ref() .map(|s| s as &dyn LocalDatapointBoxSource) } + + pub fn get_update_box_source(&self) -> &dyn UpdateBoxSource { + &self.update_box_scan as &dyn UpdateBoxSource + } } impl<'a> PoolBoxSource for PoolBoxScan<'a> { @@ -462,6 +525,55 @@ impl<'a> LocalDatapointBoxSource for LocalOracleDatapointScan<'a> { } } +impl<'a> VoteBallotBoxesSource for BallotBoxesScan<'a> { + fn get_ballot_boxes(&self) -> Result> { + Ok(self + .scan + .get_boxes()? + .into_iter() + .map(|ballot_box| { + // Build Parameters for each Ballot Box + let ec = ballot_box + .get_register(NonMandatoryRegisterId::R4.into()) + .ok_or(BallotBoxError::NoGroupElementInR4)? + .try_extract_into::()?; + + let address = AddressEncoder::new( + self.ballot_box_wrapper_inputs + .parameters + .contract_parameters + .p2s + .network(), + ) + .address_to_str(&Address::P2Pk(ProveDlog::from(ec))); + + let ballot_box_wrapper_parameters = BallotBoxWrapperParameters { + vote_parameters: None, + ballot_token_owner_address: address, + ..self.ballot_box_wrapper_inputs.parameters.clone() + }; + let ballot_box_wrapper_inputs = BallotBoxWrapperInputs { + parameters: &ballot_box_wrapper_parameters, + ..self.ballot_box_wrapper_inputs + }; + Ok(VoteBallotBoxWrapper::new( + ballot_box, + ballot_box_wrapper_inputs, + )?) + }) + .filter_map(Result::ok) // Filter out boxes that are not participating in voting + .collect()) + } +} + +impl<'a> UpdateBoxSource for UpdateBoxScan<'a> { + fn get_update_box(&self) -> Result { + let box_wrapper = + UpdateBoxWrapper::new(self.scan.get_box()?, self.update_box_wrapper_inputs)?; + Ok(box_wrapper) + } +} + impl StageDataSource for Stage { /// Returns all boxes held at the given stage based on the registered scan fn get_boxes(&self) -> Result> { diff --git a/core/src/pool_commands/test_utils.rs b/core/src/pool_commands/test_utils.rs index cdffc534..b985858a 100644 --- a/core/src/pool_commands/test_utils.rs +++ b/core/src/pool_commands/test_utils.rs @@ -27,6 +27,8 @@ use crate::box_kind::BallotBoxWrapper; use crate::box_kind::OracleBoxWrapper; use crate::box_kind::PoolBoxWrapper; use crate::box_kind::PoolBoxWrapperInputs; +use crate::box_kind::UpdateBoxWrapper; +use crate::box_kind::VoteBallotBoxWrapper; use crate::contracts::oracle::OracleContract; use crate::contracts::oracle::OracleContractInputs; use crate::contracts::oracle::OracleContractParameters; @@ -36,6 +38,8 @@ use crate::contracts::pool::PoolContractParameters; use crate::node_interface::SignTransaction; use crate::oracle_config::TokenIds; use crate::oracle_state::LocalBallotBoxSource; +use crate::oracle_state::UpdateBoxSource; +use crate::oracle_state::VoteBallotBoxesSource; use crate::oracle_state::{LocalDatapointBoxSource, PoolBoxSource, StageError}; use super::*; @@ -73,6 +77,16 @@ impl LocalBallotBoxSource for BallotBoxMock { } } +pub struct BallotBoxesMock { + pub ballot_boxes: Vec, +} + +impl VoteBallotBoxesSource for BallotBoxesMock { + fn get_ballot_boxes(&self) -> std::result::Result, StageError> { + Ok(self.ballot_boxes.clone()) + } +} + #[derive(Clone)] pub(crate) struct WalletDataMock { pub unspent_boxes: Vec, @@ -84,6 +98,16 @@ impl WalletDataSource for WalletDataMock { } } +pub(crate) struct UpdateBoxMock { + pub update_box: UpdateBoxWrapper, +} + +impl UpdateBoxSource for UpdateBoxMock { + fn get_update_box(&self) -> crate::oracle_state::Result { + Ok(self.update_box.clone()) + } +} + pub(crate) fn make_pool_box( datapoint: i64, epoch_counter: i32, diff --git a/core/src/scans.rs b/core/src/scans.rs index 593a24b1..3d6cc4b4 100644 --- a/core/src/scans.rs +++ b/core/src/scans.rs @@ -1,6 +1,6 @@ use crate::address_util::{address_to_raw_for_register, AddressUtilError}; use crate::box_kind::{PoolBoxWrapperInputs, RefreshBoxWrapperInputs}; -use crate::contracts::pool::PoolContract; +use crate::contracts::pool::{PoolContract, PoolContractError}; use crate::contracts::refresh::{RefreshContract, RefreshContractError}; /// This file holds logic related to UTXO-set scans use crate::node_interface::{get_scan_boxes, register_scan, serialize_box, serialize_boxes}; @@ -9,6 +9,8 @@ use derive_more::From; use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox; use ergo_lib::ergotree_ir::chain::token::TokenId; use ergo_lib::ergotree_ir::ergo_tree::ErgoTree; +use ergo_lib::ergotree_ir::mir::constant::Constant; +use ergo_lib::ergotree_ir::serialization::SigmaSerializable; use ergo_node_interface::node_interface::NodeError; use log::info; use serde_json::json; @@ -31,6 +33,8 @@ pub enum ScanError { IoError(std::io::Error), #[error("refresh contract error: {0}")] RefreshContract(RefreshContractError), + #[error("pool contract error: {0}")] + PoolContract(PoolContractError), #[error("address util error: {0}")] AddressUtilError(AddressUtilError), } @@ -117,11 +121,9 @@ pub fn save_scan_ids_locally(scans: Vec) -> Result { /// This function registers scanning for the pool box pub fn register_pool_box_scan(inputs: PoolBoxWrapperInputs) -> Result { // ErgoTree bytes of the P2S address/script - let pool_box_tree_bytes = PoolContract::new(inputs.into()) - .unwrap() + let pool_box_tree_bytes = PoolContract::new(inputs.into())? .ergo_tree() - .to_base16_bytes() - .unwrap(); + .to_scan_bytes(); // Scan for NFT id + Oracle Pool Epoch address let scan_json = json! ( { @@ -133,7 +135,7 @@ pub fn register_pool_box_scan(inputs: PoolBoxWrapperInputs) -> Result { }, { "predicate": "equals", - "value": pool_box_tree_bytes.clone(), + "value": &pool_box_tree_bytes } ] } ); @@ -149,8 +151,7 @@ pub fn register_refresh_box_scan( // ErgoTree bytes of the P2S address/script let tree_bytes = RefreshContract::new(inputs.into())? .ergo_tree() - .to_base16_bytes() - .unwrap(); + .to_scan_bytes(); // Scan for NFT id + Oracle Pool Epoch address let scan_json = json! ( { @@ -162,7 +163,7 @@ pub fn register_refresh_box_scan( }, { "predicate": "equals", - "value": tree_bytes.clone(), + "value": tree_bytes, } ] } ); @@ -178,6 +179,7 @@ pub fn register_local_oracle_datapoint_scan( ) -> Result { // Raw EC bytes + type identifier let oracle_add_bytes = address_to_raw_for_register(oracle_address)?; + let datapoint_bytes = datapoint_address.to_scan_bytes(); // Scan for pool participant token id + datapoint contract address + oracle_address in R4 let scan_json = json! ( { @@ -189,7 +191,7 @@ pub fn register_local_oracle_datapoint_scan( }, { "predicate": "equals", - "value": datapoint_address.to_base16_bytes().unwrap(), + "value": datapoint_bytes, }, { "predicate": "equals", @@ -207,6 +209,7 @@ pub fn register_datapoint_scan( oracle_pool_participant_token: &TokenId, datapoint_address: &ErgoTree, ) -> Result { + let datapoint_bytes = datapoint_address.to_scan_bytes(); // Scan for pool participant token id + datapoint contract address + oracle_address in R4 let scan_json = json! ( { "predicate": "and", @@ -217,7 +220,7 @@ pub fn register_datapoint_scan( }, { "predicate": "equals", - "value": datapoint_address.to_base16_bytes().unwrap(), + "value": datapoint_bytes, } ] } ); @@ -233,6 +236,7 @@ pub fn register_local_ballot_box_scan( ) -> Result { // Raw EC bytes + type identifier let ballot_add_bytes = address_to_raw_for_register(ballot_token_owner_address)?; + let ballot_contract_bytes = ballot_contract_address.to_scan_bytes(); // Scan for pool participant token id + datapoint contract address + oracle_address in R4 let scan_json = json! ( { "predicate": "and", @@ -243,7 +247,7 @@ pub fn register_local_ballot_box_scan( }, { "predicate": "equals", - "value": ballot_contract_address.to_base16_bytes().unwrap(), + "value": ballot_contract_bytes, }, { "predicate": "equals", @@ -255,3 +259,51 @@ pub fn register_local_ballot_box_scan( Scan::register("Local Ballot Box Scan", scan_json) } + +/// Scan for all ballot boxes matching token id of oracle pool. When updating the pool box only ballot boxes voting for the new pool will be spent +pub fn register_ballot_box_scan( + ballot_contract_address: &ErgoTree, + ballot_token_id: &TokenId, +) -> Result { + let scan_json = json! ( { + "predicate": "and", + "args": [ + { + "predicate": "containsAsset", + "assetId": ballot_token_id.clone(), + }, + { + "predicate": "equals", + "value": ballot_contract_address.to_scan_bytes(), + } + ] }); + Scan::register("Ballot Box Scan", scan_json) +} + +// TODO: We don't currently scan for ErgoTree, since config does not store min_votes +pub fn register_update_box_scan(update_nft_token_id: &TokenId) -> Result { + let scan_json = json! ( { + "predicate": "and", + "args": [ + { + "predicate": "containsAsset", + "assetId": update_nft_token_id.clone(), + }, + ] }); + Scan::register("Update Box Scan", scan_json) +} + +/// Convert a chain type to Coll[Byte] for scans +pub trait ToScanBytes { + fn to_scan_bytes(&self) -> String; +} + +impl ToScanBytes for ErgoTree { + fn to_scan_bytes(&self) -> String { + base16::encode_lower( + &Constant::from(self.sigma_serialize_bytes().unwrap()) + .sigma_serialize_bytes() + .unwrap(), + ) + } +} diff --git a/core/src/serde.rs b/core/src/serde.rs index 6627cc6b..33c0b6d5 100644 --- a/core/src/serde.rs +++ b/core/src/serde.rs @@ -1,6 +1,6 @@ //! Types to allow oracle configuration to convert to and from Serde. -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use ergo_lib::ergotree_ir::chain::{ address::{AddressEncoder, AddressEncoderError, NetworkAddress, NetworkPrefix}, @@ -10,7 +10,10 @@ use log::LevelFilter; use serde::{Deserialize, Serialize}; use crate::{ - cli_commands::bootstrap::{Addresses, BootstrapConfig, TokensToMint}, + cli_commands::{ + bootstrap::{Addresses, BootstrapConfig, TokensToMint}, + prepare_update::{UpdateBootstrapConfig, UpdateTokensToMint}, + }, contracts::{ ballot::BallotContractParameters, oracle::OracleContractParameters, pool::PoolContractParameters, refresh::RefreshContractParameters, @@ -38,6 +41,7 @@ pub(crate) struct OracleConfigSerde { oracle_contract_parameters: OracleContractParametersSerde, pool_contract_parameters: PoolContractParametersSerde, refresh_contract_parameters: RefreshContractParametersSerde, + update_contract_parameters: UpdateContractParametersSerde, ballot_parameters: BallotBoxWrapperParametersSerde, token_ids: TokenIds, addresses: AddressesSerde, @@ -60,6 +64,8 @@ impl TryFrom for OracleConfig { let refresh_contract_parameters = RefreshContractParameters::try_from((c.refresh_contract_parameters, prefix))?; + let update_contract_parameters = + UpdateContractParameters::try_from((c.update_contract_parameters, prefix))?; let ballot_parameters = BallotBoxWrapperParameters { contract_parameters: BallotContractParameters::try_from(( @@ -83,6 +89,7 @@ impl TryFrom for OracleConfig { oracle_contract_parameters, pool_contract_parameters, refresh_contract_parameters, + update_contract_parameters, ballot_parameters, token_ids: c.token_ids, addresses: Addresses::try_from((c.addresses, prefix))?, @@ -105,6 +112,8 @@ impl From for OracleConfigSerde { vote_parameters: c.ballot_parameters.vote_parameters, ballot_token_owner_address: c.ballot_parameters.ballot_token_owner_address, }; + let update_contract_parameters = + UpdateContractParametersSerde::from(c.update_contract_parameters); let prefix = if c.on_mainnet { NetworkPrefix::Mainnet @@ -127,6 +136,7 @@ impl From for OracleConfigSerde { pool_contract_parameters, refresh_contract_parameters, ballot_parameters, + update_contract_parameters, token_ids: c.token_ids, addresses: AddressesSerde::from((c.addresses, prefix)), } @@ -247,7 +257,7 @@ impl TryFrom for BootstrapConfig { } #[derive(Debug, Clone, Serialize, Deserialize)] -struct OracleContractParametersSerde { +pub struct OracleContractParametersSerde { p2s: String, pool_nft_index: usize, } @@ -438,6 +448,46 @@ impl From for UpdateContractParametersSerde { } } +#[derive(Clone, Deserialize)] +pub struct UpdateBootstrapConfigSerde { + pool_contract_parameters: Option, + refresh_contract_parameters: Option, + update_contract_parameters: Option, + tokens_to_mint: UpdateTokensToMint, + addresses: AddressesSerde, +} + +impl TryFrom for UpdateBootstrapConfig { + type Error = AddressEncoderError; + fn try_from(c: UpdateBootstrapConfigSerde) -> Result { + let prefix = if crate::oracle_config::ORACLE_CONFIG.on_mainnet { + NetworkPrefix::Mainnet + } else { + NetworkPrefix::Testnet + }; + let pool_contract_parameters = c + .pool_contract_parameters + .map(|r| (r, prefix).try_into()) + .transpose()?; + let refresh_contract_parameters = c + .refresh_contract_parameters + .map(|r| (r, prefix).try_into()) + .transpose()?; + let update_contract_parameters = c + .update_contract_parameters + .map(|r| (r, prefix).try_into()) + .transpose()?; + let addresses = (c.addresses, prefix).try_into()?; + Ok(UpdateBootstrapConfig { + pool_contract_parameters, + refresh_contract_parameters, + update_contract_parameters, + tokens_to_mint: c.tokens_to_mint, + addresses, + }) + } +} + pub(crate) fn token_id_as_base64_string( value: &TokenId, serializer: S,