From 1bc4bc976259253474bfbe920512df44437de310 Mon Sep 17 00:00:00 2001 From: Oleksandr Zahorodnyi Date: Thu, 7 May 2026 17:17:50 +0300 Subject: [PATCH 1/5] Add PoC pre_lock covenant version control --- .../contracts/src/programs/asset_auth/core.rs | 4 + .../src/programs/asset_auth_vault/core.rs | 8 ++ .../src/programs/issuance_factory/core.rs | 4 + crates/contracts/src/programs/lending/core.rs | 4 + .../src/programs/ownable_script_auth/core.rs | 4 + .../contracts/src/programs/pre_lock/core.rs | 97 ++++++++++++++----- crates/contracts/src/programs/pre_lock/mod.rs | 2 +- crates/contracts/src/programs/program.rs | 11 +++ .../src/programs/script_auth/core.rs | 4 + 9 files changed, 112 insertions(+), 26 deletions(-) diff --git a/crates/contracts/src/programs/asset_auth/core.rs b/crates/contracts/src/programs/asset_auth/core.rs index 47081a3..10e7250 100644 --- a/crates/contracts/src/programs/asset_auth/core.rs +++ b/crates/contracts/src/programs/asset_auth/core.rs @@ -54,4 +54,8 @@ impl SimplexProgram for AssetAuth { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + AssetAuthProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/asset_auth_vault/core.rs b/crates/contracts/src/programs/asset_auth_vault/core.rs index 6f9df34..f123d5c 100644 --- a/crates/contracts/src/programs/asset_auth_vault/core.rs +++ b/crates/contracts/src/programs/asset_auth_vault/core.rs @@ -186,6 +186,10 @@ impl SimplexProgram for ActiveAssetAuthVault { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + AssetAuthVaultProgram::SOURCE + } } impl SimplexProgram for FinalizedAssetAuthVault { @@ -196,4 +200,8 @@ impl SimplexProgram for FinalizedAssetAuthVault { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + AssetAuthVaultProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/issuance_factory/core.rs b/crates/contracts/src/programs/issuance_factory/core.rs index d51d1eb..1c8bf43 100644 --- a/crates/contracts/src/programs/issuance_factory/core.rs +++ b/crates/contracts/src/programs/issuance_factory/core.rs @@ -171,4 +171,8 @@ impl SimplexProgram for IssuanceFactory { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + IssuanceFactoryProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/lending/core.rs b/crates/contracts/src/programs/lending/core.rs index 34c554d..bd82a7d 100644 --- a/crates/contracts/src/programs/lending/core.rs +++ b/crates/contracts/src/programs/lending/core.rs @@ -249,4 +249,8 @@ impl SimplexProgram for Lending { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + LendingProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/ownable_script_auth/core.rs b/crates/contracts/src/programs/ownable_script_auth/core.rs index 8774638..1cc44d1 100644 --- a/crates/contracts/src/programs/ownable_script_auth/core.rs +++ b/crates/contracts/src/programs/ownable_script_auth/core.rs @@ -116,4 +116,8 @@ impl SimplexProgram for OwnableScriptAuth { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + OwnableScriptAuthProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/pre_lock/core.rs b/crates/contracts/src/programs/pre_lock/core.rs index 0411d36..1231ba9 100644 --- a/crates/contracts/src/programs/pre_lock/core.rs +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -20,7 +20,62 @@ pub struct PreLock { } pub const UTILITY_NFTS_COUNT: usize = 4; -pub const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 64; +const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 68; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PreLockCreationOpReturnData { + pub covenant_id: [u8; 4], + pub borrower_pubkey: XOnlyPublicKey, + pub principal_asset_id: AssetId, +} + +impl PreLockCreationOpReturnData { + pub fn new( + covenant_id: [u8; 4], + borrower_pubkey: XOnlyPublicKey, + principal_asset_id: AssetId, + ) -> Self { + Self { + covenant_id, + borrower_pubkey, + principal_asset_id, + } + } + + pub fn decode(op_return_bytes: &[u8]) -> Result { + if op_return_bytes.len() != PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH { + return Err(PreLockError::InvalidCreationOpReturnDataLength { + expected: PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH, + actual: op_return_bytes.len(), + }); + } + + let mut covenant_id = [0; 4]; + covenant_id.copy_from_slice(&op_return_bytes[0..4]); + let borrower_pubkey = &op_return_bytes[4..36]; + let principal_asset_id = &op_return_bytes[36..68]; + + Ok(Self { + covenant_id, + borrower_pubkey: Self::decode_borrower_pubkey(borrower_pubkey)?, + principal_asset_id: AssetId::from_slice(principal_asset_id)?, + }) + } + + pub fn encode(&self) -> Vec { + let mut op_return_data = Vec::with_capacity(PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH); + op_return_data.extend_from_slice(&self.covenant_id); + op_return_data.extend_from_slice(&self.borrower_pubkey.serialize()); + op_return_data.extend_from_slice(&self.principal_asset_id.into_inner().0); + + op_return_data + } + + fn decode_borrower_pubkey(op_return_pub_key: &[u8]) -> Result { + XOnlyPublicKey::from_slice(op_return_pub_key) + .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex())) + } +} impl PreLock { pub fn new(parameters: PreLockParameters) -> Self { @@ -90,18 +145,18 @@ impl PreLock { .push_bytes() .unwrap(); - let (borrower_pubkey, principal_asset_id) = - PreLock::decode_creation_op_return_data(op_return_bytes.to_vec()).unwrap(); + let creation_op_return_data = + PreLock::decode_creation_op_return_data(op_return_bytes.to_vec())?; let pre_lock_parameters = PreLockParameters { collateral_asset_id, - principal_asset_id, + principal_asset_id: creation_op_return_data.principal_asset_id, first_parameters_nft_asset_id, second_parameters_nft_asset_id, borrower_nft_asset_id, lender_nft_asset_id, offer_parameters, - borrower_pubkey, + borrower_pubkey: creation_op_return_data.borrower_pubkey, borrower_output_script_hash: collateral_script_hash, network: *provider.get_network(), }; @@ -115,29 +170,17 @@ impl PreLock { pub fn decode_creation_op_return_data( op_return_bytes: Vec, - ) -> Result<(XOnlyPublicKey, AssetId), PreLockError> { - if op_return_bytes.len() != PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH { - return Err(PreLockError::InvalidCreationOpReturnDataLength { - expected: PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH, - actual: op_return_bytes.len(), - }); - } - - let (op_return_pub_key, op_return_asset_id) = op_return_bytes.split_at(32); - - let principal_asset_id = AssetId::from_slice(op_return_asset_id)?; - let borrower_public_key = XOnlyPublicKey::from_slice(op_return_pub_key) - .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex()))?; - - Ok((borrower_public_key, principal_asset_id)) + ) -> Result { + PreLockCreationOpReturnData::decode(&op_return_bytes) } pub fn encode_creation_op_return_data(&self) -> Vec { - let mut op_return_data = Vec::with_capacity(PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH); - op_return_data.extend_from_slice(&self.parameters.borrower_pubkey.serialize()); - op_return_data.extend_from_slice(&self.parameters.principal_asset_id.into_inner().0); - - op_return_data + PreLockCreationOpReturnData::new( + self.get_program_source_code_hash(), + self.parameters.borrower_pubkey, + self.parameters.principal_asset_id, + ) + .encode() } pub fn attach_creation(&self, ft: &mut FinalTransaction, parameter_amounts_decimals: u8) { @@ -292,4 +335,8 @@ impl SimplexProgram for PreLock { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + PreLockProgram::SOURCE + } } diff --git a/crates/contracts/src/programs/pre_lock/mod.rs b/crates/contracts/src/programs/pre_lock/mod.rs index 5bbf0f4..853bdde 100644 --- a/crates/contracts/src/programs/pre_lock/mod.rs +++ b/crates/contracts/src/programs/pre_lock/mod.rs @@ -3,7 +3,7 @@ mod error; mod params; mod witness; -pub use core::{PreLock, UTILITY_NFTS_COUNT}; +pub use core::{PreLock, PreLockCreationOpReturnData, UTILITY_NFTS_COUNT}; pub use error::PreLockError; pub use params::PreLockParameters; pub use witness::PreLockWitnessBranch; diff --git a/crates/contracts/src/programs/program.rs b/crates/contracts/src/programs/program.rs index 1d92f57..5b3db93 100644 --- a/crates/contracts/src/programs/program.rs +++ b/crates/contracts/src/programs/program.rs @@ -1,3 +1,4 @@ +use ring::digest::{SHA256, digest}; use simplex::program::{Program, WitnessTrait}; use simplex::provider::SimplicityNetwork; use simplex::transaction::partial_input::IssuanceInput; @@ -94,7 +95,17 @@ pub trait SimplexProgram { self.get_program().get_script_hash(self.get_network()) } + fn get_program_source_code_hash(&self) -> [u8; 4] { + let source_code_hash = digest(&SHA256, self.get_program_source_code().as_bytes()); + let mut hash_prefix = [0; 4]; + hash_prefix.copy_from_slice(&source_code_hash.as_ref()[..4]); + + hash_prefix + } + fn get_program(&self) -> &Program; fn get_network(&self) -> &SimplicityNetwork; + + fn get_program_source_code(&self) -> &'static str; } diff --git a/crates/contracts/src/programs/script_auth/core.rs b/crates/contracts/src/programs/script_auth/core.rs index 1136495..7b60b43 100644 --- a/crates/contracts/src/programs/script_auth/core.rs +++ b/crates/contracts/src/programs/script_auth/core.rs @@ -63,4 +63,8 @@ impl SimplexProgram for ScriptAuth { fn get_network(&self) -> &SimplicityNetwork { &self.parameters.network } + + fn get_program_source_code(&self) -> &'static str { + ScriptAuthProgram::SOURCE + } } From f679bdb7669f479124eb08f0988a07883fd75ce6 Mon Sep 17 00:00:00 2001 From: Oleksandr Zahorodnyi Date: Thu, 7 May 2026 19:51:17 +0300 Subject: [PATCH 2/5] Add pre-lock metadata tests --- .../creation_metadata_success_flow.rs | 92 +++++++++++++++++++ crates/contracts/tests/pre_lock/mod.rs | 1 + 2 files changed, 93 insertions(+) create mode 100644 crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs diff --git a/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs new file mode 100644 index 0000000..0d1c7b4 --- /dev/null +++ b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs @@ -0,0 +1,92 @@ +use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::{Transaction, Txid}; + +use super::setup::setup_pre_lock; + +fn op_return_payload(tx: &Transaction) -> Vec { + let mut op_return_instr_iter = tx.output[5].script_pubkey.instructions_minimal(); + + op_return_instr_iter.next(); + + op_return_instr_iter + .next() + .unwrap() + .unwrap() + .push_bytes() + .unwrap() + .to_vec() +} + +fn setup_default_pre_lock( + context: &simplex::TestContext, +) -> anyhow::Result<(Txid, PreLock, PreLockParameters)> { + let provider = context.get_default_provider(); + let principal_asset_amount = 20000; + let current_height = provider.fetch_tip_height()?; + + let offer_parameters = LendingOfferParameters { + collateral_amount: 3000, + principal_amount: 10000, + loan_expiration_time: current_height + 60, + principal_interest_rate: 1000, + }; + + setup_pre_lock(context, offer_parameters, principal_asset_amount) +} + +#[simplex::test] +fn creates_pre_lock_with_covenant_metadata(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = setup_default_pre_lock(&context)?; + + let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; + let op_return_data = op_return_payload(&pre_lock_creation_tx); + + assert!(pre_lock_creation_tx.output[5].is_null_data()); + assert_eq!(op_return_data.len(), 68); + assert_eq!( + &op_return_data[0..4], + pre_lock.get_program_source_code_hash().as_slice() + ); + assert_eq!( + &op_return_data[4..36], + pre_lock_parameters.borrower_pubkey.serialize().as_slice() + ); + assert_eq!( + &op_return_data[36..68], + pre_lock_parameters + .principal_asset_id + .into_inner() + .0 + .as_slice() + ); + + Ok(()) +} + +#[simplex::test] +fn decodes_pre_lock_creation_metadata(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = setup_default_pre_lock(&context)?; + + let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; + let decoded_op_return_data = + PreLock::decode_creation_op_return_data(op_return_payload(&pre_lock_creation_tx))?; + + assert_eq!( + decoded_op_return_data.covenant_id, + pre_lock.get_program_source_code_hash() + ); + assert_eq!( + decoded_op_return_data.borrower_pubkey, + pre_lock_parameters.borrower_pubkey + ); + assert_eq!( + decoded_op_return_data.principal_asset_id, + pre_lock_parameters.principal_asset_id + ); + + Ok(()) +} diff --git a/crates/contracts/tests/pre_lock/mod.rs b/crates/contracts/tests/pre_lock/mod.rs index 1dc8ef2..d8f801c 100644 --- a/crates/contracts/tests/pre_lock/mod.rs +++ b/crates/contracts/tests/pre_lock/mod.rs @@ -2,5 +2,6 @@ mod common; mod cancellation_success_flow; +mod creation_metadata_success_flow; mod lending_creation_success_flow; mod setup; From da55051cd1ac2d8b3d6507dbd181b1ccae441557 Mon Sep 17 00:00:00 2001 From: Oleksandr Zahorodnyi Date: Thu, 14 May 2026 20:59:49 +0300 Subject: [PATCH 3/5] Refine issuance factory metadata and clean up code --- .../src/programs/issuance_factory/core.rs | 145 +++++++++++++----- .../src/programs/issuance_factory/mod.rs | 2 +- .../contracts/src/programs/pre_lock/core.rs | 70 +++++---- crates/contracts/src/programs/program.rs | 46 +++++- .../creation_metadata_success_flow.rs | 99 ++++++++++++ .../contracts/tests/issuance_factory/mod.rs | 1 + .../creation_metadata_success_flow.rs | 26 +--- 7 files changed, 297 insertions(+), 92 deletions(-) create mode 100644 crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs diff --git a/crates/contracts/src/programs/issuance_factory/core.rs b/crates/contracts/src/programs/issuance_factory/core.rs index 1c8bf43..3e9fa89 100644 --- a/crates/contracts/src/programs/issuance_factory/core.rs +++ b/crates/contracts/src/programs/issuance_factory/core.rs @@ -11,17 +11,101 @@ use crate::artifacts::issuance_factory::IssuanceFactoryProgram; use crate::programs::issuance_factory::{ IssuanceFactoryError, IssuanceFactoryParameters, IssuanceFactoryWitnessBranch, }; -use crate::programs::program::SimplexProgram; +use crate::programs::program::{ + CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, op_return_payload, +}; pub struct IssuanceFactory { program: IssuanceFactoryProgram, parameters: IssuanceFactoryParameters, } -// TODO: encode constants to the factory asset amount or creation OP_RETURN -pub const PRE_LOCK_ISSUING_UTXOS_COUNT: u8 = 2; -pub const PRE_LOCK_REISSUANCE_FLAGS: u64 = 0; -pub const ISSUANCE_FACTORY_CREATION_OP_RETURN_DATA_LENGTH: usize = 32; +const CREATION_OP_RETURN_OUTPUT_INDEX: usize = 1; +const OWNER_PUBKEY_LENGTH: usize = 32; +const CREATION_OP_RETURN_DATA_LENGTH: usize = PROGRAM_ID_LENGTH + + std::mem::size_of::() + + std::mem::size_of::() + + OWNER_PUBKEY_LENGTH; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IssuanceFactoryCreationOpReturnData { + pub program_id: ProgramId, + pub issuing_utxos_count: u8, + pub reissuance_flags: u64, + pub owner_pubkey: XOnlyPublicKey, +} + +impl IssuanceFactoryCreationOpReturnData { + pub fn new( + program_id: ProgramId, + issuing_utxos_count: u8, + reissuance_flags: u64, + owner_pubkey: XOnlyPublicKey, + ) -> Self { + Self { + program_id, + issuing_utxos_count, + reissuance_flags, + owner_pubkey, + } + } + + pub fn decode(op_return_bytes: &[u8]) -> Result { + ::decode(op_return_bytes) + } + + pub fn encode(&self) -> Vec { + ::encode(self) + } +} + +impl CreationOpReturnData for IssuanceFactoryCreationOpReturnData { + type Error = IssuanceFactoryError; + + const DATA_LENGTH: usize = CREATION_OP_RETURN_DATA_LENGTH; + + fn decode(op_return_bytes: &[u8]) -> Result { + Self::validate_length(op_return_bytes, |expected, actual| { + IssuanceFactoryError::InvalidCreationOpReturnDataLength { expected, actual } + })?; + + let mut cursor = 0; + + let program_id = Self::decode_program_id(op_return_bytes); + cursor += PROGRAM_ID_LENGTH; + + let issuing_utxos_count = op_return_bytes[cursor]; + cursor += std::mem::size_of::(); + + let reissuance_flags = u64::from_le_bytes( + op_return_bytes[cursor..cursor + std::mem::size_of::()] + .try_into() + .expect("reissuance flags length is fixed"), + ); + cursor += std::mem::size_of::(); + + let owner_pubkey_bytes = &op_return_bytes[cursor..]; + let owner_pubkey = XOnlyPublicKey::from_slice(owner_pubkey_bytes) + .map_err(|_| IssuanceFactoryError::InvalidOpReturnBytes(op_return_bytes.to_hex()))?; + + Ok(Self { + program_id, + issuing_utxos_count, + reissuance_flags, + owner_pubkey, + }) + } + + fn encode(&self) -> Vec { + let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); + op_return_data.extend_from_slice(&self.program_id); + op_return_data.push(self.issuing_utxos_count); + op_return_data.extend_from_slice(&self.reissuance_flags.to_le_bytes()); + op_return_data.extend_from_slice(&self.owner_pubkey.serialize()); + + op_return_data + } +} impl IssuanceFactory { pub fn new(parameters: IssuanceFactoryParameters) -> Self { @@ -35,30 +119,25 @@ impl IssuanceFactory { tx: &Transaction, provider: &impl ProviderTrait, ) -> Result { - if tx.output.len() < 2 || !tx.output[1].is_null_data() { + if tx.output.len() <= CREATION_OP_RETURN_OUTPUT_INDEX + || !tx.output[CREATION_OP_RETURN_OUTPUT_INDEX].is_null_data() + { return Err(IssuanceFactoryError::NotAnIssuanceFactoryCreationTx( tx.txid(), )); } - let mut op_return_instr_iter = tx.output[5].script_pubkey.instructions_minimal(); + let op_return_bytes = + op_return_payload(&tx.output[CREATION_OP_RETURN_OUTPUT_INDEX].script_pubkey) + .ok_or_else(|| IssuanceFactoryError::NotAnIssuanceFactoryCreationTx(tx.txid()))?; - op_return_instr_iter.next(); - - let op_return_bytes = op_return_instr_iter - .next() - .unwrap() - .unwrap() - .push_bytes() - .unwrap(); - - let owner_pubkey = + let creation_op_return_data = IssuanceFactory::decode_creation_op_return_data(op_return_bytes.to_vec())?; let issuance_factory_parameters = IssuanceFactoryParameters { - issuing_utxos_count: PRE_LOCK_ISSUING_UTXOS_COUNT, - reissuance_flags: PRE_LOCK_REISSUANCE_FLAGS, - owner_pubkey, + issuing_utxos_count: creation_op_return_data.issuing_utxos_count, + reissuance_flags: creation_op_return_data.reissuance_flags, + owner_pubkey: creation_op_return_data.owner_pubkey, network: *provider.get_network(), }; @@ -71,26 +150,18 @@ impl IssuanceFactory { pub fn decode_creation_op_return_data( op_return_bytes: Vec, - ) -> Result { - if op_return_bytes.len() != ISSUANCE_FACTORY_CREATION_OP_RETURN_DATA_LENGTH { - return Err(IssuanceFactoryError::InvalidCreationOpReturnDataLength { - expected: ISSUANCE_FACTORY_CREATION_OP_RETURN_DATA_LENGTH, - actual: op_return_bytes.len(), - }); - } - - let owner_pubkey = XOnlyPublicKey::from_slice(op_return_bytes.as_slice()) - .map_err(|_| IssuanceFactoryError::InvalidOpReturnBytes(op_return_bytes.to_hex()))?; - - Ok(owner_pubkey) + ) -> Result { + IssuanceFactoryCreationOpReturnData::decode(&op_return_bytes) } pub fn encode_creation_op_return_data(&self) -> Vec { - let mut op_return_data = - Vec::with_capacity(ISSUANCE_FACTORY_CREATION_OP_RETURN_DATA_LENGTH); - op_return_data.extend_from_slice(&self.parameters.owner_pubkey.serialize()); - - op_return_data + IssuanceFactoryCreationOpReturnData::new( + self.get_program_id(), + self.parameters.issuing_utxos_count, + self.parameters.reissuance_flags, + self.parameters.owner_pubkey, + ) + .encode() } pub fn attach_creation( diff --git a/crates/contracts/src/programs/issuance_factory/mod.rs b/crates/contracts/src/programs/issuance_factory/mod.rs index 91a8787..902132e 100644 --- a/crates/contracts/src/programs/issuance_factory/mod.rs +++ b/crates/contracts/src/programs/issuance_factory/mod.rs @@ -3,7 +3,7 @@ mod error; mod params; mod witness; -pub use core::IssuanceFactory; +pub use core::{IssuanceFactory, IssuanceFactoryCreationOpReturnData}; pub use error::IssuanceFactoryError; pub use params::IssuanceFactoryParameters; pub use witness::IssuanceFactoryWitnessBranch; diff --git a/crates/contracts/src/programs/pre_lock/core.rs b/crates/contracts/src/programs/pre_lock/core.rs index 1231ba9..64c63ec 100644 --- a/crates/contracts/src/programs/pre_lock/core.rs +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -10,7 +10,9 @@ use simplex::utils::hash_script; use crate::artifacts::pre_lock::PreLockProgram; use crate::programs::lending::Lending; use crate::programs::pre_lock::{PreLockError, PreLockParameters, PreLockWitnessBranch}; -use crate::programs::program::SimplexProgram; +use crate::programs::program::{ + CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, op_return_payload, +}; use crate::programs::script_auth::{ScriptAuth, ScriptAuthWitnessParams}; use crate::utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}; @@ -24,57 +26,67 @@ const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 68; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PreLockCreationOpReturnData { - pub covenant_id: [u8; 4], + pub program_id: ProgramId, pub borrower_pubkey: XOnlyPublicKey, pub principal_asset_id: AssetId, } impl PreLockCreationOpReturnData { pub fn new( - covenant_id: [u8; 4], + program_id: ProgramId, borrower_pubkey: XOnlyPublicKey, principal_asset_id: AssetId, ) -> Self { Self { - covenant_id, + program_id, borrower_pubkey, principal_asset_id, } } pub fn decode(op_return_bytes: &[u8]) -> Result { - if op_return_bytes.len() != PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH { - return Err(PreLockError::InvalidCreationOpReturnDataLength { - expected: PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH, - actual: op_return_bytes.len(), - }); - } + ::decode(op_return_bytes) + } + + pub fn encode(&self) -> Vec { + ::encode(self) + } + + fn decode_borrower_pubkey(op_return_pub_key: &[u8]) -> Result { + XOnlyPublicKey::from_slice(op_return_pub_key) + .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex())) + } +} - let mut covenant_id = [0; 4]; - covenant_id.copy_from_slice(&op_return_bytes[0..4]); - let borrower_pubkey = &op_return_bytes[4..36]; +impl CreationOpReturnData for PreLockCreationOpReturnData { + type Error = PreLockError; + + const DATA_LENGTH: usize = PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH; + + fn decode(op_return_bytes: &[u8]) -> Result { + Self::validate_length(op_return_bytes, |expected, actual| { + PreLockError::InvalidCreationOpReturnDataLength { expected, actual } + })?; + + let program_id = Self::decode_program_id(op_return_bytes); + let borrower_pubkey = &op_return_bytes[PROGRAM_ID_LENGTH..36]; let principal_asset_id = &op_return_bytes[36..68]; Ok(Self { - covenant_id, + program_id, borrower_pubkey: Self::decode_borrower_pubkey(borrower_pubkey)?, principal_asset_id: AssetId::from_slice(principal_asset_id)?, }) } - pub fn encode(&self) -> Vec { - let mut op_return_data = Vec::with_capacity(PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH); - op_return_data.extend_from_slice(&self.covenant_id); + fn encode(&self) -> Vec { + let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); + op_return_data.extend_from_slice(&self.program_id); op_return_data.extend_from_slice(&self.borrower_pubkey.serialize()); op_return_data.extend_from_slice(&self.principal_asset_id.into_inner().0); op_return_data } - - fn decode_borrower_pubkey(op_return_pub_key: &[u8]) -> Result { - XOnlyPublicKey::from_slice(op_return_pub_key) - .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex())) - } } impl PreLock { @@ -134,16 +146,8 @@ impl PreLock { &pre_collateral_tx.output[prev_collateral_outpoint.vout as usize].script_pubkey, ); - let mut op_return_instr_iter = tx.output[5].script_pubkey.instructions_minimal(); - - op_return_instr_iter.next(); - - let op_return_bytes = op_return_instr_iter - .next() - .unwrap() - .unwrap() - .push_bytes() - .unwrap(); + let op_return_bytes = op_return_payload(&tx.output[5].script_pubkey) + .ok_or_else(|| PreLockError::NotAPreLockCreationTx(tx.txid()))?; let creation_op_return_data = PreLock::decode_creation_op_return_data(op_return_bytes.to_vec())?; @@ -176,7 +180,7 @@ impl PreLock { pub fn encode_creation_op_return_data(&self) -> Vec { PreLockCreationOpReturnData::new( - self.get_program_source_code_hash(), + self.get_program_id(), self.parameters.borrower_pubkey, self.parameters.principal_asset_id, ) diff --git a/crates/contracts/src/programs/program.rs b/crates/contracts/src/programs/program.rs index 5b3db93..4fe9a82 100644 --- a/crates/contracts/src/programs/program.rs +++ b/crates/contracts/src/programs/program.rs @@ -7,7 +7,49 @@ use simplex::transaction::{ RequiredSignature, UTXO, }; -use simplex::simplicityhl::elements::{AssetId, Script}; +use simplex::simplicityhl::elements::{AssetId, Script, opcodes, script::Instruction}; + +pub const PROGRAM_ID_LENGTH: usize = 4; +pub type ProgramId = [u8; PROGRAM_ID_LENGTH]; + +pub fn op_return_payload(script: &Script) -> Option<&[u8]> { + let mut instructions = script.instructions_minimal(); + + match instructions.next()? { + Ok(Instruction::Op(opcodes::all::OP_RETURN)) => {} + _ => return None, + } + + instructions.next()?.ok()?.push_bytes() +} + +pub trait CreationOpReturnData: Sized { + type Error; + + const DATA_LENGTH: usize; + + fn decode(op_return_bytes: &[u8]) -> Result; + + fn encode(&self) -> Vec; + + fn validate_length( + op_return_bytes: &[u8], + invalid_length: impl FnOnce(usize, usize) -> Self::Error, + ) -> Result<(), Self::Error> { + if op_return_bytes.len() != Self::DATA_LENGTH { + return Err(invalid_length(Self::DATA_LENGTH, op_return_bytes.len())); + } + + Ok(()) + } + + fn decode_program_id(op_return_bytes: &[u8]) -> ProgramId { + let mut program_id = [0; PROGRAM_ID_LENGTH]; + program_id.copy_from_slice(&op_return_bytes[..PROGRAM_ID_LENGTH]); + + program_id + } +} pub trait SimplexProgram { fn add_program_input<'a>( @@ -95,7 +137,7 @@ pub trait SimplexProgram { self.get_program().get_script_hash(self.get_network()) } - fn get_program_source_code_hash(&self) -> [u8; 4] { + fn get_program_id(&self) -> ProgramId { let source_code_hash = digest(&SHA256, self.get_program_source_code().as_bytes()); let mut hash_prefix = [0; 4]; hash_prefix.copy_from_slice(&source_code_hash.as_ref()[..4]); diff --git a/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs b/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs new file mode 100644 index 0000000..d50652c --- /dev/null +++ b/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs @@ -0,0 +1,99 @@ +use lending_contracts::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryParameters}; +use lending_contracts::programs::program::{ + SimplexProgram, op_return_payload as script_op_return_payload, +}; +use simplex::simplicityhl::elements::{Transaction, Txid}; + +use super::setup::setup_issuance_factory; + +fn op_return_payload(tx: &Transaction) -> Vec { + script_op_return_payload(&tx.output[1].script_pubkey) + .unwrap() + .to_vec() +} + +fn setup_default_issuance_factory( + context: &simplex::TestContext, +) -> anyhow::Result<(Txid, IssuanceFactory, IssuanceFactoryParameters)> { + let provider = context.get_default_provider(); + let issuing_utxos_count = 3; + let reissuance_flags = 0x0102_0304_0506_0708; + let (issuance_factory, issuance_factory_parameters) = + setup_issuance_factory(context, issuing_utxos_count, reissuance_flags)?; + + let issuance_factory_utxo = + provider.fetch_scripthash_utxos(&issuance_factory.get_script_pubkey())?[0].clone(); + + Ok(( + issuance_factory_utxo.outpoint.txid, + issuance_factory, + issuance_factory_parameters, + )) +} + +#[simplex::test] +fn creates_issuance_factory_with_creation_metadata( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let (issuance_factory_creation_txid, issuance_factory, issuance_factory_parameters) = + setup_default_issuance_factory(&context)?; + + let issuance_factory_creation_tx = + provider.fetch_transaction(&issuance_factory_creation_txid)?; + let op_return_data = op_return_payload(&issuance_factory_creation_tx); + let expected_reissuance_flags = issuance_factory_parameters.reissuance_flags.to_le_bytes(); + + assert!(issuance_factory_creation_tx.output[1].is_null_data()); + assert_eq!(op_return_data.len(), 45); + assert_eq!( + &op_return_data[0..4], + issuance_factory.get_program_id().as_slice() + ); + assert_eq!( + op_return_data[4], + issuance_factory_parameters.issuing_utxos_count + ); + assert_eq!(&op_return_data[5..13], expected_reissuance_flags.as_slice()); + assert_eq!( + &op_return_data[13..45], + issuance_factory_parameters + .owner_pubkey + .serialize() + .as_slice() + ); + + Ok(()) +} + +#[simplex::test] +fn decodes_issuance_factory_creation_metadata(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let (issuance_factory_creation_txid, issuance_factory, issuance_factory_parameters) = + setup_default_issuance_factory(&context)?; + + let issuance_factory_creation_tx = + provider.fetch_transaction(&issuance_factory_creation_txid)?; + let decoded_op_return_data = IssuanceFactory::decode_creation_op_return_data( + op_return_payload(&issuance_factory_creation_tx), + )?; + + assert_eq!( + decoded_op_return_data.program_id, + issuance_factory.get_program_id() + ); + assert_eq!( + decoded_op_return_data.issuing_utxos_count, + issuance_factory_parameters.issuing_utxos_count + ); + assert_eq!( + decoded_op_return_data.reissuance_flags, + issuance_factory_parameters.reissuance_flags + ); + assert_eq!( + decoded_op_return_data.owner_pubkey, + issuance_factory_parameters.owner_pubkey + ); + + Ok(()) +} diff --git a/crates/contracts/tests/issuance_factory/mod.rs b/crates/contracts/tests/issuance_factory/mod.rs index d8828d9..ec3cb8a 100644 --- a/crates/contracts/tests/issuance_factory/mod.rs +++ b/crates/contracts/tests/issuance_factory/mod.rs @@ -1,6 +1,7 @@ #[path = "../common/mod.rs"] mod common; +mod creation_metadata_success_flow; mod issue_assets_failure_flows; mod issue_assets_success_flows; mod remove_factory_failure_flows; diff --git a/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs index 0d1c7b4..b1730d7 100644 --- a/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs +++ b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs @@ -1,20 +1,14 @@ use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; -use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::programs::program::{ + SimplexProgram, op_return_payload as script_op_return_payload, +}; use lending_contracts::utils::LendingOfferParameters; use simplex::simplicityhl::elements::{Transaction, Txid}; use super::setup::setup_pre_lock; fn op_return_payload(tx: &Transaction) -> Vec { - let mut op_return_instr_iter = tx.output[5].script_pubkey.instructions_minimal(); - - op_return_instr_iter.next(); - - op_return_instr_iter - .next() - .unwrap() - .unwrap() - .push_bytes() + script_op_return_payload(&tx.output[5].script_pubkey) .unwrap() .to_vec() } @@ -37,7 +31,7 @@ fn setup_default_pre_lock( } #[simplex::test] -fn creates_pre_lock_with_covenant_metadata(context: simplex::TestContext) -> anyhow::Result<()> { +fn creates_pre_lock_with_creation_metadata(context: simplex::TestContext) -> anyhow::Result<()> { let provider = context.get_default_provider(); let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = setup_default_pre_lock(&context)?; @@ -46,10 +40,7 @@ fn creates_pre_lock_with_covenant_metadata(context: simplex::TestContext) -> any assert!(pre_lock_creation_tx.output[5].is_null_data()); assert_eq!(op_return_data.len(), 68); - assert_eq!( - &op_return_data[0..4], - pre_lock.get_program_source_code_hash().as_slice() - ); + assert_eq!(&op_return_data[0..4], pre_lock.get_program_id().as_slice()); assert_eq!( &op_return_data[4..36], pre_lock_parameters.borrower_pubkey.serialize().as_slice() @@ -75,10 +66,7 @@ fn decodes_pre_lock_creation_metadata(context: simplex::TestContext) -> anyhow:: let decoded_op_return_data = PreLock::decode_creation_op_return_data(op_return_payload(&pre_lock_creation_tx))?; - assert_eq!( - decoded_op_return_data.covenant_id, - pre_lock.get_program_source_code_hash() - ); + assert_eq!(decoded_op_return_data.program_id, pre_lock.get_program_id()); assert_eq!( decoded_op_return_data.borrower_pubkey, pre_lock_parameters.borrower_pubkey From 1a93e2f30e0d7ec7dd08a6f4ea2c7c47ab36a17c Mon Sep 17 00:00:00 2001 From: Oleksandr Zahorodnyi Date: Fri, 15 May 2026 21:45:06 +0300 Subject: [PATCH 4/5] Move metadata logic to separate files --- .../src/programs/issuance_factory/core.rs | 111 +----------------- .../src/programs/issuance_factory/metadata.rs | 104 ++++++++++++++++ .../src/programs/issuance_factory/mod.rs | 5 +- .../contracts/src/programs/pre_lock/core.rs | 89 +------------- .../src/programs/pre_lock/metadata.rs | 82 +++++++++++++ crates/contracts/src/programs/pre_lock/mod.rs | 4 +- 6 files changed, 198 insertions(+), 197 deletions(-) create mode 100644 crates/contracts/src/programs/issuance_factory/metadata.rs create mode 100644 crates/contracts/src/programs/pre_lock/metadata.rs diff --git a/crates/contracts/src/programs/issuance_factory/core.rs b/crates/contracts/src/programs/issuance_factory/core.rs index 3e9fa89..b9e4763 100644 --- a/crates/contracts/src/programs/issuance_factory/core.rs +++ b/crates/contracts/src/programs/issuance_factory/core.rs @@ -1,6 +1,5 @@ use simplex::provider::ProviderTrait; use simplex::simplicityhl::elements::{AssetId, Script, Transaction}; -use simplex::simplicityhl::elements::{hex::ToHex, schnorr::XOnlyPublicKey}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ FinalTransaction, IssuanceDetails, PartialOutput, RequiredSignature, UTXO, @@ -9,104 +8,16 @@ use simplex::{program::Program, provider::SimplicityNetwork}; use crate::artifacts::issuance_factory::IssuanceFactoryProgram; use crate::programs::issuance_factory::{ - IssuanceFactoryError, IssuanceFactoryParameters, IssuanceFactoryWitnessBranch, -}; -use crate::programs::program::{ - CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, op_return_payload, + CREATION_OP_RETURN_OUTPUT_INDEX, IssuanceFactoryError, IssuanceFactoryParameters, + IssuanceFactoryWitnessBranch, }; +use crate::programs::program::{SimplexProgram, op_return_payload}; pub struct IssuanceFactory { program: IssuanceFactoryProgram, parameters: IssuanceFactoryParameters, } -const CREATION_OP_RETURN_OUTPUT_INDEX: usize = 1; -const OWNER_PUBKEY_LENGTH: usize = 32; -const CREATION_OP_RETURN_DATA_LENGTH: usize = PROGRAM_ID_LENGTH - + std::mem::size_of::() - + std::mem::size_of::() - + OWNER_PUBKEY_LENGTH; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IssuanceFactoryCreationOpReturnData { - pub program_id: ProgramId, - pub issuing_utxos_count: u8, - pub reissuance_flags: u64, - pub owner_pubkey: XOnlyPublicKey, -} - -impl IssuanceFactoryCreationOpReturnData { - pub fn new( - program_id: ProgramId, - issuing_utxos_count: u8, - reissuance_flags: u64, - owner_pubkey: XOnlyPublicKey, - ) -> Self { - Self { - program_id, - issuing_utxos_count, - reissuance_flags, - owner_pubkey, - } - } - - pub fn decode(op_return_bytes: &[u8]) -> Result { - ::decode(op_return_bytes) - } - - pub fn encode(&self) -> Vec { - ::encode(self) - } -} - -impl CreationOpReturnData for IssuanceFactoryCreationOpReturnData { - type Error = IssuanceFactoryError; - - const DATA_LENGTH: usize = CREATION_OP_RETURN_DATA_LENGTH; - - fn decode(op_return_bytes: &[u8]) -> Result { - Self::validate_length(op_return_bytes, |expected, actual| { - IssuanceFactoryError::InvalidCreationOpReturnDataLength { expected, actual } - })?; - - let mut cursor = 0; - - let program_id = Self::decode_program_id(op_return_bytes); - cursor += PROGRAM_ID_LENGTH; - - let issuing_utxos_count = op_return_bytes[cursor]; - cursor += std::mem::size_of::(); - - let reissuance_flags = u64::from_le_bytes( - op_return_bytes[cursor..cursor + std::mem::size_of::()] - .try_into() - .expect("reissuance flags length is fixed"), - ); - cursor += std::mem::size_of::(); - - let owner_pubkey_bytes = &op_return_bytes[cursor..]; - let owner_pubkey = XOnlyPublicKey::from_slice(owner_pubkey_bytes) - .map_err(|_| IssuanceFactoryError::InvalidOpReturnBytes(op_return_bytes.to_hex()))?; - - Ok(Self { - program_id, - issuing_utxos_count, - reissuance_flags, - owner_pubkey, - }) - } - - fn encode(&self) -> Vec { - let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); - op_return_data.extend_from_slice(&self.program_id); - op_return_data.push(self.issuing_utxos_count); - op_return_data.extend_from_slice(&self.reissuance_flags.to_le_bytes()); - op_return_data.extend_from_slice(&self.owner_pubkey.serialize()); - - op_return_data - } -} - impl IssuanceFactory { pub fn new(parameters: IssuanceFactoryParameters) -> Self { Self { @@ -148,22 +59,6 @@ impl IssuanceFactory { &self.parameters } - pub fn decode_creation_op_return_data( - op_return_bytes: Vec, - ) -> Result { - IssuanceFactoryCreationOpReturnData::decode(&op_return_bytes) - } - - pub fn encode_creation_op_return_data(&self) -> Vec { - IssuanceFactoryCreationOpReturnData::new( - self.get_program_id(), - self.parameters.issuing_utxos_count, - self.parameters.reissuance_flags, - self.parameters.owner_pubkey, - ) - .encode() - } - pub fn attach_creation( &self, ft: &mut FinalTransaction, diff --git a/crates/contracts/src/programs/issuance_factory/metadata.rs b/crates/contracts/src/programs/issuance_factory/metadata.rs new file mode 100644 index 0000000..d2d2fed --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/metadata.rs @@ -0,0 +1,104 @@ +use simplex::simplicityhl::elements::{hex::ToHex, schnorr::XOnlyPublicKey}; + +use crate::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryError}; +use crate::programs::program::{ + CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, +}; + +pub(crate) const CREATION_OP_RETURN_OUTPUT_INDEX: usize = 1; + +const OWNER_PUBKEY_LENGTH: usize = 32; +const CREATION_OP_RETURN_DATA_LENGTH: usize = PROGRAM_ID_LENGTH + + std::mem::size_of::() + + std::mem::size_of::() + + OWNER_PUBKEY_LENGTH; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IssuanceFactoryCreationOpReturnData { + pub program_id: ProgramId, + pub issuing_utxos_count: u8, + pub reissuance_flags: u64, + pub owner_pubkey: XOnlyPublicKey, +} + +impl IssuanceFactoryCreationOpReturnData { + pub fn new( + program_id: ProgramId, + issuing_utxos_count: u8, + reissuance_flags: u64, + owner_pubkey: XOnlyPublicKey, + ) -> Self { + Self { + program_id, + issuing_utxos_count, + reissuance_flags, + owner_pubkey, + } + } +} + +impl CreationOpReturnData for IssuanceFactoryCreationOpReturnData { + type Error = IssuanceFactoryError; + + const DATA_LENGTH: usize = CREATION_OP_RETURN_DATA_LENGTH; + + fn decode(op_return_bytes: &[u8]) -> Result { + Self::validate_length(op_return_bytes, |expected, actual| { + IssuanceFactoryError::InvalidCreationOpReturnDataLength { expected, actual } + })?; + + let mut cursor = 0; + + let program_id = Self::decode_program_id(op_return_bytes); + cursor += PROGRAM_ID_LENGTH; + + let issuing_utxos_count = op_return_bytes[cursor]; + cursor += std::mem::size_of::(); + + let reissuance_flags = u64::from_le_bytes( + op_return_bytes[cursor..cursor + std::mem::size_of::()] + .try_into() + .expect("reissuance flags length is fixed"), + ); + cursor += std::mem::size_of::(); + + let owner_pubkey_bytes = &op_return_bytes[cursor..]; + let owner_pubkey = XOnlyPublicKey::from_slice(owner_pubkey_bytes) + .map_err(|_| IssuanceFactoryError::InvalidOpReturnBytes(op_return_bytes.to_hex()))?; + + Ok(Self { + program_id, + issuing_utxos_count, + reissuance_flags, + owner_pubkey, + }) + } + + fn encode(&self) -> Vec { + let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); + op_return_data.extend_from_slice(&self.program_id); + op_return_data.push(self.issuing_utxos_count); + op_return_data.extend_from_slice(&self.reissuance_flags.to_le_bytes()); + op_return_data.extend_from_slice(&self.owner_pubkey.serialize()); + + op_return_data + } +} + +impl IssuanceFactory { + pub fn decode_creation_op_return_data( + op_return_bytes: Vec, + ) -> Result { + IssuanceFactoryCreationOpReturnData::decode(&op_return_bytes) + } + + pub fn encode_creation_op_return_data(&self) -> Vec { + IssuanceFactoryCreationOpReturnData::new( + self.get_program_id(), + self.get_parameters().issuing_utxos_count, + self.get_parameters().reissuance_flags, + self.get_parameters().owner_pubkey, + ) + .encode() + } +} diff --git a/crates/contracts/src/programs/issuance_factory/mod.rs b/crates/contracts/src/programs/issuance_factory/mod.rs index 902132e..0fe763b 100644 --- a/crates/contracts/src/programs/issuance_factory/mod.rs +++ b/crates/contracts/src/programs/issuance_factory/mod.rs @@ -1,9 +1,12 @@ mod core; mod error; +mod metadata; mod params; mod witness; -pub use core::{IssuanceFactory, IssuanceFactoryCreationOpReturnData}; +pub use core::IssuanceFactory; pub use error::IssuanceFactoryError; +pub(crate) use metadata::CREATION_OP_RETURN_OUTPUT_INDEX; +pub use metadata::IssuanceFactoryCreationOpReturnData; pub use params::IssuanceFactoryParameters; pub use witness::IssuanceFactoryWitnessBranch; diff --git a/crates/contracts/src/programs/pre_lock/core.rs b/crates/contracts/src/programs/pre_lock/core.rs index 64c63ec..7dd5cbc 100644 --- a/crates/contracts/src/programs/pre_lock/core.rs +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -1,18 +1,14 @@ use simplex::program::Program; use simplex::provider::{ProviderTrait, SimplicityNetwork}; -use simplex::simplicityhl::elements::{ - AssetId, Script, Transaction, hex::ToHex, secp256k1_zkp::XOnlyPublicKey, -}; +use simplex::simplicityhl::elements::{AssetId, Script, Transaction}; use simplex::transaction::{FinalTransaction, PartialOutput, RequiredSignature, UTXO}; use simplex::utils::hash_script; use crate::artifacts::pre_lock::PreLockProgram; use crate::programs::lending::Lending; use crate::programs::pre_lock::{PreLockError, PreLockParameters, PreLockWitnessBranch}; -use crate::programs::program::{ - CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, op_return_payload, -}; +use crate::programs::program::{SimplexProgram, op_return_payload}; use crate::programs::script_auth::{ScriptAuth, ScriptAuthWitnessParams}; use crate::utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}; @@ -22,72 +18,6 @@ pub struct PreLock { } pub const UTILITY_NFTS_COUNT: usize = 4; -const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 68; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PreLockCreationOpReturnData { - pub program_id: ProgramId, - pub borrower_pubkey: XOnlyPublicKey, - pub principal_asset_id: AssetId, -} - -impl PreLockCreationOpReturnData { - pub fn new( - program_id: ProgramId, - borrower_pubkey: XOnlyPublicKey, - principal_asset_id: AssetId, - ) -> Self { - Self { - program_id, - borrower_pubkey, - principal_asset_id, - } - } - - pub fn decode(op_return_bytes: &[u8]) -> Result { - ::decode(op_return_bytes) - } - - pub fn encode(&self) -> Vec { - ::encode(self) - } - - fn decode_borrower_pubkey(op_return_pub_key: &[u8]) -> Result { - XOnlyPublicKey::from_slice(op_return_pub_key) - .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex())) - } -} - -impl CreationOpReturnData for PreLockCreationOpReturnData { - type Error = PreLockError; - - const DATA_LENGTH: usize = PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH; - - fn decode(op_return_bytes: &[u8]) -> Result { - Self::validate_length(op_return_bytes, |expected, actual| { - PreLockError::InvalidCreationOpReturnDataLength { expected, actual } - })?; - - let program_id = Self::decode_program_id(op_return_bytes); - let borrower_pubkey = &op_return_bytes[PROGRAM_ID_LENGTH..36]; - let principal_asset_id = &op_return_bytes[36..68]; - - Ok(Self { - program_id, - borrower_pubkey: Self::decode_borrower_pubkey(borrower_pubkey)?, - principal_asset_id: AssetId::from_slice(principal_asset_id)?, - }) - } - - fn encode(&self) -> Vec { - let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); - op_return_data.extend_from_slice(&self.program_id); - op_return_data.extend_from_slice(&self.borrower_pubkey.serialize()); - op_return_data.extend_from_slice(&self.principal_asset_id.into_inner().0); - - op_return_data - } -} impl PreLock { pub fn new(parameters: PreLockParameters) -> Self { @@ -172,21 +102,6 @@ impl PreLock { &self.parameters } - pub fn decode_creation_op_return_data( - op_return_bytes: Vec, - ) -> Result { - PreLockCreationOpReturnData::decode(&op_return_bytes) - } - - pub fn encode_creation_op_return_data(&self) -> Vec { - PreLockCreationOpReturnData::new( - self.get_program_id(), - self.parameters.borrower_pubkey, - self.parameters.principal_asset_id, - ) - .encode() - } - pub fn attach_creation(&self, ft: &mut FinalTransaction, parameter_amounts_decimals: u8) { let (first_parameters_nft_amount, second_parameters_nft_amount) = self .parameters diff --git a/crates/contracts/src/programs/pre_lock/metadata.rs b/crates/contracts/src/programs/pre_lock/metadata.rs new file mode 100644 index 0000000..08de151 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/metadata.rs @@ -0,0 +1,82 @@ +use simplex::simplicityhl::elements::{AssetId, hex::ToHex, secp256k1_zkp::XOnlyPublicKey}; + +use crate::programs::pre_lock::{PreLock, PreLockError}; +use crate::programs::program::{ + CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, +}; + +const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 68; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PreLockCreationOpReturnData { + pub program_id: ProgramId, + pub borrower_pubkey: XOnlyPublicKey, + pub principal_asset_id: AssetId, +} + +impl PreLockCreationOpReturnData { + pub fn new( + program_id: ProgramId, + borrower_pubkey: XOnlyPublicKey, + principal_asset_id: AssetId, + ) -> Self { + Self { + program_id, + borrower_pubkey, + principal_asset_id, + } + } + + fn decode_borrower_pubkey(op_return_pub_key: &[u8]) -> Result { + XOnlyPublicKey::from_slice(op_return_pub_key) + .map_err(|_| PreLockError::InvalidOpReturnBytes(op_return_pub_key.to_hex())) + } +} + +impl CreationOpReturnData for PreLockCreationOpReturnData { + type Error = PreLockError; + + const DATA_LENGTH: usize = PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH; + + fn decode(op_return_bytes: &[u8]) -> Result { + Self::validate_length(op_return_bytes, |expected, actual| { + PreLockError::InvalidCreationOpReturnDataLength { expected, actual } + })?; + + let program_id = Self::decode_program_id(op_return_bytes); + let borrower_pubkey = &op_return_bytes[PROGRAM_ID_LENGTH..36]; + let principal_asset_id = &op_return_bytes[36..68]; + + Ok(Self { + program_id, + borrower_pubkey: Self::decode_borrower_pubkey(borrower_pubkey)?, + principal_asset_id: AssetId::from_slice(principal_asset_id)?, + }) + } + + fn encode(&self) -> Vec { + let mut op_return_data = Vec::with_capacity(Self::DATA_LENGTH); + op_return_data.extend_from_slice(&self.program_id); + op_return_data.extend_from_slice(&self.borrower_pubkey.serialize()); + op_return_data.extend_from_slice(&self.principal_asset_id.into_inner().0); + + op_return_data + } +} + +impl PreLock { + pub fn decode_creation_op_return_data( + op_return_bytes: Vec, + ) -> Result { + PreLockCreationOpReturnData::decode(&op_return_bytes) + } + + pub fn encode_creation_op_return_data(&self) -> Vec { + PreLockCreationOpReturnData::new( + self.get_program_id(), + self.get_parameters().borrower_pubkey, + self.get_parameters().principal_asset_id, + ) + .encode() + } +} diff --git a/crates/contracts/src/programs/pre_lock/mod.rs b/crates/contracts/src/programs/pre_lock/mod.rs index 853bdde..1e8ea35 100644 --- a/crates/contracts/src/programs/pre_lock/mod.rs +++ b/crates/contracts/src/programs/pre_lock/mod.rs @@ -1,9 +1,11 @@ mod core; mod error; +mod metadata; mod params; mod witness; -pub use core::{PreLock, PreLockCreationOpReturnData, UTILITY_NFTS_COUNT}; +pub use core::{PreLock, UTILITY_NFTS_COUNT}; pub use error::PreLockError; +pub use metadata::PreLockCreationOpReturnData; pub use params::PreLockParameters; pub use witness::PreLockWitnessBranch; From c565faae3bfe5172c9496af50f9e7c14cd583ee3 Mon Sep 17 00:00:00 2001 From: Oleksandr Zahorodnyi Date: Tue, 19 May 2026 09:43:13 +0300 Subject: [PATCH 5/5] Add MetadataProgram trait & some fixes --- .../src/programs/issuance_factory/core.rs | 12 ++-- .../src/programs/issuance_factory/metadata.rs | 15 ++--- .../src/programs/issuance_factory/mod.rs | 1 - .../contracts/src/programs/pre_lock/core.rs | 8 +-- .../src/programs/pre_lock/metadata.rs | 13 ++-- crates/contracts/src/programs/program.rs | 29 +++++---- crates/contracts/src/utils/mod.rs | 2 + crates/contracts/src/utils/op_return.rs | 63 +++++++++++++++++++ .../creation_metadata_success_flow.rs | 11 ++-- .../creation_metadata_success_flow.rs | 7 +-- 10 files changed, 109 insertions(+), 52 deletions(-) create mode 100644 crates/contracts/src/utils/op_return.rs diff --git a/crates/contracts/src/programs/issuance_factory/core.rs b/crates/contracts/src/programs/issuance_factory/core.rs index b9e4763..1eca928 100644 --- a/crates/contracts/src/programs/issuance_factory/core.rs +++ b/crates/contracts/src/programs/issuance_factory/core.rs @@ -8,10 +8,12 @@ use simplex::{program::Program, provider::SimplicityNetwork}; use crate::artifacts::issuance_factory::IssuanceFactoryProgram; use crate::programs::issuance_factory::{ - CREATION_OP_RETURN_OUTPUT_INDEX, IssuanceFactoryError, IssuanceFactoryParameters, - IssuanceFactoryWitnessBranch, + IssuanceFactoryError, IssuanceFactoryParameters, IssuanceFactoryWitnessBranch, }; -use crate::programs::program::{SimplexProgram, op_return_payload}; + +const CREATION_OP_RETURN_OUTPUT_INDEX: usize = 1; +use crate::programs::program::{MetadataProgram, SimplexProgram}; +use crate::utils::op_return_payload; pub struct IssuanceFactory { program: IssuanceFactoryProgram, @@ -43,7 +45,7 @@ impl IssuanceFactory { .ok_or_else(|| IssuanceFactoryError::NotAnIssuanceFactoryCreationTx(tx.txid()))?; let creation_op_return_data = - IssuanceFactory::decode_creation_op_return_data(op_return_bytes.to_vec())?; + IssuanceFactory::decode_metadata_op_return(op_return_bytes.to_vec())?; let issuance_factory_parameters = IssuanceFactoryParameters { issuing_utxos_count: creation_op_return_data.issuing_utxos_count, @@ -67,7 +69,7 @@ impl IssuanceFactory { ) { self.add_program_output(ft, factory_asset_id, factory_asset_amount); - let op_return_data = self.encode_creation_op_return_data(); + let op_return_data = self.encode_metadata_op_return(); ft.add_output(PartialOutput::new( Script::new_op_return(&op_return_data), diff --git a/crates/contracts/src/programs/issuance_factory/metadata.rs b/crates/contracts/src/programs/issuance_factory/metadata.rs index d2d2fed..d698d18 100644 --- a/crates/contracts/src/programs/issuance_factory/metadata.rs +++ b/crates/contracts/src/programs/issuance_factory/metadata.rs @@ -2,11 +2,9 @@ use simplex::simplicityhl::elements::{hex::ToHex, schnorr::XOnlyPublicKey}; use crate::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryError}; use crate::programs::program::{ - CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, + CreationOpReturnData, MetadataProgram, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, }; -pub(crate) const CREATION_OP_RETURN_OUTPUT_INDEX: usize = 1; - const OWNER_PUBKEY_LENGTH: usize = 32; const CREATION_OP_RETURN_DATA_LENGTH: usize = PROGRAM_ID_LENGTH + std::mem::size_of::() @@ -85,20 +83,15 @@ impl CreationOpReturnData for IssuanceFactoryCreationOpReturnData { } } -impl IssuanceFactory { - pub fn decode_creation_op_return_data( - op_return_bytes: Vec, - ) -> Result { - IssuanceFactoryCreationOpReturnData::decode(&op_return_bytes) - } +impl MetadataProgram for IssuanceFactory { + type Metadata = IssuanceFactoryCreationOpReturnData; - pub fn encode_creation_op_return_data(&self) -> Vec { + fn build_metadata(&self) -> Self::Metadata { IssuanceFactoryCreationOpReturnData::new( self.get_program_id(), self.get_parameters().issuing_utxos_count, self.get_parameters().reissuance_flags, self.get_parameters().owner_pubkey, ) - .encode() } } diff --git a/crates/contracts/src/programs/issuance_factory/mod.rs b/crates/contracts/src/programs/issuance_factory/mod.rs index 0fe763b..e6392ae 100644 --- a/crates/contracts/src/programs/issuance_factory/mod.rs +++ b/crates/contracts/src/programs/issuance_factory/mod.rs @@ -6,7 +6,6 @@ mod witness; pub use core::IssuanceFactory; pub use error::IssuanceFactoryError; -pub(crate) use metadata::CREATION_OP_RETURN_OUTPUT_INDEX; pub use metadata::IssuanceFactoryCreationOpReturnData; pub use params::IssuanceFactoryParameters; pub use witness::IssuanceFactoryWitnessBranch; diff --git a/crates/contracts/src/programs/pre_lock/core.rs b/crates/contracts/src/programs/pre_lock/core.rs index 7dd5cbc..2831542 100644 --- a/crates/contracts/src/programs/pre_lock/core.rs +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -8,8 +8,9 @@ use simplex::utils::hash_script; use crate::artifacts::pre_lock::PreLockProgram; use crate::programs::lending::Lending; use crate::programs::pre_lock::{PreLockError, PreLockParameters, PreLockWitnessBranch}; -use crate::programs::program::{SimplexProgram, op_return_payload}; +use crate::programs::program::{MetadataProgram, SimplexProgram}; use crate::programs::script_auth::{ScriptAuth, ScriptAuthWitnessParams}; +use crate::utils::op_return_payload; use crate::utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}; pub struct PreLock { @@ -79,8 +80,7 @@ impl PreLock { let op_return_bytes = op_return_payload(&tx.output[5].script_pubkey) .ok_or_else(|| PreLockError::NotAPreLockCreationTx(tx.txid()))?; - let creation_op_return_data = - PreLock::decode_creation_op_return_data(op_return_bytes.to_vec())?; + let creation_op_return_data = PreLock::decode_metadata_op_return(op_return_bytes.to_vec())?; let pre_lock_parameters = PreLockParameters { collateral_asset_id, @@ -129,7 +129,7 @@ impl PreLock { utility_nfts_script_auth.attach_creation(ft, self.parameters.borrower_nft_asset_id, 1); utility_nfts_script_auth.attach_creation(ft, self.parameters.lender_nft_asset_id, 1); - let op_return_data = self.encode_creation_op_return_data(); + let op_return_data = self.encode_metadata_op_return(); ft.add_output(PartialOutput::new( Script::new_op_return(&op_return_data), diff --git a/crates/contracts/src/programs/pre_lock/metadata.rs b/crates/contracts/src/programs/pre_lock/metadata.rs index 08de151..178aaf5 100644 --- a/crates/contracts/src/programs/pre_lock/metadata.rs +++ b/crates/contracts/src/programs/pre_lock/metadata.rs @@ -2,7 +2,7 @@ use simplex::simplicityhl::elements::{AssetId, hex::ToHex, secp256k1_zkp::XOnlyP use crate::programs::pre_lock::{PreLock, PreLockError}; use crate::programs::program::{ - CreationOpReturnData, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, + CreationOpReturnData, MetadataProgram, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, }; const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 68; @@ -64,19 +64,14 @@ impl CreationOpReturnData for PreLockCreationOpReturnData { } } -impl PreLock { - pub fn decode_creation_op_return_data( - op_return_bytes: Vec, - ) -> Result { - PreLockCreationOpReturnData::decode(&op_return_bytes) - } +impl MetadataProgram for PreLock { + type Metadata = PreLockCreationOpReturnData; - pub fn encode_creation_op_return_data(&self) -> Vec { + fn build_metadata(&self) -> Self::Metadata { PreLockCreationOpReturnData::new( self.get_program_id(), self.get_parameters().borrower_pubkey, self.get_parameters().principal_asset_id, ) - .encode() } } diff --git a/crates/contracts/src/programs/program.rs b/crates/contracts/src/programs/program.rs index 4fe9a82..abb7a72 100644 --- a/crates/contracts/src/programs/program.rs +++ b/crates/contracts/src/programs/program.rs @@ -7,22 +7,11 @@ use simplex::transaction::{ RequiredSignature, UTXO, }; -use simplex::simplicityhl::elements::{AssetId, Script, opcodes, script::Instruction}; +use simplex::simplicityhl::elements::{AssetId, Script}; pub const PROGRAM_ID_LENGTH: usize = 4; pub type ProgramId = [u8; PROGRAM_ID_LENGTH]; -pub fn op_return_payload(script: &Script) -> Option<&[u8]> { - let mut instructions = script.instructions_minimal(); - - match instructions.next()? { - Ok(Instruction::Op(opcodes::all::OP_RETURN)) => {} - _ => return None, - } - - instructions.next()?.ok()?.push_bytes() -} - pub trait CreationOpReturnData: Sized { type Error; @@ -151,3 +140,19 @@ pub trait SimplexProgram { fn get_program_source_code(&self) -> &'static str; } + +pub trait MetadataProgram: SimplexProgram { + type Metadata: CreationOpReturnData; + + fn build_metadata(&self) -> Self::Metadata; + + fn encode_metadata_op_return(&self) -> Vec { + self.build_metadata().encode() + } + + fn decode_metadata_op_return( + op_return_bytes: Vec, + ) -> Result::Error> { + Self::Metadata::decode(&op_return_bytes) + } +} diff --git a/crates/contracts/src/utils/mod.rs b/crates/contracts/src/utils/mod.rs index b8ff19c..8fc2854 100644 --- a/crates/contracts/src/utils/mod.rs +++ b/crates/contracts/src/utils/mod.rs @@ -1,5 +1,7 @@ +pub mod op_return; pub mod parameters; pub mod seed; +pub use op_return::*; pub use parameters::*; pub use seed::*; diff --git a/crates/contracts/src/utils/op_return.rs b/crates/contracts/src/utils/op_return.rs new file mode 100644 index 0000000..e357977 --- /dev/null +++ b/crates/contracts/src/utils/op_return.rs @@ -0,0 +1,63 @@ +use simplex::simplicityhl::elements::{Script, opcodes, script::Instruction}; + +pub fn op_return_payload(script: &Script) -> Option<&[u8]> { + let mut instructions = script.instructions_minimal(); + + match instructions.next()? { + Ok(Instruction::Op(opcodes::all::OP_RETURN)) => {} + _ => return None, + } + + instructions.next()?.ok()?.push_bytes() +} + +#[cfg(test)] +mod tests { + use super::*; + + const OP_RETURN_BYTE: u8 = 0x6a; + const OP_DUP_BYTE: u8 = 0x76; + + #[test] + fn returns_payload_for_op_return_script() { + let payload: &[u8] = b"hello"; + let script = Script::new_op_return(payload); + + assert_eq!(op_return_payload(&script), Some(payload)); + } + + #[test] + fn returns_empty_payload_for_op_return_with_no_data() { + let script = Script::new_op_return(&[]); + + assert_eq!(op_return_payload(&script), Some(&[][..])); + } + + #[test] + fn returns_none_for_empty_script() { + let script = Script::new(); + + assert_eq!(op_return_payload(&script), None); + } + + #[test] + fn returns_none_when_first_opcode_is_not_op_return() { + let script = Script::from(vec![OP_DUP_BYTE]); + + assert_eq!(op_return_payload(&script), None); + } + + #[test] + fn returns_none_when_op_return_is_not_followed_by_a_push() { + let script = Script::from(vec![OP_RETURN_BYTE, OP_DUP_BYTE]); + + assert_eq!(op_return_payload(&script), None); + } + + #[test] + fn returns_none_when_only_op_return_is_present() { + let script = Script::from(vec![OP_RETURN_BYTE]); + + assert_eq!(op_return_payload(&script), None); + } +} diff --git a/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs b/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs index d50652c..397c2e7 100644 --- a/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs +++ b/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs @@ -1,7 +1,6 @@ use lending_contracts::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryParameters}; -use lending_contracts::programs::program::{ - SimplexProgram, op_return_payload as script_op_return_payload, -}; +use lending_contracts::programs::program::{MetadataProgram, SimplexProgram}; +use lending_contracts::utils::op_return_payload as script_op_return_payload; use simplex::simplicityhl::elements::{Transaction, Txid}; use super::setup::setup_issuance_factory; @@ -74,9 +73,9 @@ fn decodes_issuance_factory_creation_metadata(context: simplex::TestContext) -> let issuance_factory_creation_tx = provider.fetch_transaction(&issuance_factory_creation_txid)?; - let decoded_op_return_data = IssuanceFactory::decode_creation_op_return_data( - op_return_payload(&issuance_factory_creation_tx), - )?; + let decoded_op_return_data = IssuanceFactory::decode_metadata_op_return(op_return_payload( + &issuance_factory_creation_tx, + ))?; assert_eq!( decoded_op_return_data.program_id, diff --git a/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs index b1730d7..5feafa7 100644 --- a/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs +++ b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs @@ -1,8 +1,7 @@ use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; -use lending_contracts::programs::program::{ - SimplexProgram, op_return_payload as script_op_return_payload, -}; +use lending_contracts::programs::program::{MetadataProgram, SimplexProgram}; use lending_contracts::utils::LendingOfferParameters; +use lending_contracts::utils::op_return_payload as script_op_return_payload; use simplex::simplicityhl::elements::{Transaction, Txid}; use super::setup::setup_pre_lock; @@ -64,7 +63,7 @@ fn decodes_pre_lock_creation_metadata(context: simplex::TestContext) -> anyhow:: let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; let decoded_op_return_data = - PreLock::decode_creation_op_return_data(op_return_payload(&pre_lock_creation_tx))?; + PreLock::decode_metadata_op_return(op_return_payload(&pre_lock_creation_tx))?; assert_eq!(decoded_op_return_data.program_id, pre_lock.get_program_id()); assert_eq!(