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..1eca928 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, @@ -11,18 +10,16 @@ use crate::artifacts::issuance_factory::IssuanceFactoryProgram; use crate::programs::issuance_factory::{ IssuanceFactoryError, IssuanceFactoryParameters, IssuanceFactoryWitnessBranch, }; -use crate::programs::program::SimplexProgram; + +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, 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; - impl IssuanceFactory { pub fn new(parameters: IssuanceFactoryParameters) -> Self { Self { @@ -35,30 +32,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(); - - 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[CREATION_OP_RETURN_OUTPUT_INDEX].script_pubkey) + .ok_or_else(|| IssuanceFactoryError::NotAnIssuanceFactoryCreationTx(tx.txid()))?; - let owner_pubkey = - IssuanceFactory::decode_creation_op_return_data(op_return_bytes.to_vec())?; + let creation_op_return_data = + IssuanceFactory::decode_metadata_op_return(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(), }; @@ -69,30 +61,6 @@ impl IssuanceFactory { &self.parameters } - 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) - } - - 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 - } - pub fn attach_creation( &self, ft: &mut FinalTransaction, @@ -101,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), @@ -171,4 +139,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/issuance_factory/metadata.rs b/crates/contracts/src/programs/issuance_factory/metadata.rs new file mode 100644 index 0000000..d698d18 --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/metadata.rs @@ -0,0 +1,97 @@ +use simplex::simplicityhl::elements::{hex::ToHex, schnorr::XOnlyPublicKey}; + +use crate::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryError}; +use crate::programs::program::{ + CreationOpReturnData, MetadataProgram, PROGRAM_ID_LENGTH, ProgramId, SimplexProgram, +}; + +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 MetadataProgram for IssuanceFactory { + type Metadata = IssuanceFactoryCreationOpReturnData; + + 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, + ) + } +} diff --git a/crates/contracts/src/programs/issuance_factory/mod.rs b/crates/contracts/src/programs/issuance_factory/mod.rs index 91a8787..e6392ae 100644 --- a/crates/contracts/src/programs/issuance_factory/mod.rs +++ b/crates/contracts/src/programs/issuance_factory/mod.rs @@ -1,9 +1,11 @@ mod core; mod error; +mod metadata; mod params; mod witness; pub use core::IssuanceFactory; pub use error::IssuanceFactoryError; +pub use metadata::IssuanceFactoryCreationOpReturnData; pub use params::IssuanceFactoryParameters; pub use witness::IssuanceFactoryWitnessBranch; 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..2831542 100644 --- a/crates/contracts/src/programs/pre_lock/core.rs +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -1,17 +1,16 @@ 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::SimplexProgram; +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 { @@ -20,7 +19,6 @@ pub struct PreLock { } pub const UTILITY_NFTS_COUNT: usize = 4; -pub const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 64; impl PreLock { pub fn new(parameters: PreLockParameters) -> Self { @@ -79,29 +77,20 @@ 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(); + let op_return_bytes = op_return_payload(&tx.output[5].script_pubkey) + .ok_or_else(|| PreLockError::NotAPreLockCreationTx(tx.txid()))?; - op_return_instr_iter.next(); - - let op_return_bytes = op_return_instr_iter - .next() - .unwrap() - .unwrap() - .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_metadata_op_return(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(), }; @@ -113,33 +102,6 @@ impl PreLock { &self.parameters } - 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)) - } - - 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 - } - pub fn attach_creation(&self, ft: &mut FinalTransaction, parameter_amounts_decimals: u8) { let (first_parameters_nft_amount, second_parameters_nft_amount) = self .parameters @@ -167,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), @@ -292,4 +254,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/metadata.rs b/crates/contracts/src/programs/pre_lock/metadata.rs new file mode 100644 index 0000000..178aaf5 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/metadata.rs @@ -0,0 +1,77 @@ +use simplex::simplicityhl::elements::{AssetId, hex::ToHex, secp256k1_zkp::XOnlyPublicKey}; + +use crate::programs::pre_lock::{PreLock, PreLockError}; +use crate::programs::program::{ + CreationOpReturnData, MetadataProgram, 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 MetadataProgram for PreLock { + type Metadata = PreLockCreationOpReturnData; + + fn build_metadata(&self) -> Self::Metadata { + PreLockCreationOpReturnData::new( + self.get_program_id(), + self.get_parameters().borrower_pubkey, + self.get_parameters().principal_asset_id, + ) + } +} diff --git a/crates/contracts/src/programs/pre_lock/mod.rs b/crates/contracts/src/programs/pre_lock/mod.rs index 5bbf0f4..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, UTILITY_NFTS_COUNT}; pub use error::PreLockError; +pub use metadata::PreLockCreationOpReturnData; 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..abb7a72 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; @@ -8,6 +9,37 @@ use simplex::transaction::{ use simplex::simplicityhl::elements::{AssetId, Script}; +pub const PROGRAM_ID_LENGTH: usize = 4; +pub type ProgramId = [u8; PROGRAM_ID_LENGTH]; + +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>( &self, @@ -94,7 +126,33 @@ pub trait SimplexProgram { self.get_program().get_script_hash(self.get_network()) } + 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]); + + hash_prefix + } + fn get_program(&self) -> &Program; fn get_network(&self) -> &SimplicityNetwork; + + 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/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 + } } 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 new file mode 100644 index 0000000..397c2e7 --- /dev/null +++ b/crates/contracts/tests/issuance_factory/creation_metadata_success_flow.rs @@ -0,0 +1,98 @@ +use lending_contracts::programs::issuance_factory::{IssuanceFactory, IssuanceFactoryParameters}; +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; + +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_metadata_op_return(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 new file mode 100644 index 0000000..5feafa7 --- /dev/null +++ b/crates/contracts/tests/pre_lock/creation_metadata_success_flow.rs @@ -0,0 +1,79 @@ +use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; +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; + +fn op_return_payload(tx: &Transaction) -> Vec { + script_op_return_payload(&tx.output[5].script_pubkey) + .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_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 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_id().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_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!( + 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;