Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: part 1 of 'reserve deposit' transaction submission #354

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions toolkit/offchain/src/reserve/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//!
//! 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 crate::{
await_tx::AwaitTx, csl::TransactionContext, init_governance::get_governance_data,
reserve::get_reserve_data, scripts_data::ReserveScripts,
};
use anyhow::anyhow;
use cardano_serialization_lib::PlutusData;
use ogmios_client::{
query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
query_network::QueryNetwork,
transactions::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<McTxHash> {
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, &parameters.token, &ctx, client).await?
.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, &parameters.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<T: QueryLedgerState>(
reward_scripts: &ReserveScripts,
token_id: &TokenId,
ctx: &TransactionContext,
client: &T,
) -> Result<Option<OgmiosUtxo>, 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().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: ogmios_client::types::Datum) -> Option<ReserveDatum> {
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"),
}
}
2 changes: 1 addition & 1 deletion toolkit/offchain/src/reserve/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion toolkit/offchain/src/reserve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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,
})
}
8 changes: 7 additions & 1 deletion toolkit/offchain/src/scripts_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions toolkit/offchain/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
LGLO marked this conversation as resolved.
Show resolved Hide resolved

#[tokio::test]
async fn init_goveranance() {
Expand Down Expand Up @@ -146,11 +147,11 @@ async fn await_ogmios<T: QueryNetwork>(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<T: Transactions + QueryUtxoByUtxoId>(
client: &T,
) -> Result<McTxHash, ()> {
let signed_tx_bytes = hex!("84a400d9010281825820781cb948a37c7c38b43872af9b1e22135a94826eafd3740260a6db0a303885d800018682581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d606e1c262a68ef714d9a18363da03c701fab710ffd90a570def786bf821a3b9aca0082581d6084ba05c28879b299a8377e62128adc7a0e0df3ac438ff95efc7c84431a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1b006a8e81dfdc1f4082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b821a00989680a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba1475043526f636b731a000f4240021a000f424009a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba1475043526f636b731a000f4240a200d9010282825820e6ceac21f27c463f9065fafdc62883d7e52f6a376b498b8838ba513e44c74eca5840c1fbdbb6710bfceb40644b1177627d3938a8645301a53b418a489874f59f6cf5d5ff791d7bc50d0d53177eb1b52f174bf4a2ced4bb2c2ddd0c9d2b62196f6500825820fc014cb5f071f5d6a36cb5a7e5f168c86555989445a23d4abec33d280f71aca458409054d37928f13dc0c9d7739fc3ac2fb4b8ec94a655858813bd3b2e450d2b15be368eea9fcb47d1eadb337c1d5512ff160c662220a1f55e3fd2b54065d2f6170c01d90102818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2bf5f6");
let signed_tx_bytes = hex!("84a400d9010281825820781cb948a37c7c38b43872af9b1e22135a94826eafd3740260a6db0a303885d800018682581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1a3b9aca0082581d606e1c262a68ef714d9a18363da03c701fab710ffd90a570def786bf821a3b9aca0082581d6084ba05c28879b299a8377e62128adc7a0e0df3ac438ff95efc7c84431a3b9aca0082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b1b006a8e81dfdc1f4082581d60e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b821a00989680a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba14c52657761726420746f6b656e1a000f4240021a000f424009a1581c1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2ba14c52657761726420746f6b656e1a000f4240a200d9010282825820e6ceac21f27c463f9065fafdc62883d7e52f6a376b498b8838ba513e44c74eca58406c09c0a1bf773bbcb91cdaff46a6d7548268d2f1dbc7c203dbf4e1f1cd031895faede520f10d7758b8279d4c68484f1a055792e0881a5becf91bf5d8e861410b825820fc014cb5f071f5d6a36cb5a7e5f168c86555989445a23d4abec33d280f71aca4584083c00332dc76cd42ed33610f8a56efa0ced659b3752e5f80ee8176e726c48715c2cbdb544bf4eb4d424902d2861ab1c7deabfcfe795f779795ed9abc3dcfa10f01d90102818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2bf5f6");
let tx_hash = client
.submit_transaction(&signed_tx_bytes)
.await
Expand Down Expand Up @@ -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,
Expand Down
89 changes: 87 additions & 2 deletions toolkit/primitives/plutus-data/src/reserve.rs
Original file line number Diff line number Diff line change
@@ -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,
}
Expand Down Expand Up @@ -70,3 +74,84 @@ impl From<ReserveDatum> for PlutusData {
.into()
}
}

impl TryFrom<PlutusData> for ReserveDatum {
type Error = DataDecodingError;

fn try_from(datum: PlutusData) -> DecodingResult<Self> {
Self::decode(&datum)
}
}

impl VersionedDatum for ReserveDatum {
const NAME: &str = "ReserveDatum";

fn decode_legacy(_: &PlutusData) -> Result<Self, String> {
Err("ReserveDatum supports only versioned format".into())
}
LGLO marked this conversation as resolved.
Show resolved Hide resolved

fn decode_versioned(version: u32, datum: &PlutusData, _: &PlutusData) -> Result<Self, String> {
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<ReserveDatum> {
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<TokenId> {
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()?),
})
}
}
Loading