From 6791e9c9f7cf48bbfc195cb196ad41da91f361c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lech=20G=C5=82owiak?= Date: Tue, 7 Jan 2025 17:03:05 +0100 Subject: [PATCH 1/3] feat: part 1 of 'reserve deposit' transaction submission --- toolkit/offchain/src/reserve/deposit.rs | 117 ++++++++++++++++++ toolkit/offchain/src/reserve/init.rs | 2 +- toolkit/offchain/src/reserve/mod.rs | 19 ++- toolkit/offchain/src/scripts_data.rs | 8 +- toolkit/offchain/tests/integration_tests.rs | 9 +- toolkit/primitives/plutus-data/src/reserve.rs | 89 ++++++++++++- 6 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 toolkit/offchain/src/reserve/deposit.rs diff --git a/toolkit/offchain/src/reserve/deposit.rs b/toolkit/offchain/src/reserve/deposit.rs new file mode 100644 index 000000000..6aa1195d0 --- /dev/null +++ b/toolkit/offchain/src/reserve/deposit.rs @@ -0,0 +1,117 @@ +//! +//! Specification for deposit transaction: +//! +//! Consumes: +//! - UTXO at the validator address +//! - UTXOs at payment address that have tokens to be deposited +//! +//! Produces: +//! - UTXO at the validator address with increased token amount +//! - UTXO at the payment address with change +//! +//! Reference UTOXs: +//! - Version Oracle Validator script +//! - Reserve Auth Policy script +//! - Reserve Validator script +//! - Illiquid Supply Validator script + +use super::ReserveData; +use crate::{ + await_tx::AwaitTx, + csl::{ + convert_value, empty_asset_name, get_builder_config, get_validator_budgets, zero_ex_units, + OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, UtxoIdExt, + }, + init_governance::{get_governance_data, GovernanceData}, + reserve::get_reserve_data, + scripts_data::ReserveScripts, +}; +use anyhow::anyhow; +use cardano_serialization_lib::{ + Assets, BigInt, ConstrPlutusData, DataCost, ExUnits, JsError, Language, MinOutputAdaCalculator, + MultiAsset, PlutusData, PlutusList, PlutusScriptSource, PlutusWitness, Redeemer, RedeemerTag, + Transaction, TransactionBuilder, TransactionOutput, TransactionOutputBuilder, TxInputsBuilder, +}; +use ogmios_client::{ + query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}, + query_network::QueryNetwork, + transactions::{OgmiosEvaluateTransactionResponse, Transactions}, + types::OgmiosUtxo, +}; +use partner_chains_plutus_data::reserve::ReserveDatum; +use sidechain_domain::{McTxHash, TokenId, UtxoId}; + +pub struct TokenAmount { + pub token: TokenId, + pub amount: u64, +} + +/// Spends current UTXO at validator address and creates a new UTXO with increased token amount +pub async fn deposit_to_reserve< + T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId, + A: AwaitTx, +>( + parameters: TokenAmount, + genesis_utxo: UtxoId, + payment_key: [u8; 32], + client: &T, + _await_tx: &A, +) -> anyhow::Result { + let ctx = TransactionContext::for_payment_key(payment_key, client).await?; + let _governance = get_governance_data(genesis_utxo, client).await?; + let reserve = get_reserve_data(genesis_utxo, &ctx, client).await?; + + let utxo = get_utxo_with_tokens(&reserve.scripts, ¶meters.token, &ctx, client).await? + .ok_or_else(||anyhow!("There are not UTXOs in the Reserve Validator address that contain token Reserve Auth Policy Token. Have Reserve been created already?"))?; + let current_amount = get_token_amount(&utxo, ¶meters.token); + let _token_amount = + TokenAmount { token: parameters.token, amount: current_amount + parameters.amount }; + + todo!("implement the rest in the next PR"); +} + +async fn get_utxo_with_tokens( + reward_scripts: &ReserveScripts, + token_id: &TokenId, + ctx: &TransactionContext, + client: &T, +) -> Result, anyhow::Error> { + let validator_address = reward_scripts.validator.address_bech32(ctx.network)?; + let utxos = client.query_utxos(&[validator_address.clone()]).await?; + Ok(utxos + .into_iter() + .find(|utxo| { + utxo.value.native_tokens.contains_key(&reward_scripts.auth_policy.script_hash()) + && utxo + .datum + .clone() + .and_then(|datum| { + decode_reserve_datum(datum.bytes).filter(|reserve_datum| { + reserve_datum.immutable_settings.token == *token_id + }) + }) + .is_some() + }) + .clone()) +} + +fn decode_reserve_datum(datum_bytes: Vec) -> Option { + PlutusData::from_bytes(datum_bytes) + .ok() + .and_then(|plutus_data| ReserveDatum::try_from(plutus_data).ok()) +} + +fn get_token_amount(utxo: &OgmiosUtxo, token_id: &TokenId) -> u64 { + match token_id { + TokenId::Ada => utxo.value.lovelace, + TokenId::AssetId { policy_id, asset_name } => utxo + .value + .native_tokens + .get(&policy_id.0) + .and_then(|assets| assets.iter().find(|asset| asset.name == asset_name.0.to_vec())) + .map(|asset| asset.amount) + .unwrap_or(0) // Token can be not found if the reserve was created with the initial deposit of 0 tokens + .try_into() + .expect("Token amount in an UTXO always fits u64"), + } +} diff --git a/toolkit/offchain/src/reserve/init.rs b/toolkit/offchain/src/reserve/init.rs index 1333e417b..5ab89e8f9 100644 --- a/toolkit/offchain/src/reserve/init.rs +++ b/toolkit/offchain/src/reserve/init.rs @@ -287,7 +287,7 @@ async fn script_is_initialized< } // Finds an UTXO at Version Oracle Validator with Datum that contains -// * script id of the script being initialized +// * given script id // * Version Oracle Policy Id pub(crate) async fn find_script_utxo< T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId, diff --git a/toolkit/offchain/src/reserve/mod.rs b/toolkit/offchain/src/reserve/mod.rs index 75d7185ef..c08e386ee 100644 --- a/toolkit/offchain/src/reserve/mod.rs +++ b/toolkit/offchain/src/reserve/mod.rs @@ -11,12 +11,14 @@ use ogmios_client::{ use sidechain_domain::UtxoId; pub mod create; +pub mod deposit; pub mod init; pub(crate) struct ReserveData { pub(crate) scripts: scripts_data::ReserveScripts, pub(crate) auth_policy_version_utxo: UtxoId, pub(crate) validator_version_utxo: UtxoId, + pub(crate) illiquid_circulation_supply_validator_version_utxo: UtxoId, } pub(crate) async fn get_reserve_data< @@ -47,6 +49,21 @@ pub(crate) async fn get_reserve_data< .ok_or_else(|| { anyhow!("Reserve Validator Version Utxo not found, is the Reserve Token Management initialized?") })?; + let illiquid_circulation_supply_validator_version_utxo = find_script_utxo( + raw_scripts::ScriptId::IlliquidCirculationSupplyValidator as u32, + &version_oracle, + ctx, + client, + ) + .await? + .ok_or_else(|| { + anyhow!("Reserve Validator Version Utxo not found, is the Reserve Token Management initialized?") + })?; let scripts = scripts_data::reserve_scripts(genesis_utxo, ctx.network)?; - Ok(ReserveData { scripts, auth_policy_version_utxo, validator_version_utxo }) + Ok(ReserveData { + scripts, + auth_policy_version_utxo, + validator_version_utxo, + illiquid_circulation_supply_validator_version_utxo, + }) } diff --git a/toolkit/offchain/src/scripts_data.rs b/toolkit/offchain/src/scripts_data.rs index 1a3fb30b3..13e83fdd9 100644 --- a/toolkit/offchain/src/scripts_data.rs +++ b/toolkit/offchain/src/scripts_data.rs @@ -193,6 +193,7 @@ pub(crate) fn registered_candidates_scripts( pub(crate) struct ReserveScripts { pub(crate) validator: PlutusScript, pub(crate) auth_policy: PlutusScript, + pub(crate) illiquid_circulation_supply_validator: PlutusScript, } pub(crate) fn reserve_scripts( @@ -204,7 +205,12 @@ pub(crate) fn reserve_scripts( .apply_uplc_data(version_oracle_data.policy_id_as_plutus_data())?; let auth_policy = PlutusScript::from_wrapped_cbor(raw_scripts::RESERVE_AUTH_POLICY, PlutusV2)? .apply_uplc_data(version_oracle_data.policy_id_as_plutus_data())?; - Ok(ReserveScripts { validator, auth_policy }) + let illiquid_circulation_supply_validator = PlutusScript::from_wrapped_cbor( + raw_scripts::ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR, + PlutusV2, + )? + .apply_uplc_data(version_oracle_data.policy_id_as_plutus_data())?; + Ok(ReserveScripts { validator, auth_policy, illiquid_circulation_supply_validator }) } // Returns the simplest MultiSig policy configuration plutus data: diff --git a/toolkit/offchain/tests/integration_tests.rs b/toolkit/offchain/tests/integration_tests.rs index 19d0692ec..23b66f280 100644 --- a/toolkit/offchain/tests/integration_tests.rs +++ b/toolkit/offchain/tests/integration_tests.rs @@ -56,7 +56,8 @@ const EVE_ADDRESS: &str = "addr_test1vzzt5pwz3pum9xdgxalxyy52m3aqur0n43pcl727l37 const REWARDS_TOKEN_POLICY_ID: PolicyId = PolicyId(hex!("1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2b")); -const REWARDS_TOKEN_ASSET_NAME_STR: &str = "5043526f636b73"; +// Reward token +const REWARDS_TOKEN_ASSET_NAME_STR: &str = "52657761726420746f6b656e"; #[tokio::test] async fn init_goveranance() { @@ -146,11 +147,11 @@ async fn await_ogmios(client: &T) -> Result<(), OgmiosClientErr /// * governance authority: 1000000 REWARDS_TOKEN /// * "dave" address: addr_test1vphpcf32drhhznv6rqmrmgpuwq06kug0lkg22ux777rtlqst2er0r /// * "eve" address: addr_test1vzzt5pwz3pum9xdgxalxyy52m3aqur0n43pcl727l37ggscl8h7v8 -/// Its hash is 0xc389187c6cabf1cd2ca64cf8c76bf57288eb9c02ced6781935b810a1d0e7fbb4 +/// Its hash is 0x61ca664e056ce49a9d4fd2fb3aa2b750ea753fe4ad5c9e6167482fd88394cf7d async fn initial_transaction( client: &T, ) -> Result { - let signed_tx_bytes = hex!("84a400d9010281825820781cb948a37c7c38b43872af9b1e22135a94826eafd3740260a6db0a303885d800018682581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d606e1c262a68ef714d9a18363da03c701fab710ffd90a570def786bf821a3b9aca0082581d6084ba05c28879b299a8377e62128adc7a0e0df3ac438ff95efc7c84431a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1b006a8e81dfdc1f4082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b821a00989680a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba1475043526f636b731a000f4240021a000f424009a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba1475043526f636b731a000f4240a200d9010282825820e6ceac21f27c463f9065fafdc62883d7e52f6a376b498b8838ba513e44c74eca5840c1fbdbb6710bfceb40644b1177627d3938a8645301a53b418a489874f59f6cf5d5ff791d7bc50d0d53177eb1b52f174bf4a2ced4bb2c2ddd0c9d2b62196f6500825820fc014cb5f071f5d6a36cb5a7e5f168c86555989445a23d4abec33d280f71aca458409054d37928f13dc0c9d7739fc3ac2fb4b8ec94a655858813bd3b2e450d2b15be368eea9fcb47d1eadb337c1d5512ff160c662220a1f55e3fd2b54065d2f6170c01d90102818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2bf5f6"); + let signed_tx_bytes = hex!("84a400d9010281825820781cb948a37c7c38b43872af9b1e22135a94826eafd3740260a6db0a303885d800018682581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d606e1c262a68ef714d9a18363da03c701fab710ffd90a570def786bf821a3b9aca0082581d6084ba05c28879b299a8377e62128adc7a0e0df3ac438ff95efc7c84431a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1b006a8e81dfdc1f4082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b821a00989680a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba14c52657761726420746f6b656e1a000f4240021a000f424009a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba14c52657761726420746f6b656e1a000f4240a200d9010282825820e6ceac21f27c463f9065fafdc62883d7e52f6a376b498b8838ba513e44c74eca58406c09c0a1bf773bbcb91cdaff46a6d7548268d2f1dbc7c203dbf4e1f1cd031895faede520f10d7758b8279d4c68484f1a055792e0881a5becf91bf5d8e861410b825820fc014cb5f071f5d6a36cb5a7e5f168c86555989445a23d4abec33d280f71aca4584083c00332dc76cd42ed33610f8a56efa0ced659b3752e5f80ee8176e726c48715c2cbdb544bf4eb4d424902d2861ab1c7deabfcfe795f779795ed9abc3dcfa10f01d90102818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2bf5f6"); let tx_hash = client .submit_transaction(&signed_tx_bytes) .await @@ -270,7 +271,7 @@ async fn run_create_reserve_management< policy_id: REWARDS_TOKEN_POLICY_ID, asset_name: AssetName::from_hex_unsafe(REWARDS_TOKEN_ASSET_NAME_STR), }, - initial_deposit: 1000000, + initial_deposit: 500000, }, genesis_utxo, GOVERNANCE_AUTHORITY_PAYMENT_KEY.0, diff --git a/toolkit/primitives/plutus-data/src/reserve.rs b/toolkit/primitives/plutus-data/src/reserve.rs index 8c9af1823..38bd5dad2 100644 --- a/toolkit/primitives/plutus-data/src/reserve.rs +++ b/toolkit/primitives/plutus-data/src/reserve.rs @@ -1,23 +1,27 @@ -use crate::VersionedGenericDatumShape; +use crate::{DataDecodingError, DecodingResult, VersionedDatum, VersionedGenericDatumShape}; use cardano_serialization_lib::{BigInt, BigNum, ConstrPlutusData, PlutusData, PlutusList}; -use sidechain_domain::{PolicyId, TokenId}; +use sidechain_domain::{AssetName, PolicyId, TokenId}; +#[derive(Debug, Clone)] pub struct ReserveDatum { pub immutable_settings: ReserveImmutableSettings, pub mutable_settings: ReserveMutableSettings, pub stats: ReserveStats, } +#[derive(Debug, Clone)] pub struct ReserveImmutableSettings { pub t0: u64, pub token: TokenId, } +#[derive(Debug, Clone)] pub struct ReserveMutableSettings { pub total_accrued_function_script_hash: PolicyId, pub initial_incentive: u64, } +#[derive(Debug, Clone)] pub struct ReserveStats { pub token_total_amount_transferred: u64, } @@ -70,3 +74,84 @@ impl From for PlutusData { .into() } } + +impl TryFrom for ReserveDatum { + type Error = DataDecodingError; + + fn try_from(datum: PlutusData) -> DecodingResult { + Self::decode(&datum) + } +} + +impl VersionedDatum for ReserveDatum { + const NAME: &str = "ReserveDatum"; + + fn decode_legacy(_: &PlutusData) -> Result { + Err("ReserveDatum supports only versioned format".into()) + } + + fn decode_versioned(version: u32, datum: &PlutusData, _: &PlutusData) -> Result { + match version { + 0 => decode_v0_reserve_datum(datum).ok_or("Can not parse ReserveDatum".to_string()), + _ => Err(format!("Unknown version: {version}")), + } + } +} + +fn decode_v0_reserve_datum(datum: &PlutusData) -> Option { + let outer_list = datum.as_list()?; + let mut outer_iter = outer_list.into_iter(); + + let immutable_settings_list = outer_iter.next()?.as_list()?; + let mut immutable_settings_iter = immutable_settings_list.into_iter(); + let t0: u64 = immutable_settings_iter.next()?.as_integer()?.as_u64()?.into(); + let token = decode_token_id_datum(immutable_settings_iter.next()?)?; + + let v_function_hash_and_initial_incentive_list = outer_iter.next()?.as_list()?; + let mut v_function_hash_and_initial_incentive_iter = + v_function_hash_and_initial_incentive_list.into_iter(); + let total_accrued_function_script_hash = PolicyId( + v_function_hash_and_initial_incentive_iter + .next()? + .as_bytes()? + .to_vec() + .try_into() + .ok()?, + ); + let initial_incentive = v_function_hash_and_initial_incentive_iter + .next()? + .as_integer()? + .as_u64()? + .into(); + + let stats = ReserveStats { + token_total_amount_transferred: outer_iter.next()?.as_integer()?.as_u64()?.into(), + }; + + Some(ReserveDatum { + immutable_settings: ReserveImmutableSettings { t0, token }, + mutable_settings: ReserveMutableSettings { + total_accrued_function_script_hash, + initial_incentive, + }, + stats, + }) +} + +fn decode_token_id_datum(pd: &PlutusData) -> Option { + let token_id_list = pd + .as_constr_plutus_data() + .filter(|constr| constr.alternative() == BigNum::zero()) + .map(|constr| constr.data())?; + let mut token_id_list_iter = token_id_list.into_iter(); + let policy_id = token_id_list_iter.next()?.as_bytes()?.to_vec(); + let asset_name = token_id_list_iter.next()?.as_bytes()?.to_vec(); + if policy_id.is_empty() && asset_name.is_empty() { + Some(TokenId::Ada) + } else { + Some(TokenId::AssetId { + policy_id: PolicyId(policy_id.try_into().ok()?), + asset_name: AssetName(asset_name.try_into().ok()?), + }) + } +} From 628056d014218f72dddf28b69a3fcdf268c474ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lech=20G=C5=82owiak?= Date: Thu, 9 Jan 2025 08:03:34 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Nikolaos Dymitriadis Co-authored-by: Krisztian Pinter <159046756+kpinter-iohk@users.noreply.github.com> --- toolkit/offchain/src/reserve/deposit.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/toolkit/offchain/src/reserve/deposit.rs b/toolkit/offchain/src/reserve/deposit.rs index 6aa1195d0..33f525a49 100644 --- a/toolkit/offchain/src/reserve/deposit.rs +++ b/toolkit/offchain/src/reserve/deposit.rs @@ -62,7 +62,7 @@ pub async fn deposit_to_reserve< let reserve = get_reserve_data(genesis_utxo, &ctx, client).await?; let utxo = get_utxo_with_tokens(&reserve.scripts, ¶meters.token, &ctx, client).await? - .ok_or_else(||anyhow!("There are not UTXOs in the Reserve Validator address that contain token Reserve Auth Policy Token. Have Reserve been created already?"))?; + .ok_or_else(||anyhow!("There are no UTXOs in the Reserve Validator address that contain token Reserve Auth Policy Token. Has Reserve been created already?"))?; let current_amount = get_token_amount(&utxo, ¶meters.token); let _token_amount = TokenAmount { token: parameters.token, amount: current_amount + parameters.amount }; @@ -85,12 +85,11 @@ async fn get_utxo_with_tokens( && utxo .datum .clone() - .and_then(|datum| { - decode_reserve_datum(datum.bytes).filter(|reserve_datum| { + .is_some_and(|datum| { + decode_reserve_datum(datum.bytes).is_some_and(|reserve_datum| { reserve_datum.immutable_settings.token == *token_id }) }) - .is_some() }) .clone()) } From e2612b123e4e45eb59aee6bbc736f172c2a7f665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lech=20G=C5=82owiak?= Date: Thu, 9 Jan 2025 08:13:36 +0100 Subject: [PATCH 3/3] Refactor --- toolkit/offchain/src/reserve/deposit.rs | 34 ++++++++----------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/toolkit/offchain/src/reserve/deposit.rs b/toolkit/offchain/src/reserve/deposit.rs index 33f525a49..28ad94d88 100644 --- a/toolkit/offchain/src/reserve/deposit.rs +++ b/toolkit/offchain/src/reserve/deposit.rs @@ -15,27 +15,16 @@ //! - Reserve Validator script //! - Illiquid Supply Validator script -use super::ReserveData; use crate::{ - await_tx::AwaitTx, - csl::{ - convert_value, empty_asset_name, get_builder_config, get_validator_budgets, zero_ex_units, - OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, UtxoIdExt, - }, - init_governance::{get_governance_data, GovernanceData}, - reserve::get_reserve_data, - scripts_data::ReserveScripts, + await_tx::AwaitTx, csl::TransactionContext, init_governance::get_governance_data, + reserve::get_reserve_data, scripts_data::ReserveScripts, }; use anyhow::anyhow; -use cardano_serialization_lib::{ - Assets, BigInt, ConstrPlutusData, DataCost, ExUnits, JsError, Language, MinOutputAdaCalculator, - MultiAsset, PlutusData, PlutusList, PlutusScriptSource, PlutusWitness, Redeemer, RedeemerTag, - Transaction, TransactionBuilder, TransactionOutput, TransactionOutputBuilder, TxInputsBuilder, -}; +use cardano_serialization_lib::PlutusData; use ogmios_client::{ query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}, query_network::QueryNetwork, - transactions::{OgmiosEvaluateTransactionResponse, Transactions}, + transactions::Transactions, types::OgmiosUtxo, }; use partner_chains_plutus_data::reserve::ReserveDatum; @@ -82,20 +71,17 @@ async fn get_utxo_with_tokens( .into_iter() .find(|utxo| { utxo.value.native_tokens.contains_key(&reward_scripts.auth_policy.script_hash()) - && utxo - .datum - .clone() - .is_some_and(|datum| { - decode_reserve_datum(datum.bytes).is_some_and(|reserve_datum| { - reserve_datum.immutable_settings.token == *token_id - }) + && utxo.datum.clone().is_some_and(|datum| { + decode_reserve_datum(datum).is_some_and(|reserve_datum| { + reserve_datum.immutable_settings.token == *token_id }) + }) }) .clone()) } -fn decode_reserve_datum(datum_bytes: Vec) -> Option { - PlutusData::from_bytes(datum_bytes) +fn decode_reserve_datum(datum: ogmios_client::types::Datum) -> Option { + PlutusData::from_bytes(datum.bytes) .ok() .and_then(|plutus_data| ReserveDatum::try_from(plutus_data).ok()) }