diff --git a/Cargo.lock b/Cargo.lock index 5019d5a..8dca5df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2982,8 +2982,9 @@ dependencies = [ [[package]] name = "smplx-build" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b686d6e0586c28e60cc08141fdce07ffe6617db9dc3bf7b5a4b600d8310cd" dependencies = [ "glob", "globwalk", @@ -3000,8 +3001,9 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e397157e9caf526beea4b7352bd4aefb7796016a56e2cfec3ed41f292b5d00" dependencies = [ "smplx-build", "smplx-test", @@ -3010,11 +3012,15 @@ dependencies = [ [[package]] name = "smplx-regtest" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102989e1221f169624eb9eda5f516280c86016a17d423e9d14afea9c7940711e" dependencies = [ "electrsd", + "hex", + "hmac", "serde", + "sha2", "smplx-sdk", "thiserror 2.0.18", "toml 0.9.12+spec-1.1.0", @@ -3022,8 +3028,9 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce05115b4c973d0a32d3532c0a8f2abae81949b455882a1f6d58d8060c64d9" dependencies = [ "bip39", "bitcoin_hashes", @@ -3041,8 +3048,9 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426623f89436edcf8fb0cb802a3ae05cbd8bf7debae23781a610120a7e133e83" dependencies = [ "either", "serde", @@ -3054,8 +3062,9 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.3" -source = "git+https://github.com/BlockstreamResearch/smplx.git?rev=1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa#1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183774eb3bf52c3e5cf4c1c709a052ccb2b90d9de16983ff8c738ccf4667d1e1" dependencies = [ "electrsd", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 68cd545..1930458 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ multiple_crate_versions = "allow" [workspace.dependencies] ring = "0.17.14" hex = "0.4.3" -simplex = { git = "https://github.com/BlockstreamResearch/smplx.git", rev = "1a6f3bdaad58d243b4ac07b5ae57ec11a7f2c7fa", package = "smplx-std" } +smplx-std = "0.0.4" sha2 = { version = "0.10.9", features = ["compress"] } serde = { version = "1.0.228", features = ["derive"]} thiserror = { version = "2.0.18" } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 3ce2bf7..fb919fd 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -18,7 +18,7 @@ workspace = true thiserror = { workspace = true } hex = { workspace = true } serde = { workspace = true } -simplex = { workspace = true } +smplx-std = { workspace = true } anyhow = "1" dotenvy = "0.15" diff --git a/crates/cli/src/commands/account/core.rs b/crates/cli/src/commands/account/core.rs index d67a674..4c464bb 100644 --- a/crates/cli/src/commands/account/core.rs +++ b/crates/cli/src/commands/account/core.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use clap::Subcommand; -use lending_contracts::transactions::core::SimplexInput; use simplex::{ provider::ProviderTrait, simplicityhl::elements::{Address, AssetId, OutPoint, hex::ToHex}, @@ -12,6 +11,7 @@ use crate::{cli::CliContext, commands::account::AccountCommandError}; #[derive(Debug, Subcommand)] pub enum AccountCommand { + /// Send policy asset to another account SendPolicyAsset { /// Recipient address (Liquid testnet bech32m) #[arg(long = "to-address")] @@ -20,6 +20,7 @@ pub enum AccountCommand { #[arg(long = "amount")] amount: u64, }, + /// Send arbitrary asset to another account SendAsset { /// Recipient address (Liquid testnet bech32m) #[arg(long = "to-address")] @@ -31,6 +32,7 @@ pub enum AccountCommand { #[arg(long = "amount")] amount: u64, }, + /// Split specific UTXO to different parts SplitUTXO { /// UTXO to split #[arg(long = "outpoint")] @@ -39,7 +41,9 @@ pub enum AccountCommand { #[arg(long = "amounts", value_delimiter = ',', num_args = 1..)] amounts: Vec, }, + /// Show current account info ShowAccountInfo, + /// Show account UTXOs ShowAccountUTXOS, } @@ -96,10 +100,8 @@ impl Account { let mut total_inputs_amount = 0; for utxo in asset_utxos { - let input = SimplexInput::new(&utxo, RequiredSignature::NativeEcdsa); - - total_inputs_amount += input.explicit_amount(); - inputs.push(input); + total_inputs_amount += utxo.explicit_amount(); + inputs.push((utxo, RequiredSignature::NativeEcdsa)); if total_inputs_amount >= amount { break; @@ -117,7 +119,7 @@ impl Account { let mut ft = FinalTransaction::new(); for input in inputs { - ft.add_input(input.partial_input().clone(), input.required_sig().clone()); + ft.add_input(PartialInput::new(input.0), input.1); } ft.add_output(PartialOutput::new( diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index 0f89175..5822c72 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -7,18 +7,22 @@ use crate::commands::{ #[derive(Debug, Subcommand)] pub enum Command { + /// Account helper commands Account { #[command(subcommand)] command: AccountCommand, }, + /// Lending offer related commands Lending { #[command(subcommand)] command: LendingCommand, }, + /// Offer creation commands PreLock { #[command(subcommand)] command: PreLockCommand, }, + /// Utility steps related commands Utility { #[command(subcommand)] command: UtilityCommand, diff --git a/crates/cli/src/commands/lending/core.rs b/crates/cli/src/commands/lending/core.rs index fc544f1..a7dfdba 100644 --- a/crates/cli/src/commands/lending/core.rs +++ b/crates/cli/src/commands/lending/core.rs @@ -1,30 +1,31 @@ use clap::Subcommand; -use lending_contracts::programs::Lending; -use lending_contracts::transactions::asset_auth::unlock_asset_auth; -use lending_contracts::transactions::core::SimplexInput; -use lending_contracts::transactions::lending::{ - extract_lending_parameters_from_tx, liquidate_loan, repay_loan, -}; +use lending_contracts::programs::asset_auth::AssetAuthWitnessParams; +use lending_contracts::programs::lending::Lending; use simplex::provider::ProviderTrait; -use simplex::simplicityhl::elements::{OutPoint, Txid}; -use simplex::transaction::{PartialOutput, RequiredSignature, UTXO}; +use simplex::simplicityhl::elements::{OutPoint, Script, Txid}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; use crate::cli::CliContext; use crate::commands::lending::LendingCommandError; #[derive(Debug, Subcommand)] pub enum LendingCommand { + /// Repay loan offer as a borrower Repay { /// Lending covenant creation txid #[arg(long = "lending-creation-txid")] lending_creation_txid: Txid, }, + /// Liquidate loan offer as a lender Liquidate { /// Lending covenant creation txid #[arg(long = "lending-creation-txid")] lending_creation_txid: Txid, }, + /// Claim repaid principal assets as a lender Claim { /// Lending covenant creation txid #[arg(long = "lending-creation-txid")] @@ -65,9 +66,8 @@ impl CliLending { .esplora_provider .fetch_transaction(&lending_creation_txid)?; - let lending_parameters = - extract_lending_parameters_from_tx(&lending_creation_tx, &context.esplora_provider)?; - let lending = Lending::new(lending_parameters); + let lending = Lending::try_from_tx(&lending_creation_tx, &context.esplora_provider)?; + let lending_parameters = lending.get_parameters(); let borrower_nft_utxos = context .signer @@ -77,62 +77,80 @@ impl CliLending { return Err(LendingCommandError::NotABorrower(lending_creation_txid)); } - let borrower_nft_utxo = borrower_nft_utxos.first().unwrap(); + let borrower_nft_utxo = borrower_nft_utxos[0].clone(); let principal_utxos = context .signer .get_utxos_asset(lending_parameters.principal_asset_id)?; - let mut principal_inputs: Vec = Vec::new(); - let mut total_inputs_amount = 0; + let mut principal_inputs: Vec<(UTXO, RequiredSignature)> = Vec::new(); + let mut total_principal_inputs_amount = 0; let principal_with_interest = lending_parameters .offer_parameters .calculate_principal_with_interest(); for utxo in principal_utxos { - let input = SimplexInput::new(&utxo, RequiredSignature::NativeEcdsa); - - total_inputs_amount += input.explicit_amount(); - principal_inputs.push(input); + total_principal_inputs_amount += utxo.explicit_amount(); + principal_inputs.push((utxo, RequiredSignature::NativeEcdsa)); - if total_inputs_amount >= principal_with_interest { + if total_principal_inputs_amount >= principal_with_interest { break; } } - if total_inputs_amount < principal_with_interest { + if total_principal_inputs_amount < principal_with_interest { return Err(LendingCommandError::NotEnoughPrincipalToRepay { expected_amount: principal_with_interest, - actual_amount: total_inputs_amount, + actual_amount: total_principal_inputs_amount, }); } - let ft = repay_loan( - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 0), - txout: lending_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 2), - txout: lending_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 3), - txout: lending_creation_tx.output[3].clone(), - secrets: None, - }, - &SimplexInput::new(borrower_nft_utxo, RequiredSignature::NativeEcdsa), - principal_inputs, - PartialOutput::new( - context.signer.get_address().script_pubkey(), - lending_parameters.offer_parameters.collateral_amount, - lending_parameters.collateral_asset_id, - ), - lending, - )?; + let lending_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 0), + txout: lending_creation_tx.output[0].clone(), + secrets: None, + }; + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_repayment( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + ft.add_input( + PartialInput::new(borrower_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + + for principal_input in principal_inputs { + ft.add_input(PartialInput::new(principal_input.0), principal_input.1); + } + + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + total_principal_inputs_amount - principal_with_interest, + lending_parameters.principal_asset_id, + )); println!("Repaying the loan..."); @@ -153,9 +171,8 @@ impl CliLending { .esplora_provider .fetch_transaction(&lending_creation_txid)?; - let lending_parameters = - extract_lending_parameters_from_tx(&lending_creation_tx, &context.esplora_provider)?; - let lending = Lending::new(lending_parameters); + let lending = Lending::try_from_tx(&lending_creation_tx, &context.esplora_provider)?; + let lending_parameters = lending.get_parameters(); let lender_nft_utxos = context .signer @@ -165,7 +182,7 @@ impl CliLending { return Err(LendingCommandError::NotALender(lending_creation_txid)); } - let lender_nft_utxo = lender_nft_utxos.first().unwrap(); + let lender_nft_utxo = lender_nft_utxos[0].clone(); let current_height = context.esplora_provider.fetch_tip_height()?; @@ -176,30 +193,41 @@ impl CliLending { }); } - let ft = liquidate_loan( - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 0), - txout: lending_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 2), - txout: lending_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_creation_txid, 3), - txout: lending_creation_tx.output[3].clone(), - secrets: None, - }, - &SimplexInput::new(lender_nft_utxo, RequiredSignature::NativeEcdsa), - PartialOutput::new( - context.signer.get_address().script_pubkey(), - lending_parameters.offer_parameters.collateral_amount, - lending_parameters.collateral_asset_id, - ), - lending, - )?; + let lending_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 0), + txout: lending_creation_tx.output[0].clone(), + secrets: None, + }; + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_liquidation( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + ft.add_input( + PartialInput::new(lender_nft_utxo), + RequiredSignature::NativeEcdsa, + ); println!("Liquidating the loan..."); @@ -221,8 +249,8 @@ impl CliLending { .esplora_provider .fetch_transaction(&lending_creation_txid)?; - let lending_parameters = - extract_lending_parameters_from_tx(&lending_creation_tx, &context.esplora_provider)?; + let lending = Lending::try_from_tx(&lending_creation_tx, &context.esplora_provider)?; + let lending_parameters = lending.get_parameters(); let lender_nft_utxos = context .signer @@ -232,7 +260,7 @@ impl CliLending { return Err(LendingCommandError::NotALender(lending_creation_txid)); } - let lender_nft_utxo = lender_nft_utxos.first().unwrap(); + let lender_nft_utxo = lender_nft_utxos[0].clone(); let principal_asset_auth = lending_parameters.get_lender_principal_asset_auth(); let principal_with_interest = lending_parameters @@ -243,20 +271,35 @@ impl CliLending { .esplora_provider .fetch_transaction(&lending_repayment_txid)?; - let ft = unlock_asset_auth( - UTXO { - outpoint: OutPoint::new(lending_repayment_txid, 1), - txout: lending_repayment_tx.output[1].clone(), - secrets: None, - }, - &SimplexInput::new(lender_nft_utxo, RequiredSignature::NativeEcdsa), - PartialOutput::new( - context.signer.get_address().script_pubkey(), - principal_with_interest, - lending_parameters.principal_asset_id, - ), - principal_asset_auth, + let principal_asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + let principal_asset_auth_utxo = UTXO { + outpoint: OutPoint::new(lending_repayment_txid, 1), + txout: lending_repayment_tx.output[1].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + principal_asset_auth.attach_unlocking( + &mut ft, + principal_asset_auth_utxo, + principal_asset_auth_witness_params, + ); + + ft.add_input( + PartialInput::new(lender_nft_utxo), + RequiredSignature::NativeEcdsa, ); + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + principal_with_interest, + lending_parameters.principal_asset_id, + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + 1, + lending_parameters.lender_nft_asset_id, + )); println!("Claiming principal with interest..."); diff --git a/crates/cli/src/commands/lending/error.rs b/crates/cli/src/commands/lending/error.rs index 9d91d04..67b97fc 100644 --- a/crates/cli/src/commands/lending/error.rs +++ b/crates/cli/src/commands/lending/error.rs @@ -1,4 +1,4 @@ -use lending_contracts::transactions::lending::LendingTransactionError; +use lending_contracts::programs::lending::LendingError; use simplex::{ provider::ProviderError, signer::SignerError, @@ -29,8 +29,8 @@ pub enum LendingCommandError { actual_amount: u64, }, - #[error("Failed to build lending transaction: {0}")] - LendingTransaction(#[from] LendingTransactionError), + #[error("Lending error: {0}")] + Lending(#[from] LendingError), #[error("Simplex Signer error: {0}")] Signer(#[from] SignerError), diff --git a/crates/cli/src/commands/pre_lock/core.rs b/crates/cli/src/commands/pre_lock/core.rs index 0d5096e..0339899 100644 --- a/crates/cli/src/commands/pre_lock/core.rs +++ b/crates/cli/src/commands/pre_lock/core.rs @@ -2,16 +2,13 @@ use std::str::FromStr; use clap::Subcommand; -use lending_contracts::programs::{PreLock, PreLockParameters}; -use lending_contracts::transactions::core::SimplexInput; -use lending_contracts::transactions::pre_lock::{ - cancel_pre_lock, create_lending_from_pre_lock, create_pre_lock, - extract_pre_lock_parameters_from_tx, -}; +use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; use lending_contracts::utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}; use simplex::provider::ProviderTrait; use simplex::simplicityhl::elements::{AssetId, OutPoint, Txid}; -use simplex::transaction::{PartialOutput, RequiredSignature, UTXO}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; use simplex::utils::hash_script; use crate::cli::CliContext; @@ -19,6 +16,7 @@ use crate::commands::pre_lock::PreLockCommandError; #[derive(Debug, Subcommand)] pub enum PreLockCommand { + /// Finish offer creation process Create { /// Utility NFTs issuance txid #[arg(long = "utility-nfts-issuance-txid")] @@ -30,11 +28,13 @@ pub enum PreLockCommand { #[arg(long = "principal-asset-id-hex-be")] principal_asset_id_hex_be: String, }, + /// Accept offer as a lender CreateLending { /// PreLock covenant creation txid #[arg(long = "pre-lock-creation-txid")] pre_lock_creation_txid: Txid, }, + /// Cancel offer as a borrower CancelOffer { /// PreLock covenant creation txid #[arg(long = "pre-lock-creation-txid")] @@ -51,7 +51,7 @@ impl CliPreLock { utility_nfts_issuance_txid, collateral_asset_id_hex_be, principal_asset_id_hex_be, - } => CliPreLock::create_pre_lock_tx( + } => CliPreLock::create_pre_lock( context, *utility_nfts_issuance_txid, collateral_asset_id_hex_be, @@ -59,14 +59,14 @@ impl CliPreLock { ), PreLockCommand::CreateLending { pre_lock_creation_txid, - } => CliPreLock::create_lending_from_pre_lock_tx(context, *pre_lock_creation_txid), + } => CliPreLock::create_lending_from_pre_lock(context, *pre_lock_creation_txid), PreLockCommand::CancelOffer { pre_lock_creation_txid, - } => CliPreLock::cancel_pre_lock_tx(context, *pre_lock_creation_txid), + } => CliPreLock::cancel_pre_lock(context, *pre_lock_creation_txid), } } - fn create_pre_lock_tx( + fn create_pre_lock( context: CliContext, utility_nfts_issuance_txid: Txid, collateral_asset_id_hex_be: &str, @@ -138,44 +138,54 @@ impl CliPreLock { )); } - let collateral_utxo = collateral_utxos.first().unwrap(); - - let (ft, _) = create_pre_lock( - &SimplexInput::new(collateral_utxo, RequiredSignature::NativeEcdsa), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 0), - txout: utility_nfts_tx.output[0].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 1), - txout: utility_nfts_tx.output[1].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 2), - txout: utility_nfts_tx.output[2].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 3), - txout: utility_nfts_tx.output[3].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - pre_lock_parameters, + let pre_lock = PreLock::new(pre_lock_parameters); + + let collateral_utxo = collateral_utxos[0].clone(); + let first_parameters_utxo = UTXO { + outpoint: OutPoint::new(utility_nfts_issuance_txid, 0), + txout: utility_nfts_tx.output[0].clone(), + secrets: None, + }; + let second_parameters_utxo = UTXO { + outpoint: OutPoint::new(utility_nfts_issuance_txid, 1), + txout: utility_nfts_tx.output[1].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(utility_nfts_issuance_txid, 2), + txout: utility_nfts_tx.output[2].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(utility_nfts_issuance_txid, 3), + txout: utility_nfts_tx.output[3].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(collateral_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(first_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(second_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(borrower_nft_utxo.clone()), + RequiredSignature::NativeEcdsa, ); + ft.add_input( + PartialInput::new(lender_nft_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + pre_lock.attach_creation(&mut ft, 1); println!( "Creating Lending offer with next parameters: {:?}", @@ -191,7 +201,7 @@ impl CliPreLock { Ok(()) } - fn create_lending_from_pre_lock_tx( + fn create_lending_from_pre_lock( context: CliContext, pre_lock_creation_txid: Txid, ) -> Result<(), PreLockCommandError> { @@ -199,27 +209,33 @@ impl CliPreLock { .esplora_provider .fetch_transaction(&pre_lock_creation_txid)?; - let pre_lock_parameters = - extract_pre_lock_parameters_from_tx(&pre_lock_creation_tx, &context.esplora_provider)?; - let pre_lock = PreLock::new(pre_lock_parameters); + let pre_lock = PreLock::try_from_tx(&pre_lock_creation_tx, &context.esplora_provider)?; + let pre_lock_parameters = pre_lock.get_parameters(); - let principal_utxos = context.signer.get_utxos_filter( - &|utxo| { - utxo.explicit_asset() == pre_lock_parameters.principal_asset_id - && utxo.explicit_amount() - == pre_lock_parameters.offer_parameters.principal_amount - }, - &|_| true, - )?; + let principal_utxos = context + .signer + .get_utxos_asset(pre_lock_parameters.principal_asset_id)?; - if principal_utxos.is_empty() { - return Err(PreLockCommandError::NoSuitablePrincipalUTXOsFound( - pre_lock_parameters.offer_parameters.principal_amount, - )); + let mut principal_inputs: Vec<(UTXO, RequiredSignature)> = Vec::new(); + let mut total_principal_inputs_amount = 0; + + for utxo in principal_utxos { + total_principal_inputs_amount += utxo.explicit_amount(); + principal_inputs.push((utxo, RequiredSignature::NativeEcdsa)); + + if total_principal_inputs_amount + >= pre_lock_parameters.offer_parameters.principal_amount + { + break; + } } - let principal_utxo = principal_utxos.first().unwrap(); - let signer_script_pubkey = context.signer.get_address().script_pubkey(); + if total_principal_inputs_amount < pre_lock_parameters.offer_parameters.principal_amount { + return Err(PreLockCommandError::NotEnoughPrincipalToAcceptOffer { + expected_amount: pre_lock_parameters.offer_parameters.principal_amount, + actual_amount: total_principal_inputs_amount, + }); + } let prev_collateral_outpoint = pre_lock_creation_tx.input[0].previous_output; let pre_collateral_tx = context @@ -228,45 +244,74 @@ impl CliPreLock { let borrower_output_script = &pre_collateral_tx.output[prev_collateral_outpoint.vout as usize].script_pubkey; - let (ft, _) = create_lending_from_pre_lock( - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 0), - txout: pre_lock_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 1), - txout: pre_lock_creation_tx.output[1].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 2), - txout: pre_lock_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 3), - txout: pre_lock_creation_tx.output[3].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 4), - txout: pre_lock_creation_tx.output[4].clone(), - secrets: None, - }, - vec![&SimplexInput::new( - principal_utxo, - RequiredSignature::NativeEcdsa, - )], - PartialOutput::new( - signer_script_pubkey.clone(), - 1, - pre_lock_parameters.lender_nft_asset_id, - ), - borrower_output_script.clone(), - pre_lock, + let pre_lock_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 0), + txout: pre_lock_creation_tx.output[0].clone(), + secrets: None, + }; + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 1), + txout: pre_lock_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 2), + txout: pre_lock_creation_tx.output[2].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 3), + txout: pre_lock_creation_tx.output[3].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 4), + txout: pre_lock_creation_tx.output[4].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + pre_lock.attach_lending_creation( + &mut ft, + pre_lock_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + borrower_nft_utxo, + lender_nft_utxo, ); + for input in principal_inputs { + ft.add_input(PartialInput::new(input.0), input.1); + } + + ft.add_output(PartialOutput::new( + borrower_output_script.clone(), + 1, + pre_lock_parameters.borrower_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + 1, + pre_lock_parameters.lender_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + borrower_output_script.clone(), + pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + + if total_principal_inputs_amount > pre_lock_parameters.offer_parameters.principal_amount { + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + total_principal_inputs_amount + - pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + } + println!("Activating Lending offer..."); let (tx, _) = context.signer.finalize(&ft)?; @@ -278,7 +323,7 @@ impl CliPreLock { Ok(()) } - fn cancel_pre_lock_tx( + fn cancel_pre_lock( context: CliContext, pre_lock_creation_txid: Txid, ) -> Result<(), PreLockCommandError> { @@ -286,42 +331,50 @@ impl CliPreLock { .esplora_provider .fetch_transaction(&pre_lock_creation_txid)?; - let pre_lock_parameters = - extract_pre_lock_parameters_from_tx(&pre_lock_creation_tx, &context.esplora_provider)?; - let pre_lock = PreLock::new(pre_lock_parameters); + let pre_lock = PreLock::try_from_tx(&pre_lock_creation_tx, &context.esplora_provider)?; + let pre_lock_parameters = pre_lock.get_parameters(); - let ft = cancel_pre_lock( - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 0), - txout: pre_lock_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 1), - txout: pre_lock_creation_tx.output[1].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 2), - txout: pre_lock_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 3), - txout: pre_lock_creation_tx.output[3].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_creation_txid, 4), - txout: pre_lock_creation_tx.output[4].clone(), - secrets: None, - }, - PartialOutput::new( - context.signer.get_address().script_pubkey(), - pre_lock_parameters.offer_parameters.collateral_amount, - pre_lock_parameters.collateral_asset_id, - ), - pre_lock, + let pre_lock_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 0), + txout: pre_lock_creation_tx.output[0].clone(), + secrets: None, + }; + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 1), + txout: pre_lock_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 2), + txout: pre_lock_creation_tx.output[2].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 3), + txout: pre_lock_creation_tx.output[3].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 4), + txout: pre_lock_creation_tx.output[4].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + context.signer.get_address().script_pubkey(), + pre_lock_parameters.offer_parameters.collateral_amount, + pre_lock_parameters.collateral_asset_id, + )); + + pre_lock.attach_cancellation( + &mut ft, + pre_lock_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + borrower_nft_utxo, + lender_nft_utxo, ); println!("Cancelling Lending offer..."); diff --git a/crates/cli/src/commands/pre_lock/error.rs b/crates/cli/src/commands/pre_lock/error.rs index 8ee0ff7..2e1b0e3 100644 --- a/crates/cli/src/commands/pre_lock/error.rs +++ b/crates/cli/src/commands/pre_lock/error.rs @@ -1,4 +1,4 @@ -use lending_contracts::transactions::pre_lock::PreLockTransactionError; +use lending_contracts::programs::pre_lock::PreLockError; use simplex::{ provider::ProviderError, signer::SignerError, simplicityhl::simplicity::hex::HexToArrayError, }; @@ -11,8 +11,16 @@ pub enum PreLockCommandError { #[error("No suitable principal utxos found for the {0} principal amount")] NoSuitablePrincipalUTXOsFound(u64), - #[error("Failed to build pre lock transaction: {0}")] - PreLockTransaction(#[from] PreLockTransactionError), + #[error( + "Not enough principal assets to accept the offer: expected - {expected_amount}, actual - {actual_amount}" + )] + NotEnoughPrincipalToAcceptOffer { + expected_amount: u64, + actual_amount: u64, + }, + + #[error("PreLock error: {0}")] + PreLock(#[from] PreLockError), #[error("Simplex Signer error: {0}")] Signer(#[from] SignerError), diff --git a/crates/cli/src/commands/utility/core.rs b/crates/cli/src/commands/utility/core.rs index 3c4e015..abc17c7 100644 --- a/crates/cli/src/commands/utility/core.rs +++ b/crates/cli/src/commands/utility/core.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use clap::Subcommand; -use lending_contracts::transactions::core::SimplexInput; +use lending_contracts::programs::pre_lock::UTILITY_NFTS_COUNT; use lending_contracts::utils::{LendingOfferParameters, get_random_seed}; use simplex::provider::ProviderTrait; use simplex::simplicityhl::elements::AssetId; @@ -10,21 +10,20 @@ use simplex::simplicityhl::elements::hex::ToHex; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; -use lending_contracts::transactions::utility::{ - UTILITY_NFTS_COUNT, issue_preparation_utxos, issue_utility_nfts, -}; - use crate::cli::CliContext; use crate::commands::utility::UtilityCommandError; #[derive(Debug, Subcommand)] pub enum UtilityCommand { + /// Issue arbitrary amount of new asset IssueAsset { /// Asset amount to issue #[arg(long = "asset-amount")] asset_amount: u64, }, + /// Issue preparation UTXOs for the Utility NFTs issuance process IssuePreparationUTXOS, + /// Issue Utility NFTs for the loan offer IssueUtilityNfts { /// Preparation UTXOs asset ID in hexadecimal (big-endian) #[arg(long = "preparation-utxos-asset-id-hex-be")] @@ -44,6 +43,8 @@ pub enum UtilityCommand { }, } +const PREPARATION_UTXO_ASSET_AMOUNT: u64 = 10; + pub struct Utility {} impl Utility { @@ -88,7 +89,7 @@ impl Utility { let mut ft = FinalTransaction::new(); - let asset_id = ft.add_issuance_input( + let (asset_id, _) = ft.add_issuance_input( PartialInput::new(first_utxo.clone()), IssuanceInput::new(asset_amount, asset_entropy), RequiredSignature::NativeEcdsa, @@ -122,17 +123,30 @@ impl Utility { let policy_utxos = context .signer - .get_utxos_asset(context.esplora_provider.network.policy_asset())?; + .get_utxos_asset(context.get_network().policy_asset())?; let issuance_utxo = policy_utxos .first() .expect("Must be at least one policy asset UTXO to issue preparation utxos"); - let (ft, asset_id) = issue_preparation_utxos( - &SimplexInput::new(issuance_utxo, RequiredSignature::NativeEcdsa), - signer_script_pubkey, - context.get_network(), + let mut ft = FinalTransaction::new(); + + let total_asset_amount = PREPARATION_UTXO_ASSET_AMOUNT * UTILITY_NFTS_COUNT as u64; + let asset_entropy = get_random_seed(); + + let (asset_id, _) = ft.add_issuance_input( + PartialInput::new(issuance_utxo.clone()), + IssuanceInput::new(total_asset_amount, asset_entropy), + RequiredSignature::NativeEcdsa, ); + for _ in 0..UTILITY_NFTS_COUNT { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + PREPARATION_UTXO_ASSET_AMOUNT, + asset_id, + )); + } + println!( "Issuing preparation UTXOs with the {} asset id...", asset_id.to_hex() @@ -163,19 +177,45 @@ impl Utility { }); } - let issuance_inputs = issuance_utxos - .iter() - .map(|utxo| SimplexInput::new(utxo, RequiredSignature::NativeEcdsa)) - .collect(); + let mut ft = FinalTransaction::new(); + + let (first_parameters_nft_amount, second_parameters_nft_amount) = + offer_parameters.encode_parameters_nft_amounts(1)?; - let issuance_asset_entropy = get_random_seed(); - let ft = issue_utility_nfts( - issuance_inputs, - signer_script_pubkey, - &offer_parameters, + let utility_nfts_amounts = [ + first_parameters_nft_amount, + second_parameters_nft_amount, + 1, 1, - issuance_asset_entropy, - )?; + ]; + let mut asset_ids: Vec = Vec::with_capacity(UTILITY_NFTS_COUNT); + + let issuance_asset_entropy = get_random_seed(); + + for (index, utxo) in issuance_utxos.iter().enumerate() { + let (asset_id, _) = ft.add_issuance_input( + PartialInput::new(utxo.clone()), + IssuanceInput::new(utility_nfts_amounts[index], issuance_asset_entropy), + RequiredSignature::NativeEcdsa, + ); + asset_ids.push(asset_id); + } + + for (index, asset_id) in asset_ids.into_iter().enumerate() { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + utility_nfts_amounts[index], + asset_id, + )); + } + + for utxo in issuance_utxos { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + utxo.explicit_amount(), + utxo.explicit_asset(), + )); + } println!( "Issuing utility NFTs with the next offer parameters: {:?}", diff --git a/crates/cli/src/commands/utility/error.rs b/crates/cli/src/commands/utility/error.rs index 6e62dc7..6b90965 100644 --- a/crates/cli/src/commands/utility/error.rs +++ b/crates/cli/src/commands/utility/error.rs @@ -1,4 +1,4 @@ -use lending_contracts::transactions::utility::UtilityTransactionError; +use lending_contracts::utils::ParametersError; use simplex::{ provider::ProviderError, signer::SignerError, simplicityhl::simplicity::hex::HexToArrayError, }; @@ -8,8 +8,8 @@ pub enum UtilityCommandError { #[error("Invalid preparation UTXOs count: expected - {expected}, actual - {actual}")] InvalidPreparationUTXOsCount { expected: usize, actual: usize }, - #[error("Failed to build utility transaction: {0}")] - UtilityTransaction(#[from] UtilityTransactionError), + #[error("Parameters error: {0}")] + Parameters(#[from] ParametersError), #[error("Simplex Signer error: {0}")] Signer(#[from] SignerError), diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index f80c2e1..45202d6 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -25,7 +25,7 @@ hex = { workspace = true } thiserror = { workspace = true } modular-bitfield = { workspace = true } -simplex = { workspace = true } +smplx-std = { workspace = true } [dev-dependencies] anyhow = "1" diff --git a/crates/contracts/simf/issuance_factory.simf b/crates/contracts/simf/issuance_factory.simf new file mode 100644 index 0000000..78a6db9 --- /dev/null +++ b/crates/contracts/simf/issuance_factory.simf @@ -0,0 +1,161 @@ +// Helper getters + +fn get_script_hash(index: u32, is_input_index: bool) -> u256 { + let script_hash: u256 = match is_input_index { + true => unwrap(jet::input_script_hash(index)), + false => unwrap(jet::output_script_hash(index)), + }; + + script_hash +} + +fn get_asset_and_amount(index: u32, is_input_index: bool) -> (u256, u64) { + let pair: (Asset1, Amount1) = match is_input_index { + true => unwrap(jet::input_amount(index)), + false => unwrap(jet::output_amount(index)), + }; + let (asset, amount): (Asset1, Amount1) = pair; + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +// Check helpers + +fn check_asset_amounts_eq(asset_amount_1: u64, asset_amount_2: u64) { + assert!(jet::eq_64(asset_amount_1, asset_amount_2)); +} + +fn check_assets_eq(asset_bits_1: u256, asset_bits_2: u256) { + assert!(jet::eq_256(asset_bits_1, asset_bits_2)); +} + +fn check_script_hashes_eq(script_1: u256, script_2: u256) { + assert!(jet::eq_256(script_1, script_2)); +} + +fn check_flags_eq(flag_1: bool, flag_2: bool) { + assert!(jet::eq_1(::into(flag_1), ::into(flag_2))); +} + +fn ensure_asset_with_amount(index: u32, is_input_index: bool, expected_asset_bits: u256, expected_amount: u64) { + let (asset_bits, amount): (u256, u64) = get_asset_and_amount(index, is_input_index); + + check_assets_eq(asset_bits, expected_asset_bits); + check_asset_amounts_eq(amount, expected_amount); +} + +fn ensure_input_and_output_eq(input_index: u32, output_index: u32) { + let (input_asset_bits, input_amount): (u256, u64) = get_asset_and_amount(input_index, true); + let (output_asset_bits, output_amount): (u256, u64) = get_asset_and_amount(output_index, false); + + check_assets_eq(input_asset_bits, input_asset_bits); + check_asset_amounts_eq(input_amount, output_amount); +} + +fn ensure_input_and_output_script_hashes_eq(input_index: u32, output_index: u32) { + let input_script_hash: u256 = get_script_hash(input_index, true); + let output_script_hash: u256 = get_script_hash(output_index, false); + + check_script_hashes_eq(input_script_hash, output_script_hash); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +// Main paths logic + +fn get_reissuance_flag(index: u8) -> bool { + let shifted: u64 = jet::right_shift_64(index, param::REISSUANCE_FLAGS); + let bit_val: u1 = jet::rightmost_64_1(shifted); + + ::into(bit_val) +} + +fn verify_issuance(input_index: u32, output_index: u32, reissuance_flag: bool) { + let contract_hash: u256 = unwrap(unwrap(jet::new_issuance_contract(input_index))); + let outpoint: Outpoint = unwrap(jet::input_prev_outpoint(input_index)); + let issuance_entropy: u256 = jet::calculate_issuance_entropy(outpoint, contract_hash); + + let has_reissuance: bool = unwrap(unwrap(jet::issuance(input_index))); + + assert!(jet::eq_1(::into(has_reissuance), ::into(reissuance_flag))); + + match has_reissuance { + true => { + let conf_token: ExplicitAsset = jet::calculate_confidential_token(issuance_entropy); + }, + false => {}, + }; + + let issuance_amount: Amount1 = unwrap(unwrap(jet::issuance_asset_amount(input_index))); + let issuance_asset: ExplicitAsset = unwrap(unwrap(jet::issuance_asset(input_index))); + + let issuance_amount: u64 = unwrap_right::<(u1, u256)>(issuance_amount); + + ensure_asset_with_amount(output_index, false, issuance_asset, issuance_amount); +} + +fn verify_issuance_step(acc: (), start_output_index: u32, i: u8) -> Either<(), ()> { + match jet::le_8(param::ISSUING_UTXOS_COUNT, i) { + true => Left(()), + false => { + let step_index: u32 = <(u16, u16)>::into((0, <(u8, u8)>::into((0, i)))); + + let (carry, input_index): (bool, u32) = jet::add_32(jet::current_index(), step_index); + ensure_zero_bit(carry); + + let (carry, output_index): (bool, u32) = jet::add_32(start_output_index, step_index); + ensure_zero_bit(carry); + + verify_issuance(input_index, output_index, get_reissuance_flag(i)); + + Right(()) + } + } +} + +fn issue_new_assets(output_index: u32, owner_sig: Signature) { + let current_index: u32 = jet::current_index(); + + assert!(jet::eq_32(current_index, 0)); + jet::bip_0340_verify((param::FACTORY_OWNER_PUBKEY, jet::sig_all_hash()), owner_sig); + + ensure_input_and_output_script_hashes_eq(current_index, output_index); + + let (carry, issued_outputs_start_index): (bool, u32) = jet::add_32(output_index, 1); + ensure_zero_bit(carry); + + unwrap_left::<()>(for_while::((), issued_outputs_start_index)); +} + +fn remove_factory(output_index: u32, owner_sig: Signature) { + let current_index: u32 = jet::current_index(); + + assert!(jet::eq_32(current_index, 0)); + jet::bip_0340_verify((param::FACTORY_OWNER_PUBKEY, jet::sig_all_hash()), owner_sig); + + ensure_output_is_op_return(output_index); + ensure_input_and_output_eq(current_index, output_index); +} + +fn main() { + match witness::PATH { + Left(params: (u32, Signature)) => { + let (output_index, owner_sig): (u32, Signature) = params; + + issue_new_assets(output_index, owner_sig); + }, + Right(params: (u32, Signature)) => { + let (output_index, owner_sig): (u32, Signature) = params; + + remove_factory(output_index, owner_sig); + } + } +} \ No newline at end of file diff --git a/crates/contracts/simf/ownable_script_auth.simf b/crates/contracts/simf/ownable_script_auth.simf index 64564f8..a9cc274 100644 --- a/crates/contracts/simf/ownable_script_auth.simf +++ b/crates/contracts/simf/ownable_script_auth.simf @@ -124,16 +124,14 @@ fn script_auth_check(owner: Pubkey, owner_sig: Signature, input_script_index: u3 } fn main() { - let owner_sig: Signature = witness::SIGNATURE; - match witness::PATH { - Left(params: (Pubkey, Pubkey, u32)) => { - let (current_owner, new_owner, program_output_index): (Pubkey, Pubkey, u32) = params; + Left(params: (Pubkey, Pubkey, Signature, u32)) => { + let (current_owner, new_owner, owner_sig, program_output_index): (Pubkey, Pubkey, Signature, u32) = params; ownership_transfer(current_owner, new_owner, owner_sig, program_output_index); }, - Right(params: (Pubkey, u32)) => { - let (owner, input_script_index): (Pubkey, u32) = params; + Right(params: (Pubkey, Signature, u32)) => { + let (owner, owner_sig, input_script_index): (Pubkey, Signature, u32) = params; script_auth_check(owner, owner_sig, input_script_index); } diff --git a/crates/contracts/simf/pre_lock.simf b/crates/contracts/simf/pre_lock.simf index d6f8cc9..e10d5f3 100644 --- a/crates/contracts/simf/pre_lock.simf +++ b/crates/contracts/simf/pre_lock.simf @@ -171,10 +171,10 @@ fn create_lending_path() { assert!(jet::eq_32(jet::current_index(), 0)); ensure_input_and_output_assets_with_amount_eq(0, 0, param::COLLATERAL_ASSET_ID, param::COLLATERAL_AMOUNT); - let first_parameters_amount: u64 = ensure_input_and_output_assets_eq(1, 2, param::FIRST_PARAMETERS_NFT_ASSET_ID); - let second_parameters_amount: u64 = ensure_input_and_output_assets_eq(2, 3, param::SECOND_PARAMETERS_NFT_ASSET_ID); - ensure_input_and_output_assets_with_amount_eq(3, 4, param::BORROWER_NFT_ASSET_ID, 1); - ensure_input_and_output_assets_with_amount_eq(4, 5, param::LENDER_NFT_ASSET_ID, 1); + let first_parameters_amount: u64 = ensure_input_and_output_assets_eq(1, 1, param::FIRST_PARAMETERS_NFT_ASSET_ID); + let second_parameters_amount: u64 = ensure_input_and_output_assets_eq(2, 2, param::SECOND_PARAMETERS_NFT_ASSET_ID); + ensure_input_and_output_assets_with_amount_eq(3, 3, param::BORROWER_NFT_ASSET_ID, 1); + ensure_input_and_output_assets_with_amount_eq(4, 4, param::LENDER_NFT_ASSET_ID, 1); let ( collateral_amount, @@ -185,13 +185,13 @@ fn create_lending_path() { validate_lending_params(collateral_amount, principal_amount, loan_expiration_time, interest_rate); - ensure_asset_with_amount(1, false, param::PRINCIPAL_ASSET_ID, param::PRINCIPAL_AMOUNT); + ensure_asset_with_amount(5, false, param::PRINCIPAL_ASSET_ID, param::PRINCIPAL_AMOUNT); ensure_script_hash(0, false, param::LENDING_COV_HASH); // Lending covenant script hash - ensure_script_hash(1, false, param::PRINCIPAL_OUTPUT_SCRIPT_HASH); // P2PKH script created with the BORROWER_PUB_KEY + ensure_script_hash(1, false, param::PARAMETERS_NFT_OUTPUT_SCRIPT_HASH); // ScriptAuth covenant script hash with the LENDING_COV_HASH as auth script ensure_script_hash(2, false, param::PARAMETERS_NFT_OUTPUT_SCRIPT_HASH); // ScriptAuth covenant script hash with the LENDING_COV_HASH as auth script - ensure_script_hash(3, false, param::PARAMETERS_NFT_OUTPUT_SCRIPT_HASH); // ScriptAuth covenant script hash with the LENDING_COV_HASH as auth script - ensure_script_hash(4, false, param::BORROWER_NFT_OUTPUT_SCRIPT_HASH); // P2PKH script created with the BORROWER_PUB_KEY + ensure_script_hash(3, false, param::BORROWER_NFT_OUTPUT_SCRIPT_HASH); // P2PKH script created with the BORROWER_PUB_KEY + ensure_script_hash(5, false, param::PRINCIPAL_OUTPUT_SCRIPT_HASH); // P2PKH script created with the BORROWER_PUB_KEY } fn cancel_pre_lock_path(sig: Signature) { @@ -225,9 +225,8 @@ fn main() { Left(params: ()) => { create_lending_path(); }, - Right(params: ()) => { - let sig: Signature = witness::SIGNATURE; - cancel_pre_lock_path(sig); + Right(cancellation_sig: Signature) => { + cancel_pre_lock_path(cancellation_sig); } } } \ No newline at end of file diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 88f5fa2..7f0f6ed 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -1,4 +1,3 @@ pub mod artifacts; pub mod programs; -pub mod transactions; pub mod utils; diff --git a/crates/contracts/src/programs/asset_auth.rs b/crates/contracts/src/programs/asset_auth.rs deleted file mode 100644 index ffff120..0000000 --- a/crates/contracts/src/programs/asset_auth.rs +++ /dev/null @@ -1,65 +0,0 @@ -use simplex::program::Program; -use simplex::provider::SimplicityNetwork; -use simplex::simplicityhl::elements::AssetId; - -use crate::artifacts::asset_auth::AssetAuthProgram; -use crate::artifacts::asset_auth::derived_asset_auth::{AssetAuthArguments, AssetAuthWitness}; -use crate::programs::program::SimplexProgram; - -#[derive(Debug, Clone, Copy)] -pub struct AssetAuthParameters { - pub asset_id: AssetId, - pub asset_amount: u64, - pub with_asset_burn: bool, - pub network: SimplicityNetwork, -} - -impl From for AssetAuthArguments { - fn from(value: AssetAuthParameters) -> Self { - Self { - with_asset_burn: value.with_asset_burn, - asset_amount: value.asset_amount, - asset_id: value.asset_id.into_inner().0, - } - } -} - -pub struct AssetAuth { - program: AssetAuthProgram, - parameters: AssetAuthParameters, -} - -pub struct AssetAuthWitnessParams { - pub input_asset_index: u32, - pub output_asset_index: u32, -} - -impl AssetAuth { - pub fn new(parameters: AssetAuthParameters) -> Self { - Self { - program: AssetAuthProgram::new(AssetAuthArguments::from(parameters)), - parameters, - } - } - - pub fn get_asset_auth_witness(witness_params: &AssetAuthWitnessParams) -> AssetAuthWitness { - AssetAuthWitness { - input_asset_index: witness_params.input_asset_index, - output_asset_index: witness_params.output_asset_index, - } - } - - pub fn get_asset_auth_parameters(&self) -> &AssetAuthParameters { - &self.parameters - } -} - -impl SimplexProgram for AssetAuth { - fn get_program(&self) -> &Program { - self.program.get_program() - } - - fn get_network(&self) -> &SimplicityNetwork { - &self.parameters.network - } -} diff --git a/crates/contracts/src/programs/asset_auth/core.rs b/crates/contracts/src/programs/asset_auth/core.rs new file mode 100644 index 0000000..47081a3 --- /dev/null +++ b/crates/contracts/src/programs/asset_auth/core.rs @@ -0,0 +1,57 @@ +use simplex::{ + program::Program, + provider::SimplicityNetwork, + simplicityhl::elements::AssetId, + transaction::{FinalTransaction, UTXO}, +}; + +use crate::artifacts::asset_auth::AssetAuthProgram; + +use crate::programs::asset_auth::{AssetAuthParameters, AssetAuthWitnessParams}; +use crate::programs::program::SimplexProgram; + +pub struct AssetAuth { + program: AssetAuthProgram, + parameters: AssetAuthParameters, +} + +impl AssetAuth { + pub fn new(parameters: AssetAuthParameters) -> Self { + Self { + program: AssetAuthProgram::new(parameters.build_arguments()), + parameters, + } + } + + pub fn get_parameters(&self) -> &AssetAuthParameters { + &self.parameters + } + + pub fn attach_creation( + &self, + ft: &mut FinalTransaction, + asset_id_to_lock: AssetId, + amount_to_lock: u64, + ) { + self.add_program_output(ft, asset_id_to_lock, amount_to_lock); + } + + pub fn attach_unlocking( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + witness_params: AssetAuthWitnessParams, + ) { + self.add_program_input(ft, program_utxo, witness_params.build_witness()); + } +} + +impl SimplexProgram for AssetAuth { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/asset_auth/mod.rs b/crates/contracts/src/programs/asset_auth/mod.rs new file mode 100644 index 0000000..1acbaa0 --- /dev/null +++ b/crates/contracts/src/programs/asset_auth/mod.rs @@ -0,0 +1,7 @@ +mod core; +mod params; +mod witness; + +pub use core::AssetAuth; +pub use params::AssetAuthParameters; +pub use witness::AssetAuthWitnessParams; diff --git a/crates/contracts/src/programs/asset_auth/params.rs b/crates/contracts/src/programs/asset_auth/params.rs new file mode 100644 index 0000000..b4cc746 --- /dev/null +++ b/crates/contracts/src/programs/asset_auth/params.rs @@ -0,0 +1,21 @@ +use simplex::{provider::SimplicityNetwork, simplicityhl::elements::AssetId}; + +use crate::artifacts::asset_auth::derived_asset_auth::AssetAuthArguments; + +#[derive(Debug, Clone, Copy)] +pub struct AssetAuthParameters { + pub asset_id: AssetId, + pub asset_amount: u64, + pub with_asset_burn: bool, + pub network: SimplicityNetwork, +} + +impl AssetAuthParameters { + pub fn build_arguments(&self) -> AssetAuthArguments { + AssetAuthArguments { + with_asset_burn: self.with_asset_burn, + asset_amount: self.asset_amount, + asset_id: self.asset_id.into_inner().0, + } + } +} diff --git a/crates/contracts/src/programs/asset_auth/witness.rs b/crates/contracts/src/programs/asset_auth/witness.rs new file mode 100644 index 0000000..cdaa9f8 --- /dev/null +++ b/crates/contracts/src/programs/asset_auth/witness.rs @@ -0,0 +1,23 @@ +use crate::artifacts::asset_auth::derived_asset_auth::AssetAuthWitness; + +#[derive(Debug, Clone, Copy)] +pub struct AssetAuthWitnessParams { + pub input_asset_index: u32, + pub output_asset_index: u32, +} + +impl AssetAuthWitnessParams { + pub fn new(input_asset_index: u32, output_asset_index: u32) -> Self { + Self { + input_asset_index, + output_asset_index, + } + } + + pub fn build_witness(&self) -> Box { + Box::new(AssetAuthWitness { + input_asset_index: self.input_asset_index, + output_asset_index: self.output_asset_index, + }) + } +} diff --git a/crates/contracts/src/programs/issuance_factory/core.rs b/crates/contracts/src/programs/issuance_factory/core.rs new file mode 100644 index 0000000..6ca34bd --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/core.rs @@ -0,0 +1,170 @@ +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, PartialOutput, RequiredSignature, UTXO}; +use simplex::{program::Program, provider::SimplicityNetwork}; + +use crate::artifacts::issuance_factory::IssuanceFactoryProgram; +use crate::programs::issuance_factory::{ + IssuanceFactoryError, IssuanceFactoryParameters, IssuanceFactoryWitnessBranch, +}; +use crate::programs::program::SimplexProgram; + +pub struct IssuanceFactory { + program: IssuanceFactoryProgram, + parameters: IssuanceFactoryParameters, +} + +// TODO: encode constants to the factory asset amount +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 { + program: IssuanceFactoryProgram::new(parameters.build_arguments()), + parameters, + } + } + + pub fn try_from_tx( + tx: &Transaction, + provider: &impl ProviderTrait, + ) -> Result { + if tx.output.len() < 2 || !tx.output[1].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 owner_pubkey = + 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, + network: *provider.get_network(), + }; + + Ok(Self::new(issuance_factory_parameters)) + } + + pub fn get_parameters(&self) -> &IssuanceFactoryParameters { + &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, + factory_asset_id: AssetId, + factory_asset_amount: u64, + ) { + self.add_program_output(ft, factory_asset_id, factory_asset_amount); + + let op_return_data = self.encode_creation_op_return_data(); + + ft.add_output(PartialOutput::new( + Script::new_op_return(&op_return_data), + 0, + AssetId::default(), + )); + } + + pub fn attach_assets_issuing( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + program_issuance_input: IssuanceInput, + ) { + let issuance_factory_amount = program_utxo.explicit_amount(); + let issuance_factory_asset = program_utxo.explicit_asset(); + + let issuance_factory_output_index = ft.n_outputs() as u32; + + let issuance_factory_witness_branch = IssuanceFactoryWitnessBranch::IssueAssets { + output_index: issuance_factory_output_index, + }; + + self.add_program_issuance_input_with_signature( + ft, + program_utxo, + program_issuance_input, + issuance_factory_witness_branch.build_witness(), + RequiredSignature::witness_with_path("PATH", &["Left", "1"]), + ); + + self.add_program_output(ft, issuance_factory_asset, issuance_factory_amount); + } + + pub fn attach_factory_removing(&self, ft: &mut FinalTransaction, program_utxo: UTXO) { + let issuance_factory_amount = program_utxo.explicit_amount(); + let issuance_factory_asset = program_utxo.explicit_asset(); + + let issuance_factory_output_index = ft.n_outputs() as u32; + + let issuance_factory_witness_branch = IssuanceFactoryWitnessBranch::RemoveFactory { + output_index: issuance_factory_output_index, + }; + + self.add_program_input_with_signature( + ft, + program_utxo, + issuance_factory_witness_branch.build_witness(), + RequiredSignature::witness_with_path("PATH", &["Right", "1"]), + ); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + issuance_factory_amount, + issuance_factory_asset, + )); + } +} + +impl SimplexProgram for IssuanceFactory { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/issuance_factory/error.rs b/crates/contracts/src/programs/issuance_factory/error.rs new file mode 100644 index 0000000..9103b80 --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/error.rs @@ -0,0 +1,16 @@ +use simplex::simplicityhl::elements::Txid; + +#[derive(thiserror::Error, Debug)] +pub enum IssuanceFactoryError { + #[error("Invalid creation OP_RETURN data length: expected - {expected}, actual - {actual}")] + InvalidCreationOpReturnDataLength { expected: usize, actual: usize }, + + #[error("Invalid OP_RETURN owner pubkey bytes: {0}")] + InvalidOpReturnBytes(String), + + #[error("Confidential assets currently are not supported")] + ConfidentialAssetsAreNotSupported(), + + #[error("Passed transaction is not an issuance factory creation transaction")] + NotAnIssuanceFactoryCreationTx(Txid), +} diff --git a/crates/contracts/src/programs/issuance_factory/mod.rs b/crates/contracts/src/programs/issuance_factory/mod.rs new file mode 100644 index 0000000..91a8787 --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/mod.rs @@ -0,0 +1,9 @@ +mod core; +mod error; +mod params; +mod witness; + +pub use core::IssuanceFactory; +pub use error::IssuanceFactoryError; +pub use params::IssuanceFactoryParameters; +pub use witness::IssuanceFactoryWitnessBranch; diff --git a/crates/contracts/src/programs/issuance_factory/params.rs b/crates/contracts/src/programs/issuance_factory/params.rs new file mode 100644 index 0000000..9c3554e --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/params.rs @@ -0,0 +1,21 @@ +use simplex::{provider::SimplicityNetwork, simplicityhl::elements::schnorr::XOnlyPublicKey}; + +use crate::artifacts::issuance_factory::derived_issuance_factory::IssuanceFactoryArguments; + +#[derive(Debug, Clone, Copy)] +pub struct IssuanceFactoryParameters { + pub issuing_utxos_count: u8, + pub reissuance_flags: u64, + pub owner_pubkey: XOnlyPublicKey, + pub network: SimplicityNetwork, +} + +impl IssuanceFactoryParameters { + pub fn build_arguments(&self) -> IssuanceFactoryArguments { + IssuanceFactoryArguments { + issuing_utxos_count: self.issuing_utxos_count, + reissuance_flags: self.reissuance_flags, + factory_owner_pubkey: self.owner_pubkey.serialize(), + } + } +} diff --git a/crates/contracts/src/programs/issuance_factory/witness.rs b/crates/contracts/src/programs/issuance_factory/witness.rs new file mode 100644 index 0000000..c9b9541 --- /dev/null +++ b/crates/contracts/src/programs/issuance_factory/witness.rs @@ -0,0 +1,27 @@ +use simplex::{ + constants::DUMMY_SIGNATURE, + either::Either::{Left, Right}, +}; + +use crate::artifacts::issuance_factory::derived_issuance_factory::IssuanceFactoryWitness; + +#[derive(Debug, Clone, Copy)] +pub enum IssuanceFactoryWitnessBranch { + IssueAssets { output_index: u32 }, + RemoveFactory { output_index: u32 }, +} + +impl IssuanceFactoryWitnessBranch { + pub fn build_witness(&self) -> Box { + let path = match self { + IssuanceFactoryWitnessBranch::IssueAssets { output_index } => { + Left((*output_index, DUMMY_SIGNATURE)) + } + IssuanceFactoryWitnessBranch::RemoveFactory { output_index } => { + Right((*output_index, DUMMY_SIGNATURE)) + } + }; + + Box::new(IssuanceFactoryWitness { path }) + } +} diff --git a/crates/contracts/src/programs/lending.rs b/crates/contracts/src/programs/lending.rs deleted file mode 100644 index 78be4e4..0000000 --- a/crates/contracts/src/programs/lending.rs +++ /dev/null @@ -1,96 +0,0 @@ -use simplex::either::Either::{Left, Right}; -use simplex::program::Program; -use simplex::provider::SimplicityNetwork; -use simplex::simplicityhl::elements::AssetId; - -use crate::artifacts::lending::LendingProgram; -use crate::artifacts::lending::derived_lending::{LendingArguments, LendingWitness}; -use crate::programs::program::SimplexProgram; -use crate::programs::{AssetAuth, AssetAuthParameters}; -use crate::utils::LendingOfferParameters; - -#[derive(Debug, Clone, Copy)] -pub struct LendingParameters { - pub collateral_asset_id: AssetId, - pub principal_asset_id: AssetId, - pub first_parameters_nft_asset_id: AssetId, - pub second_parameters_nft_asset_id: AssetId, - pub borrower_nft_asset_id: AssetId, - pub lender_nft_asset_id: AssetId, - pub offer_parameters: LendingOfferParameters, - pub network: SimplicityNetwork, -} - -impl From for LendingArguments { - fn from(value: LendingParameters) -> Self { - let lender_principal_asset_auth = value.get_lender_principal_asset_auth(); - - Self { - collateral_asset_id: value.collateral_asset_id.into_inner().0, - principal_asset_id: value.principal_asset_id.into_inner().0, - first_parameters_nft_asset_id: value.first_parameters_nft_asset_id.into_inner().0, - second_parameters_nft_asset_id: value.second_parameters_nft_asset_id.into_inner().0, - borrower_nft_asset_id: value.borrower_nft_asset_id.into_inner().0, - lender_nft_asset_id: value.lender_nft_asset_id.into_inner().0, - collateral_amount: value.offer_parameters.collateral_amount, - principal_amount: value.offer_parameters.principal_amount, - principal_interest_rate: value.offer_parameters.principal_interest_rate, - loan_expiration_time: value.offer_parameters.loan_expiration_time, - lender_principal_cov_hash: lender_principal_asset_auth.get_script_hash(), - } - } -} - -impl LendingParameters { - pub fn get_lender_principal_asset_auth(&self) -> AssetAuth { - AssetAuth::new(AssetAuthParameters { - asset_id: self.lender_nft_asset_id, - asset_amount: 1, - with_asset_burn: true, - network: self.network, - }) - } -} - -pub struct Lending { - program: LendingProgram, - parameters: LendingParameters, -} - -#[derive(Debug, Clone, Copy)] -pub enum LendingBranch { - LoanRepayment, - LoanLiquidation, -} - -impl Lending { - pub fn new(parameters: LendingParameters) -> Self { - Self { - program: LendingProgram::new(LendingArguments::from(parameters)), - parameters, - } - } - - pub fn get_lending_witness(witness_branch: &LendingBranch) -> LendingWitness { - let path = match witness_branch { - LendingBranch::LoanRepayment => Left(()), - LendingBranch::LoanLiquidation => Right(()), - }; - - LendingWitness { path } - } - - pub fn get_lending_parameters(&self) -> &LendingParameters { - &self.parameters - } -} - -impl SimplexProgram for Lending { - fn get_program(&self) -> &Program { - self.program.get_program() - } - - fn get_network(&self) -> &SimplicityNetwork { - &self.parameters.network - } -} diff --git a/crates/contracts/src/programs/lending/core.rs b/crates/contracts/src/programs/lending/core.rs new file mode 100644 index 0000000..34c554d --- /dev/null +++ b/crates/contracts/src/programs/lending/core.rs @@ -0,0 +1,252 @@ +use simplex::{ + program::Program, + provider::{ProviderTrait, SimplicityNetwork}, + simplicityhl::elements::{LockTime, Script, Sequence, Transaction}, + transaction::{FinalTransaction, PartialInput, PartialOutput, UTXO}, +}; + +use crate::programs::{ + lending::{LendingError, LendingParameters, LendingWitnessBranch}, + program::SimplexProgram, + script_auth::{ScriptAuth, ScriptAuthWitnessParams}, +}; +use crate::{ + artifacts::lending::LendingProgram, + utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}, +}; + +pub struct Lending { + program: LendingProgram, + parameters: LendingParameters, +} + +impl Lending { + pub fn new(parameters: LendingParameters) -> Self { + Self { + program: LendingProgram::new(parameters.build_arguments()), + parameters, + } + } + + pub fn try_from_tx( + tx: &Transaction, + provider: &impl ProviderTrait, + ) -> Result { + if tx.input.len() < 7 || tx.output.len() < 7 { + return Err(LendingError::NotALendingCreationTx(tx.txid())); + } + + let collateral_asset_id = tx.output[0] + .asset + .explicit() + .ok_or_else(LendingError::ConfidentialAssetsAreNotSupported)?; + let first_parameters_nft_asset_id = tx.output[1] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let second_parameters_nft_asset_id = tx.output[2] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let borrower_nft_asset_id = tx.output[3] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let lender_nft_asset_id = tx.output[4] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let principal_asset_id = tx.output[5] + .asset + .explicit() + .ok_or_else(LendingError::ConfidentialAssetsAreNotSupported)?; + + let first_parameters_nft_amount = tx.output[1] + .value + .explicit() + .expect("Parameter NFT must have explicit amount"); + let second_parameters_nft_amount = tx.output[2] + .value + .explicit() + .expect("Parameter NFT must have explicit amount"); + + let offer_parameters = LendingOfferParameters::build_from_parameters_nfts( + &FirstNFTParameters::decode(first_parameters_nft_amount), + &SecondNFTParameters::decode(second_parameters_nft_amount), + ); + + let lending_parameters = LendingParameters { + collateral_asset_id, + principal_asset_id, + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + offer_parameters, + network: *provider.get_network(), + }; + + Ok(Self::new(lending_parameters)) + } + + pub fn get_parameters(&self) -> &LendingParameters { + &self.parameters + } + + pub fn attach_creation( + &self, + ft: &mut FinalTransaction, + first_parameters_nft_utxo: UTXO, + second_parameters_nft_utxo: UTXO, + ) { + self.add_program_output( + ft, + self.parameters.collateral_asset_id, + self.parameters.offer_parameters.collateral_amount, + ); + + let parameter_nfts_script_auth = ScriptAuth::from_simplex_program(self); + let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); + let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); + + parameter_nfts_script_auth.attach_creation( + ft, + self.parameters.first_parameters_nft_asset_id, + first_parameters_nft_amount, + ); + parameter_nfts_script_auth.attach_creation( + ft, + self.parameters.second_parameters_nft_asset_id, + second_parameters_nft_amount, + ); + } + + pub fn attach_loan_repayment( + &self, + ft: &mut FinalTransaction, + lending_utxo: UTXO, + first_parameters_nft_utxo: UTXO, + second_parameters_nft_utxo: UTXO, + ) { + let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); + let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); + let lending_input_index = ft.n_inputs() as u32; + + self.add_program_input( + ft, + lending_utxo, + LendingWitnessBranch::LoanRepayment.build_witness(), + ); + + let parameters_script_auth = ScriptAuth::from_simplex_program(self); + let parameters_script_auth_witness = ScriptAuthWitnessParams::new(lending_input_index); + + parameters_script_auth.attach_unlocking( + ft, + first_parameters_nft_utxo, + parameters_script_auth_witness, + ); + parameters_script_auth.attach_unlocking( + ft, + second_parameters_nft_utxo, + parameters_script_auth_witness, + ); + + let principal_with_interest = self + .parameters + .offer_parameters + .calculate_principal_with_interest(); + let lender_principal_asset_auth = self.parameters.get_lender_principal_asset_auth(); + + lender_principal_asset_auth.add_program_output( + ft, + self.parameters.principal_asset_id, + principal_with_interest, + ); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + first_parameters_nft_amount, + self.parameters.first_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + second_parameters_nft_amount, + self.parameters.second_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + 1, + self.parameters.borrower_nft_asset_id, + )); + } + + pub fn attach_loan_liquidation( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + first_parameters_nft_utxo: UTXO, + second_parameters_nft_utxo: UTXO, + ) { + let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); + let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); + let lending_input_index = ft.n_inputs() as u32; + + let locktime = + LockTime::from_height(self.parameters.offer_parameters.loan_expiration_time).unwrap(); + + let lending_input = PartialInput::new(program_utxo) + .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) + .with_locktime(locktime); + + self.add_program_input_from_partial_input( + ft, + lending_input, + LendingWitnessBranch::LoanLiquidation.build_witness(), + ); + + let parameters_script_auth = ScriptAuth::from_simplex_program(self); + let parameters_script_auth_witness = ScriptAuthWitnessParams::new(lending_input_index); + + parameters_script_auth.attach_unlocking( + ft, + first_parameters_nft_utxo, + parameters_script_auth_witness, + ); + parameters_script_auth.attach_unlocking( + ft, + second_parameters_nft_utxo, + parameters_script_auth_witness, + ); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + first_parameters_nft_amount, + self.parameters.first_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + second_parameters_nft_amount, + self.parameters.second_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + 1, + self.parameters.lender_nft_asset_id, + )); + } +} + +impl SimplexProgram for Lending { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/lending/error.rs b/crates/contracts/src/programs/lending/error.rs new file mode 100644 index 0000000..0885b78 --- /dev/null +++ b/crates/contracts/src/programs/lending/error.rs @@ -0,0 +1,10 @@ +use simplex::simplicityhl::elements::Txid; + +#[derive(thiserror::Error, Debug)] +pub enum LendingError { + #[error("Confidential assets currently are not supported")] + ConfidentialAssetsAreNotSupported(), + + #[error("Passed transaction is not a lending creation transaction")] + NotALendingCreationTx(Txid), +} diff --git a/crates/contracts/src/programs/lending/mod.rs b/crates/contracts/src/programs/lending/mod.rs new file mode 100644 index 0000000..812d4b2 --- /dev/null +++ b/crates/contracts/src/programs/lending/mod.rs @@ -0,0 +1,9 @@ +mod core; +mod error; +mod params; +mod witness; + +pub use core::Lending; +pub use error::LendingError; +pub use params::LendingParameters; +pub use witness::LendingWitnessBranch; diff --git a/crates/contracts/src/programs/lending/params.rs b/crates/contracts/src/programs/lending/params.rs new file mode 100644 index 0000000..fefbcbe --- /dev/null +++ b/crates/contracts/src/programs/lending/params.rs @@ -0,0 +1,51 @@ +use simplex::{provider::SimplicityNetwork, simplicityhl::elements::AssetId}; + +use crate::{ + artifacts::lending::derived_lending::LendingArguments, + programs::{ + asset_auth::{AssetAuth, AssetAuthParameters}, + program::SimplexProgram, + }, + utils::LendingOfferParameters, +}; + +#[derive(Debug, Clone, Copy)] +pub struct LendingParameters { + pub collateral_asset_id: AssetId, + pub principal_asset_id: AssetId, + pub first_parameters_nft_asset_id: AssetId, + pub second_parameters_nft_asset_id: AssetId, + pub borrower_nft_asset_id: AssetId, + pub lender_nft_asset_id: AssetId, + pub offer_parameters: LendingOfferParameters, + pub network: SimplicityNetwork, +} + +impl LendingParameters { + pub fn get_lender_principal_asset_auth(&self) -> AssetAuth { + AssetAuth::new(AssetAuthParameters { + asset_id: self.lender_nft_asset_id, + asset_amount: 1, + with_asset_burn: true, + network: self.network, + }) + } + + pub fn build_arguments(&self) -> LendingArguments { + let lender_principal_asset_auth = self.get_lender_principal_asset_auth(); + + LendingArguments { + collateral_asset_id: self.collateral_asset_id.into_inner().0, + principal_asset_id: self.principal_asset_id.into_inner().0, + first_parameters_nft_asset_id: self.first_parameters_nft_asset_id.into_inner().0, + second_parameters_nft_asset_id: self.second_parameters_nft_asset_id.into_inner().0, + borrower_nft_asset_id: self.borrower_nft_asset_id.into_inner().0, + lender_nft_asset_id: self.lender_nft_asset_id.into_inner().0, + collateral_amount: self.offer_parameters.collateral_amount, + principal_amount: self.offer_parameters.principal_amount, + principal_interest_rate: self.offer_parameters.principal_interest_rate, + loan_expiration_time: self.offer_parameters.loan_expiration_time, + lender_principal_cov_hash: lender_principal_asset_auth.get_script_hash(), + } + } +} diff --git a/crates/contracts/src/programs/lending/witness.rs b/crates/contracts/src/programs/lending/witness.rs new file mode 100644 index 0000000..55156d7 --- /dev/null +++ b/crates/contracts/src/programs/lending/witness.rs @@ -0,0 +1,20 @@ +use simplex::either::Either::{Left, Right}; + +use crate::artifacts::lending::derived_lending::LendingWitness; + +#[derive(Debug, Clone, Copy)] +pub enum LendingWitnessBranch { + LoanRepayment, + LoanLiquidation, +} + +impl LendingWitnessBranch { + pub fn build_witness(&self) -> Box { + let path = match self { + LendingWitnessBranch::LoanRepayment => Left(()), + LendingWitnessBranch::LoanLiquidation => Right(()), + }; + + Box::new(LendingWitness { path }) + } +} diff --git a/crates/contracts/src/programs/mod.rs b/crates/contracts/src/programs/mod.rs index 469ef52..94aa627 100644 --- a/crates/contracts/src/programs/mod.rs +++ b/crates/contracts/src/programs/mod.rs @@ -1,12 +1,7 @@ pub mod asset_auth; +pub mod issuance_factory; pub mod lending; pub mod ownable_script_auth; pub mod pre_lock; pub mod program; pub mod script_auth; - -pub use asset_auth::*; -pub use lending::*; -pub use ownable_script_auth::*; -pub use pre_lock::*; -pub use script_auth::*; diff --git a/crates/contracts/src/programs/ownable_script_auth.rs b/crates/contracts/src/programs/ownable_script_auth.rs deleted file mode 100644 index 1f4c78c..0000000 --- a/crates/contracts/src/programs/ownable_script_auth.rs +++ /dev/null @@ -1,103 +0,0 @@ -use simplex::constants::DUMMY_SIGNATURE; -use simplex::either::Either::{Left, Right}; -use simplex::program::Program; -use simplex::provider::SimplicityNetwork; -use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - -use crate::artifacts::ownable_script_auth::OwnableScriptAuthProgram; -use crate::artifacts::ownable_script_auth::derived_ownable_script_auth::{ - OwnableScriptAuthArguments, OwnableScriptAuthWitness, -}; -use crate::programs::program::SimplexProgram; - -#[derive(Debug, Clone, Copy)] -pub struct OwnableScriptAuthParameters { - pub script_hash: [u8; 32], - pub owner_pubkey: XOnlyPublicKey, - pub network: SimplicityNetwork, -} - -impl From for OwnableScriptAuthArguments { - fn from(value: OwnableScriptAuthParameters) -> Self { - Self { - script_hash: value.script_hash, - } - } -} - -pub struct OwnableScriptAuth { - program: OwnableScriptAuthProgram, - parameters: OwnableScriptAuthParameters, -} - -#[derive(Debug, Clone, Copy)] -pub enum OwnableScriptAuthBranch { - OwnershipTransfer { - current_owner: XOnlyPublicKey, - new_owner: XOnlyPublicKey, - program_output_index: u32, - }, - ScriptAuthUnlock { - owner: XOnlyPublicKey, - input_script_index: u32, - }, -} - -impl OwnableScriptAuth { - pub fn new(parameters: OwnableScriptAuthParameters) -> Self { - let mut program = - OwnableScriptAuthProgram::new(OwnableScriptAuthArguments::from(parameters)) - .with_storage_capacity(1); - - program.set_storage_at(0, parameters.owner_pubkey.serialize()); - - Self { - program, - parameters, - } - } - - pub fn get_ownable_script_auth_witness( - witness_branch: &OwnableScriptAuthBranch, - ) -> OwnableScriptAuthWitness { - let path = match witness_branch { - OwnableScriptAuthBranch::OwnershipTransfer { - current_owner, - new_owner, - program_output_index, - } => Left(( - current_owner.serialize(), - new_owner.serialize(), - *program_output_index, - )), - OwnableScriptAuthBranch::ScriptAuthUnlock { - owner, - input_script_index, - } => Right((owner.serialize(), *input_script_index)), - }; - - OwnableScriptAuthWitness { - path, - signature: DUMMY_SIGNATURE, - } - } - - pub fn apply_ownership_transfer(&mut self, new_owner: XOnlyPublicKey) { - self.program.set_storage_at(0, new_owner.serialize()); - self.parameters.owner_pubkey = new_owner; - } - - pub fn get_ownable_script_auth_parameters(&self) -> &OwnableScriptAuthParameters { - &self.parameters - } -} - -impl SimplexProgram for OwnableScriptAuth { - fn get_program(&self) -> &Program { - self.program.get_program() - } - - fn get_network(&self) -> &SimplicityNetwork { - &self.parameters.network - } -} diff --git a/crates/contracts/src/programs/ownable_script_auth/core.rs b/crates/contracts/src/programs/ownable_script_auth/core.rs new file mode 100644 index 0000000..8774638 --- /dev/null +++ b/crates/contracts/src/programs/ownable_script_auth/core.rs @@ -0,0 +1,119 @@ +use simplex::program::Program; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::{AssetId, Script}; +use simplex::transaction::{FinalTransaction, PartialOutput, RequiredSignature, UTXO}; + +use crate::artifacts::ownable_script_auth::OwnableScriptAuthProgram; + +use crate::programs::ownable_script_auth::{ + OwnableScriptAuthParameters, OwnableScriptAuthWitnessBranch, +}; +use crate::programs::program::SimplexProgram; + +pub struct OwnableScriptAuth { + program: OwnableScriptAuthProgram, + parameters: OwnableScriptAuthParameters, +} + +impl OwnableScriptAuth { + pub fn new(parameters: OwnableScriptAuthParameters) -> Self { + let mut program = + OwnableScriptAuthProgram::new(parameters.build_arguments()).with_storage_capacity(1); + + program.set_storage_at(0, parameters.owner_pubkey.serialize()); + + Self { + program, + parameters, + } + } + + pub fn get_parameters(&self) -> &OwnableScriptAuthParameters { + &self.parameters + } + + pub fn attach_creation( + &self, + ft: &mut FinalTransaction, + asset_id_to_lock: AssetId, + amount_to_lock: u64, + ) { + self.add_program_output(ft, asset_id_to_lock, amount_to_lock); + + ft.add_output(PartialOutput::new( + Script::new_op_return(self.parameters.owner_pubkey.serialize().as_slice()), + 0, + AssetId::default(), + )); + } + + pub fn attach_ownership_transfer( + &mut self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + new_owner: XOnlyPublicKey, + ) { + let outputs_count = ft.n_outputs() as u32; + + let witness_branch = OwnableScriptAuthWitnessBranch::OwnershipTransfer { + current_owner: self.parameters.owner_pubkey, + new_owner, + program_output_index: outputs_count, + }; + + let locked_asset = program_utxo.explicit_asset(); + let locked_amount = program_utxo.explicit_amount(); + + self.add_program_input_with_signature( + ft, + program_utxo, + witness_branch.build_witness(), + RequiredSignature::witness_with_path("PATH", &["Left", "2"]), + ); + + self.apply_ownership_transfer(new_owner); + + self.add_program_output(ft, locked_asset, locked_amount); + + ft.add_output(PartialOutput::new( + Script::new_op_return(new_owner.serialize().as_slice()), + 0, + AssetId::default(), + )); + } + + pub fn attach_unlocking( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + auth_input_index: u32, + ) { + let witness_branch = OwnableScriptAuthWitnessBranch::ScriptAuthUnlock { + owner: self.parameters.owner_pubkey, + input_script_index: auth_input_index, + }; + + self.add_program_input_with_signature( + ft, + program_utxo.clone(), + witness_branch.build_witness(), + RequiredSignature::witness_with_path("PATH", &["Right", "1"]), + ); + } + + fn apply_ownership_transfer(&mut self, new_owner: XOnlyPublicKey) { + self.program.set_storage_at(0, new_owner.serialize()); + self.parameters.owner_pubkey = new_owner; + } +} + +impl SimplexProgram for OwnableScriptAuth { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/ownable_script_auth/mod.rs b/crates/contracts/src/programs/ownable_script_auth/mod.rs new file mode 100644 index 0000000..84d5873 --- /dev/null +++ b/crates/contracts/src/programs/ownable_script_auth/mod.rs @@ -0,0 +1,7 @@ +mod core; +mod params; +mod witness; + +pub use core::OwnableScriptAuth; +pub use params::OwnableScriptAuthParameters; +pub use witness::OwnableScriptAuthWitnessBranch; diff --git a/crates/contracts/src/programs/ownable_script_auth/params.rs b/crates/contracts/src/programs/ownable_script_auth/params.rs new file mode 100644 index 0000000..4dcc162 --- /dev/null +++ b/crates/contracts/src/programs/ownable_script_auth/params.rs @@ -0,0 +1,18 @@ +use simplex::{provider::SimplicityNetwork, simplicityhl::elements::schnorr::XOnlyPublicKey}; + +use crate::artifacts::ownable_script_auth::derived_ownable_script_auth::OwnableScriptAuthArguments; + +#[derive(Debug, Clone, Copy)] +pub struct OwnableScriptAuthParameters { + pub script_hash: [u8; 32], + pub owner_pubkey: XOnlyPublicKey, + pub network: SimplicityNetwork, +} + +impl OwnableScriptAuthParameters { + pub fn build_arguments(&self) -> OwnableScriptAuthArguments { + OwnableScriptAuthArguments { + script_hash: self.script_hash, + } + } +} diff --git a/crates/contracts/src/programs/ownable_script_auth/witness.rs b/crates/contracts/src/programs/ownable_script_auth/witness.rs new file mode 100644 index 0000000..7b9fe4f --- /dev/null +++ b/crates/contracts/src/programs/ownable_script_auth/witness.rs @@ -0,0 +1,43 @@ +use simplex::{ + constants::DUMMY_SIGNATURE, + either::Either::{Left, Right}, + simplicityhl::elements::schnorr::XOnlyPublicKey, +}; + +use crate::artifacts::ownable_script_auth::derived_ownable_script_auth::OwnableScriptAuthWitness; + +#[derive(Debug, Clone, Copy)] +pub enum OwnableScriptAuthWitnessBranch { + OwnershipTransfer { + current_owner: XOnlyPublicKey, + new_owner: XOnlyPublicKey, + program_output_index: u32, + }, + ScriptAuthUnlock { + owner: XOnlyPublicKey, + input_script_index: u32, + }, +} + +impl OwnableScriptAuthWitnessBranch { + pub fn build_witness(&self) -> Box { + let path = match self { + OwnableScriptAuthWitnessBranch::OwnershipTransfer { + current_owner, + new_owner, + program_output_index, + } => Left(( + current_owner.serialize(), + new_owner.serialize(), + DUMMY_SIGNATURE, + *program_output_index, + )), + OwnableScriptAuthWitnessBranch::ScriptAuthUnlock { + owner, + input_script_index, + } => Right((owner.serialize(), DUMMY_SIGNATURE, *input_script_index)), + }; + + Box::new(OwnableScriptAuthWitness { path }) + } +} diff --git a/crates/contracts/src/programs/pre_lock.rs b/crates/contracts/src/programs/pre_lock.rs deleted file mode 100644 index acad02e..0000000 --- a/crates/contracts/src/programs/pre_lock.rs +++ /dev/null @@ -1,171 +0,0 @@ -use simplex::constants::DUMMY_SIGNATURE; -use simplex::either::Either::{Left, Right}; -use simplex::program::Program; - -use simplex::provider::SimplicityNetwork; -use simplex::simplicityhl::elements::AssetId; -use simplex::simplicityhl::elements::hashes::FromSliceError; -use simplex::simplicityhl::elements::hex::ToHex; -use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; - -use crate::artifacts::pre_lock::PreLockProgram; -use crate::artifacts::pre_lock::derived_pre_lock::{PreLockArguments, PreLockWitness}; -use crate::programs::program::SimplexProgram; -use crate::programs::{Lending, LendingParameters, ScriptAuth}; -use crate::utils::LendingOfferParameters; - -#[derive(Debug, Clone, Copy)] -pub struct PreLockParameters { - pub collateral_asset_id: AssetId, - pub principal_asset_id: AssetId, - pub first_parameters_nft_asset_id: AssetId, - pub second_parameters_nft_asset_id: AssetId, - pub borrower_nft_asset_id: AssetId, - pub lender_nft_asset_id: AssetId, - pub offer_parameters: LendingOfferParameters, - pub borrower_pubkey: XOnlyPublicKey, - pub borrower_output_script_hash: [u8; 32], - pub network: SimplicityNetwork, -} - -impl From for LendingParameters { - fn from(value: PreLockParameters) -> Self { - LendingParameters::from(&value) - } -} - -impl From<&PreLockParameters> for LendingParameters { - fn from(value: &PreLockParameters) -> Self { - Self { - collateral_asset_id: value.collateral_asset_id, - principal_asset_id: value.principal_asset_id, - first_parameters_nft_asset_id: value.first_parameters_nft_asset_id, - second_parameters_nft_asset_id: value.second_parameters_nft_asset_id, - borrower_nft_asset_id: value.borrower_nft_asset_id, - lender_nft_asset_id: value.lender_nft_asset_id, - offer_parameters: value.offer_parameters, - network: value.network, - } - } -} - -impl From for PreLockArguments { - fn from(value: PreLockParameters) -> Self { - let parameter_nfts_script_auth = value.get_parameter_nfts_script_auth(); - - Self { - collateral_asset_id: value.collateral_asset_id.into_inner().0, - principal_asset_id: value.principal_asset_id.into_inner().0, - first_parameters_nft_asset_id: value.first_parameters_nft_asset_id.into_inner().0, - second_parameters_nft_asset_id: value.second_parameters_nft_asset_id.into_inner().0, - borrower_nft_asset_id: value.borrower_nft_asset_id.into_inner().0, - lender_nft_asset_id: value.lender_nft_asset_id.into_inner().0, - collateral_amount: value.offer_parameters.collateral_amount, - principal_amount: value.offer_parameters.principal_amount, - principal_interest_rate: value.offer_parameters.principal_interest_rate, - loan_expiration_time: value.offer_parameters.loan_expiration_time, - borrower_pub_key: value.borrower_pubkey.serialize(), - lending_cov_hash: parameter_nfts_script_auth - .get_script_auth_parameters() - .script_hash, - parameters_nft_output_script_hash: parameter_nfts_script_auth.get_script_hash(), - borrower_nft_output_script_hash: value.borrower_output_script_hash, - principal_output_script_hash: value.borrower_output_script_hash, - } - } -} - -impl PreLockParameters { - pub fn get_parameter_nfts_script_auth(&self) -> ScriptAuth { - let lending = Lending::new(self.into()); - - ScriptAuth::from_simplex_program(&lending) - } -} - -pub struct PreLock { - program: PreLockProgram, - parameters: PreLockParameters, -} - -#[derive(Debug, Clone, Copy)] -pub enum PreLockBranch { - LendingCreation, - PreLockCancellation, -} - -#[derive(thiserror::Error, Debug)] -pub enum PreLockError { - #[error("Invalid creation OP_RETURN data length: expected - {expected}, actual - {actual}")] - InvalidCreationOpReturnDataLength { expected: usize, actual: usize }, - - #[error("Invalid OP_RETURN borrower pubkey bytes: {0}")] - InvalidOpReturnBytes(String), - - #[error("Failed to convert OP_RETURN asset id bytes to valid asset id: {0}")] - FromSlice(#[from] FromSliceError), -} - -pub const PRE_LOCK_CREATION_OP_RETURN_DATA_LENGTH: usize = 64; - -impl PreLock { - pub fn new(parameters: PreLockParameters) -> Self { - Self { - program: PreLockProgram::new(PreLockArguments::from(parameters)), - parameters, - } - } - - pub fn get_pre_lock_witness(witness_branch: &PreLockBranch) -> PreLockWitness { - let path = match witness_branch { - PreLockBranch::LendingCreation => Left(()), - PreLockBranch::PreLockCancellation => Right(()), - }; - - PreLockWitness { - path, - signature: DUMMY_SIGNATURE, - } - } - - 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 get_pre_lock_parameters(&self) -> &PreLockParameters { - &self.parameters - } -} - -impl SimplexProgram for PreLock { - fn get_program(&self) -> &Program { - self.program.get_program() - } - - fn get_network(&self) -> &SimplicityNetwork { - &self.parameters.network - } -} diff --git a/crates/contracts/src/programs/pre_lock/core.rs b/crates/contracts/src/programs/pre_lock/core.rs new file mode 100644 index 0000000..0411d36 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/core.rs @@ -0,0 +1,295 @@ +use simplex::program::Program; + +use simplex::provider::{ProviderTrait, SimplicityNetwork}; +use simplex::simplicityhl::elements::{ + AssetId, Script, Transaction, hex::ToHex, secp256k1_zkp::XOnlyPublicKey, +}; +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::script_auth::{ScriptAuth, ScriptAuthWitnessParams}; +use crate::utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}; + +pub struct PreLock { + program: PreLockProgram, + parameters: PreLockParameters, +} + +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 { + Self { + program: PreLockProgram::new(parameters.build_arguments()), + parameters, + } + } + + pub fn try_from_tx( + tx: &Transaction, + provider: &impl ProviderTrait, + ) -> Result { + if tx.input.len() < 5 || tx.output.len() < 7 || !tx.output[5].is_null_data() { + return Err(PreLockError::NotAPreLockCreationTx(tx.txid())); + } + + let collateral_asset_id = tx.output[0] + .asset + .explicit() + .ok_or_else(PreLockError::ConfidentialAssetsAreNotSupported)?; + let first_parameters_nft_asset_id = tx.output[1] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let second_parameters_nft_asset_id = tx.output[2] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let borrower_nft_asset_id = tx.output[3] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + let lender_nft_asset_id = tx.output[4] + .asset + .explicit() + .expect("Utility NFT must be explicit"); + + let first_parameters_nft_amount = tx.output[1] + .value + .explicit() + .expect("Parameter NFT must have explicit amount"); + let second_parameters_nft_amount = tx.output[2] + .value + .explicit() + .expect("Parameter NFT must have explicit amount"); + + let offer_parameters = LendingOfferParameters::build_from_parameters_nfts( + &FirstNFTParameters::decode(first_parameters_nft_amount), + &SecondNFTParameters::decode(second_parameters_nft_amount), + ); + + let prev_collateral_outpoint = tx.input[0].previous_output; + let pre_collateral_tx = provider.fetch_transaction(&prev_collateral_outpoint.txid)?; + let collateral_script_hash = hash_script( + &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 (borrower_pubkey, principal_asset_id) = + PreLock::decode_creation_op_return_data(op_return_bytes.to_vec()).unwrap(); + + let pre_lock_parameters = PreLockParameters { + collateral_asset_id, + 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_output_script_hash: collateral_script_hash, + network: *provider.get_network(), + }; + + Ok(Self::new(pre_lock_parameters)) + } + + pub fn get_parameters(&self) -> &PreLockParameters { + &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 + .offer_parameters + .encode_parameters_nft_amounts(parameter_amounts_decimals) + .expect("Invalid offer parameters"); + + self.add_program_output( + ft, + self.parameters.collateral_asset_id, + self.parameters.offer_parameters.collateral_amount, + ); + + let utility_nfts_script_auth = ScriptAuth::from_simplex_program(self); + utility_nfts_script_auth.attach_creation( + ft, + self.parameters.first_parameters_nft_asset_id, + first_parameters_nft_amount, + ); + utility_nfts_script_auth.attach_creation( + ft, + self.parameters.second_parameters_nft_asset_id, + second_parameters_nft_amount, + ); + 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(); + + ft.add_output(PartialOutput::new( + Script::new_op_return(&op_return_data), + 0, + AssetId::default(), + )); + } + + pub fn attach_lending_creation( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + first_parameters_nft_utxo: UTXO, + second_parameters_nft_utxo: UTXO, + borrower_nft_utxo: UTXO, + lender_nft_utxo: UTXO, + ) { + let pre_lock_input_index = ft.n_inputs() as u32; + + self.add_program_input( + ft, + program_utxo, + PreLockWitnessBranch::LendingCreation.build_witness(), + ); + + let utility_nfts_script_auth = ScriptAuth::from_simplex_program(self); + let utility_nfts_witness_params = ScriptAuthWitnessParams::new(pre_lock_input_index); + + utility_nfts_script_auth.attach_unlocking( + ft, + first_parameters_nft_utxo.clone(), + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking( + ft, + second_parameters_nft_utxo.clone(), + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking( + ft, + borrower_nft_utxo, + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking(ft, lender_nft_utxo, utility_nfts_witness_params); + + let lending = Lending::new(self.parameters.into()); + + lending.attach_creation(ft, first_parameters_nft_utxo, second_parameters_nft_utxo); + } + + pub fn attach_cancellation( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + first_parameters_nft_utxo: UTXO, + second_parameters_nft_utxo: UTXO, + borrower_nft_utxo: UTXO, + lender_nft_utxo: UTXO, + ) { + let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); + let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); + let pre_lock_input_index = ft.n_inputs() as u32; + + self.add_program_input_with_signature( + ft, + program_utxo, + PreLockWitnessBranch::PreLockCancellation.build_witness(), + RequiredSignature::witness_with_path("PATH", &["Right"]), + ); + + let utility_nfts_script_auth = ScriptAuth::from_simplex_program(self); + let utility_nfts_witness_params = ScriptAuthWitnessParams::new(pre_lock_input_index); + + utility_nfts_script_auth.attach_unlocking( + ft, + first_parameters_nft_utxo, + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking( + ft, + second_parameters_nft_utxo, + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking( + ft, + borrower_nft_utxo, + utility_nfts_witness_params, + ); + utility_nfts_script_auth.attach_unlocking(ft, lender_nft_utxo, utility_nfts_witness_params); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + first_parameters_nft_amount, + self.parameters.first_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + second_parameters_nft_amount, + self.parameters.second_parameters_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + 1, + self.parameters.borrower_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + 1, + self.parameters.lender_nft_asset_id, + )); + } +} + +impl SimplexProgram for PreLock { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/pre_lock/error.rs b/crates/contracts/src/programs/pre_lock/error.rs new file mode 100644 index 0000000..224ed21 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/error.rs @@ -0,0 +1,25 @@ +use simplex::{ + provider::ProviderError, + simplicityhl::elements::{Txid, hashes::FromSliceError}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum PreLockError { + #[error("Invalid creation OP_RETURN data length: expected - {expected}, actual - {actual}")] + InvalidCreationOpReturnDataLength { expected: usize, actual: usize }, + + #[error("Invalid OP_RETURN borrower pubkey bytes: {0}")] + InvalidOpReturnBytes(String), + + #[error("Confidential assets currently are not supported")] + ConfidentialAssetsAreNotSupported(), + + #[error("Passed transaction is not a pre lock creation transaction")] + NotAPreLockCreationTx(Txid), + + #[error("Failed to convert OP_RETURN asset id bytes to valid asset id: {0}")] + FromSlice(#[from] FromSliceError), + + #[error(transparent)] + SimplexProvider(#[from] ProviderError), +} diff --git a/crates/contracts/src/programs/pre_lock/mod.rs b/crates/contracts/src/programs/pre_lock/mod.rs new file mode 100644 index 0000000..5bbf0f4 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/mod.rs @@ -0,0 +1,9 @@ +mod core; +mod error; +mod params; +mod witness; + +pub use core::{PreLock, UTILITY_NFTS_COUNT}; +pub use error::PreLockError; +pub use params::PreLockParameters; +pub use witness::PreLockWitnessBranch; diff --git a/crates/contracts/src/programs/pre_lock/params.rs b/crates/contracts/src/programs/pre_lock/params.rs new file mode 100644 index 0000000..637a629 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/params.rs @@ -0,0 +1,77 @@ +use simplex::{ + provider::SimplicityNetwork, + simplicityhl::elements::{AssetId, schnorr::XOnlyPublicKey}, +}; + +use crate::{ + artifacts::pre_lock::derived_pre_lock::PreLockArguments, + programs::lending::{Lending, LendingParameters}, + programs::program::SimplexProgram, + programs::script_auth::ScriptAuth, + utils::LendingOfferParameters, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PreLockParameters { + pub collateral_asset_id: AssetId, + pub principal_asset_id: AssetId, + pub first_parameters_nft_asset_id: AssetId, + pub second_parameters_nft_asset_id: AssetId, + pub borrower_nft_asset_id: AssetId, + pub lender_nft_asset_id: AssetId, + pub offer_parameters: LendingOfferParameters, + pub borrower_pubkey: XOnlyPublicKey, + pub borrower_output_script_hash: [u8; 32], + pub network: SimplicityNetwork, +} + +impl From for LendingParameters { + fn from(value: PreLockParameters) -> Self { + LendingParameters::from(&value) + } +} + +impl From<&PreLockParameters> for LendingParameters { + fn from(value: &PreLockParameters) -> Self { + Self { + collateral_asset_id: value.collateral_asset_id, + principal_asset_id: value.principal_asset_id, + first_parameters_nft_asset_id: value.first_parameters_nft_asset_id, + second_parameters_nft_asset_id: value.second_parameters_nft_asset_id, + borrower_nft_asset_id: value.borrower_nft_asset_id, + lender_nft_asset_id: value.lender_nft_asset_id, + offer_parameters: value.offer_parameters, + network: value.network, + } + } +} + +impl PreLockParameters { + pub fn get_parameter_nfts_script_auth(&self) -> ScriptAuth { + let lending = Lending::new(self.into()); + + ScriptAuth::from_simplex_program(&lending) + } + + pub fn build_arguments(&self) -> PreLockArguments { + let parameter_nfts_script_auth = self.get_parameter_nfts_script_auth(); + + PreLockArguments { + collateral_asset_id: self.collateral_asset_id.into_inner().0, + principal_asset_id: self.principal_asset_id.into_inner().0, + first_parameters_nft_asset_id: self.first_parameters_nft_asset_id.into_inner().0, + second_parameters_nft_asset_id: self.second_parameters_nft_asset_id.into_inner().0, + borrower_nft_asset_id: self.borrower_nft_asset_id.into_inner().0, + lender_nft_asset_id: self.lender_nft_asset_id.into_inner().0, + collateral_amount: self.offer_parameters.collateral_amount, + principal_amount: self.offer_parameters.principal_amount, + principal_interest_rate: self.offer_parameters.principal_interest_rate, + loan_expiration_time: self.offer_parameters.loan_expiration_time, + borrower_pub_key: self.borrower_pubkey.serialize(), + lending_cov_hash: parameter_nfts_script_auth.get_parameters().script_hash, + parameters_nft_output_script_hash: parameter_nfts_script_auth.get_script_hash(), + borrower_nft_output_script_hash: self.borrower_output_script_hash, + principal_output_script_hash: self.borrower_output_script_hash, + } + } +} diff --git a/crates/contracts/src/programs/pre_lock/witness.rs b/crates/contracts/src/programs/pre_lock/witness.rs new file mode 100644 index 0000000..b5c4026 --- /dev/null +++ b/crates/contracts/src/programs/pre_lock/witness.rs @@ -0,0 +1,23 @@ +use simplex::{ + constants::DUMMY_SIGNATURE, + either::Either::{Left, Right}, +}; + +use crate::artifacts::pre_lock::derived_pre_lock::PreLockWitness; + +#[derive(Debug, Clone, Copy)] +pub enum PreLockWitnessBranch { + LendingCreation, + PreLockCancellation, +} + +impl PreLockWitnessBranch { + pub fn build_witness(&self) -> Box { + let path = match self { + PreLockWitnessBranch::LendingCreation => Left(()), + PreLockWitnessBranch::PreLockCancellation => Right(DUMMY_SIGNATURE), + }; + + Box::new(PreLockWitness { path }) + } +} diff --git a/crates/contracts/src/programs/program.rs b/crates/contracts/src/programs/program.rs index 41520e6..bf042ab 100644 --- a/crates/contracts/src/programs/program.rs +++ b/crates/contracts/src/programs/program.rs @@ -1,5 +1,6 @@ use simplex::program::{Program, WitnessTrait}; use simplex::provider::SimplicityNetwork; +use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, UTXO, }; @@ -42,17 +43,35 @@ pub trait SimplexProgram { ft: &'a mut FinalTransaction, program_utxo: UTXO, witness: Box, - sig_witness_name: String, + required_signature: RequiredSignature, ) -> &'a mut FinalTransaction { ft.add_program_input( PartialInput::new(program_utxo), ProgramInput::new(Box::new(self.get_program().clone()), witness), - RequiredSignature::Witness(sig_witness_name), + required_signature, ); ft } + fn add_program_issuance_input_with_signature( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + issuance_input: IssuanceInput, + witness: Box, + required_signature: RequiredSignature, + ) -> AssetId { + let (asset_id, _) = ft.add_program_issuance_input( + PartialInput::new(program_utxo), + ProgramInput::new(Box::new(self.get_program().clone()), witness), + issuance_input, + required_signature, + ); + + asset_id + } + fn add_program_output<'a>( &self, ft: &'a mut FinalTransaction, diff --git a/crates/contracts/src/programs/script_auth.rs b/crates/contracts/src/programs/script_auth.rs deleted file mode 100644 index 86f47a4..0000000 --- a/crates/contracts/src/programs/script_auth.rs +++ /dev/null @@ -1,59 +0,0 @@ -use simplex::program::Program; -use simplex::provider::SimplicityNetwork; - -use crate::artifacts::script_auth::derived_script_auth::ScriptAuthWitness; -use crate::artifacts::script_auth::{ScriptAuthProgram, derived_script_auth::ScriptAuthArguments}; -use crate::programs::program::SimplexProgram; - -#[derive(Debug, Clone, Copy)] -pub struct ScriptAuthParameters { - pub script_hash: [u8; 32], - pub network: SimplicityNetwork, -} - -impl From for ScriptAuthArguments { - fn from(value: ScriptAuthParameters) -> Self { - Self { - script_hash: value.script_hash, - } - } -} - -pub struct ScriptAuth { - program: ScriptAuthProgram, - parameters: ScriptAuthParameters, -} - -impl ScriptAuth { - pub fn new(parameters: ScriptAuthParameters) -> Self { - Self { - program: ScriptAuthProgram::new(ScriptAuthArguments::from(parameters)), - parameters, - } - } - - pub fn from_simplex_program(program: &impl SimplexProgram) -> Self { - Self::new(ScriptAuthParameters { - script_hash: program.get_script_hash(), - network: *program.get_network(), - }) - } - - pub fn get_script_auth_witness(input_script_index: u32) -> ScriptAuthWitness { - ScriptAuthWitness { input_script_index } - } - - pub fn get_script_auth_parameters(&self) -> &ScriptAuthParameters { - &self.parameters - } -} - -impl SimplexProgram for ScriptAuth { - fn get_program(&self) -> &Program { - self.program.get_program() - } - - fn get_network(&self) -> &SimplicityNetwork { - &self.parameters.network - } -} diff --git a/crates/contracts/src/programs/script_auth/core.rs b/crates/contracts/src/programs/script_auth/core.rs new file mode 100644 index 0000000..1136495 --- /dev/null +++ b/crates/contracts/src/programs/script_auth/core.rs @@ -0,0 +1,66 @@ +use simplex::{ + program::Program, + provider::SimplicityNetwork, + simplicityhl::elements::AssetId, + transaction::{FinalTransaction, UTXO}, +}; + +use crate::{ + artifacts::script_auth::ScriptAuthProgram, programs::script_auth::ScriptAuthWitnessParams, +}; + +use crate::programs::program::SimplexProgram; +use crate::programs::script_auth::ScriptAuthParameters; + +pub struct ScriptAuth { + program: ScriptAuthProgram, + parameters: ScriptAuthParameters, +} + +impl ScriptAuth { + pub fn new(parameters: ScriptAuthParameters) -> Self { + Self { + program: ScriptAuthProgram::new(parameters.build_arguments()), + parameters, + } + } + + pub fn from_simplex_program(program: &impl SimplexProgram) -> Self { + Self::new(ScriptAuthParameters { + script_hash: program.get_script_hash(), + network: *program.get_network(), + }) + } + + pub fn get_parameters(&self) -> &ScriptAuthParameters { + &self.parameters + } + + pub fn attach_creation( + &self, + ft: &mut FinalTransaction, + asset_id_to_lock: AssetId, + amount_to_lock: u64, + ) { + self.add_program_output(ft, asset_id_to_lock, amount_to_lock); + } + + pub fn attach_unlocking( + &self, + ft: &mut FinalTransaction, + program_utxo: UTXO, + witness_params: ScriptAuthWitnessParams, + ) { + self.add_program_input(ft, program_utxo, witness_params.build_witness()); + } +} + +impl SimplexProgram for ScriptAuth { + fn get_program(&self) -> &Program { + self.program.as_ref() + } + + fn get_network(&self) -> &SimplicityNetwork { + &self.parameters.network + } +} diff --git a/crates/contracts/src/programs/script_auth/mod.rs b/crates/contracts/src/programs/script_auth/mod.rs new file mode 100644 index 0000000..0e4a93c --- /dev/null +++ b/crates/contracts/src/programs/script_auth/mod.rs @@ -0,0 +1,7 @@ +mod core; +mod params; +mod witness; + +pub use core::ScriptAuth; +pub use params::ScriptAuthParameters; +pub use witness::ScriptAuthWitnessParams; diff --git a/crates/contracts/src/programs/script_auth/params.rs b/crates/contracts/src/programs/script_auth/params.rs new file mode 100644 index 0000000..124d960 --- /dev/null +++ b/crates/contracts/src/programs/script_auth/params.rs @@ -0,0 +1,17 @@ +use simplex::provider::SimplicityNetwork; + +use crate::artifacts::script_auth::derived_script_auth::ScriptAuthArguments; + +#[derive(Debug, Clone, Copy)] +pub struct ScriptAuthParameters { + pub script_hash: [u8; 32], + pub network: SimplicityNetwork, +} + +impl ScriptAuthParameters { + pub fn build_arguments(&self) -> ScriptAuthArguments { + ScriptAuthArguments { + script_hash: self.script_hash, + } + } +} diff --git a/crates/contracts/src/programs/script_auth/witness.rs b/crates/contracts/src/programs/script_auth/witness.rs new file mode 100644 index 0000000..3c52acd --- /dev/null +++ b/crates/contracts/src/programs/script_auth/witness.rs @@ -0,0 +1,18 @@ +use crate::artifacts::script_auth::derived_script_auth::ScriptAuthWitness; + +#[derive(Debug, Clone, Copy)] +pub struct ScriptAuthWitnessParams { + input_script_index: u32, +} + +impl ScriptAuthWitnessParams { + pub fn new(input_script_index: u32) -> Self { + Self { input_script_index } + } + + pub fn build_witness(&self) -> Box { + Box::new(ScriptAuthWitness { + input_script_index: self.input_script_index, + }) + } +} diff --git a/crates/contracts/src/transactions/asset_auth/creation.rs b/crates/contracts/src/transactions/asset_auth/creation.rs deleted file mode 100644 index b739404..0000000 --- a/crates/contracts/src/transactions/asset_auth/creation.rs +++ /dev/null @@ -1,35 +0,0 @@ -use simplex::transaction::FinalTransaction; - -use crate::{ - programs::{AssetAuth, AssetAuthParameters, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn create_asset_auth( - input_to_lock: &SimplexInput, - parameters: AssetAuthParameters, -) -> (FinalTransaction, AssetAuth) { - let amount_to_lock = input_to_lock.explicit_amount(); - - create_asset_auth_with_amount(input_to_lock, amount_to_lock, parameters) -} - -pub fn create_asset_auth_with_amount( - input_to_lock: &SimplexInput, - amount_to_lock: u64, - parameters: AssetAuthParameters, -) -> (FinalTransaction, AssetAuth) { - let mut ft = FinalTransaction::new(); - let asset_auth = AssetAuth::new(parameters); - - let asset_id_to_lock = input_to_lock.explicit_asset(); - - ft.add_input( - input_to_lock.partial_input().clone(), - input_to_lock.required_sig().clone(), - ); - - asset_auth.add_program_output(&mut ft, asset_id_to_lock, amount_to_lock); - - (ft, asset_auth) -} diff --git a/crates/contracts/src/transactions/asset_auth/mod.rs b/crates/contracts/src/transactions/asset_auth/mod.rs deleted file mode 100644 index 1c6cc2b..0000000 --- a/crates/contracts/src/transactions/asset_auth/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod creation; -pub mod unlocking; - -pub use creation::*; -pub use unlocking::*; diff --git a/crates/contracts/src/transactions/asset_auth/unlocking.rs b/crates/contracts/src/transactions/asset_auth/unlocking.rs deleted file mode 100644 index 4e3ae91..0000000 --- a/crates/contracts/src/transactions/asset_auth/unlocking.rs +++ /dev/null @@ -1,40 +0,0 @@ -use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; - -use crate::{ - programs::{AssetAuth, AssetAuthWitnessParams, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn unlock_asset_auth( - program_utxo: UTXO, - auth_input: &SimplexInput, - unlocked_output: PartialOutput, - asset_auth: AssetAuth, -) -> FinalTransaction { - let parameters = asset_auth.get_asset_auth_parameters(); - - let mut ft = FinalTransaction::new(); - - let witness_params = AssetAuthWitnessParams { - input_asset_index: 1, - output_asset_index: 1, - }; - let witness = AssetAuth::get_asset_auth_witness(&witness_params); - - asset_auth.add_program_input(&mut ft, program_utxo, Box::new(witness)); - - ft.add_input( - auth_input.partial_input().clone(), - auth_input.required_sig().clone(), - ); - - ft.add_output(unlocked_output); - - if parameters.with_asset_burn { - ft.add_output(auth_input.new_burn_partial_output()); - } else { - ft.add_output(auth_input.new_partial_output()); - } - - ft -} diff --git a/crates/contracts/src/transactions/core.rs b/crates/contracts/src/transactions/core.rs deleted file mode 100644 index cd17938..0000000 --- a/crates/contracts/src/transactions/core.rs +++ /dev/null @@ -1,52 +0,0 @@ -use simplex::simplicityhl::elements::{AssetId, Script}; -use simplex::transaction::{PartialInput, PartialOutput, RequiredSignature, UTXO}; - -pub struct SimplexInput { - partial_input: PartialInput, - required_sig: RequiredSignature, -} - -impl SimplexInput { - pub fn new(utxo: &UTXO, required_sig: RequiredSignature) -> Self { - Self { - partial_input: PartialInput::new(utxo.clone()), - required_sig, - } - } - - pub fn explicit_asset(&self) -> AssetId { - self.partial_input.asset.expect("Not an explicit asset") - } - - pub fn explicit_amount(&self) -> u64 { - self.partial_input.amount.expect("Not an explicit amount") - } - - pub fn utxo_script_pubkey(&self) -> Script { - self.partial_input.witness_utxo.script_pubkey.clone() - } - - pub fn new_partial_output(&self) -> PartialOutput { - PartialOutput::new( - self.utxo_script_pubkey(), - self.explicit_amount(), - self.explicit_asset(), - ) - } - - pub fn new_burn_partial_output(&self) -> PartialOutput { - PartialOutput::new( - Script::new_op_return(b"burn"), - self.explicit_amount(), - self.explicit_asset(), - ) - } - - pub fn partial_input(&self) -> &PartialInput { - &self.partial_input - } - - pub fn required_sig(&self) -> &RequiredSignature { - &self.required_sig - } -} diff --git a/crates/contracts/src/transactions/lending/error.rs b/crates/contracts/src/transactions/lending/error.rs deleted file mode 100644 index 2c501d6..0000000 --- a/crates/contracts/src/transactions/lending/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -use simplex::simplicityhl::elements::Txid; - -#[derive(thiserror::Error, Debug)] -pub enum LendingTransactionError { - #[error("Confidential assets currently are not supported")] - ConfidentialAssetsAreNotSupported(), - - #[error("Not enough principal assets to repay: expected - {expected}, actual - {actual}")] - NotEnoughPrincipalToRepay { expected: u64, actual: u64 }, - - #[error("Passed transaction is not a lending creation transaction")] - NotALendingCreationTx(Txid), - - #[error("Failed to convert loan expiration time to LockTime: {0}")] - InvalidLockHeight(u32), -} diff --git a/crates/contracts/src/transactions/lending/liquidation.rs b/crates/contracts/src/transactions/lending/liquidation.rs deleted file mode 100644 index 63825fc..0000000 --- a/crates/contracts/src/transactions/lending/liquidation.rs +++ /dev/null @@ -1,80 +0,0 @@ -use simplex::{ - simplicityhl::elements::{LockTime, Script, Sequence}, - transaction::{FinalTransaction, PartialInput, PartialOutput, UTXO}, -}; - -use crate::{ - programs::{Lending, LendingBranch, ScriptAuth, program::SimplexProgram}, - transactions::{core::SimplexInput, lending::LendingTransactionError}, -}; - -pub fn liquidate_loan( - lending_utxo: UTXO, - first_parameters_nft_utxo: UTXO, - second_parameters_nft_utxo: UTXO, - lender_nft_input: &SimplexInput, - collateral_output: PartialOutput, - lending: Lending, -) -> Result { - let lending_parameters = lending.get_lending_parameters(); - let mut ft = FinalTransaction::new(); - - let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); - let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); - - let witness = Lending::get_lending_witness(&LendingBranch::LoanLiquidation); - - let locktime = LockTime::from_height(lending_parameters.offer_parameters.loan_expiration_time) - .map_err(|_| { - LendingTransactionError::InvalidLockHeight( - lending_parameters.offer_parameters.loan_expiration_time, - ) - })?; - - let lending_input = PartialInput::new(lending_utxo) - .with_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF) - .with_locktime(locktime); - - lending.add_program_input_from_partial_input(&mut ft, lending_input, Box::new(witness)); - - let parameters_script_auth = ScriptAuth::from_simplex_program(&lending); - let parameters_witness = ScriptAuth::get_script_auth_witness(0); - - parameters_script_auth.add_program_input( - &mut ft, - first_parameters_nft_utxo, - Box::new(parameters_witness.clone()), - ); - parameters_script_auth.add_program_input( - &mut ft, - second_parameters_nft_utxo, - Box::new(parameters_witness), - ); - - ft.add_input( - lender_nft_input.partial_input().clone(), - lender_nft_input.required_sig().clone(), - ); - - ft.add_output(collateral_output); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - first_parameters_nft_amount, - lending_parameters.first_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - second_parameters_nft_amount, - lending_parameters.second_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - 1, - lending_parameters.lender_nft_asset_id, - )); - - Ok(ft) -} diff --git a/crates/contracts/src/transactions/lending/mod.rs b/crates/contracts/src/transactions/lending/mod.rs deleted file mode 100644 index 404eb27..0000000 --- a/crates/contracts/src/transactions/lending/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod error; -pub mod liquidation; -pub mod parameters; -pub mod repayment; - -pub use error::*; -pub use liquidation::*; -pub use parameters::*; -pub use repayment::*; diff --git a/crates/contracts/src/transactions/lending/parameters.rs b/crates/contracts/src/transactions/lending/parameters.rs deleted file mode 100644 index e0a8abb..0000000 --- a/crates/contracts/src/transactions/lending/parameters.rs +++ /dev/null @@ -1,68 +0,0 @@ -use simplex::{provider::ProviderTrait, simplicityhl::elements::Transaction}; - -use crate::{ - programs::LendingParameters, - transactions::lending::LendingTransactionError, - utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}, -}; - -pub fn extract_lending_parameters_from_tx( - tx: &Transaction, - provider: &impl ProviderTrait, -) -> Result { - if tx.input.len() < 7 || tx.output.len() < 7 { - return Err(LendingTransactionError::NotALendingCreationTx(tx.txid())); - } - - let collateral_asset_id = tx.output[0] - .asset - .explicit() - .ok_or_else(LendingTransactionError::ConfidentialAssetsAreNotSupported)?; - let principal_asset_id = tx.output[1] - .asset - .explicit() - .ok_or_else(LendingTransactionError::ConfidentialAssetsAreNotSupported)?; - let first_parameters_nft_asset_id = tx.output[2] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let second_parameters_nft_asset_id = tx.output[3] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let borrower_nft_asset_id = tx.output[4] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let lender_nft_asset_id = tx.output[5] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - - let first_parameters_nft_amount = tx.output[2] - .value - .explicit() - .expect("Parameter NFT must have explicit amount"); - let second_parameters_nft_amount = tx.output[3] - .value - .explicit() - .expect("Parameter NFT must have explicit amount"); - - let offer_parameters = LendingOfferParameters::build_from_parameters_nfts( - &FirstNFTParameters::decode(first_parameters_nft_amount), - &SecondNFTParameters::decode(second_parameters_nft_amount), - ); - - let lending_parameters = LendingParameters { - collateral_asset_id, - principal_asset_id, - first_parameters_nft_asset_id, - second_parameters_nft_asset_id, - borrower_nft_asset_id, - lender_nft_asset_id, - offer_parameters, - network: *provider.get_network(), - }; - - Ok(lending_parameters) -} diff --git a/crates/contracts/src/transactions/lending/repayment.rs b/crates/contracts/src/transactions/lending/repayment.rs deleted file mode 100644 index 12ebb8b..0000000 --- a/crates/contracts/src/transactions/lending/repayment.rs +++ /dev/null @@ -1,107 +0,0 @@ -use simplex::simplicityhl::elements::Script; -use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; - -use crate::transactions::core::SimplexInput; -use crate::{ - programs::{Lending, LendingBranch, ScriptAuth, program::SimplexProgram}, - transactions::lending::LendingTransactionError, -}; - -pub fn repay_loan( - lending_utxo: UTXO, - first_parameters_nft_utxo: UTXO, - second_parameters_nft_utxo: UTXO, - borrower_nft_input: &SimplexInput, - principal_inputs: Vec, - collateral_output: PartialOutput, - lending: Lending, -) -> Result { - let lending_parameters = lending.get_lending_parameters(); - let mut ft = FinalTransaction::new(); - - let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); - let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); - - let witness = Lending::get_lending_witness(&LendingBranch::LoanRepayment); - - lending.add_program_input(&mut ft, lending_utxo, Box::new(witness)); - - let parameters_script_auth = ScriptAuth::from_simplex_program(&lending); - let parameters_witness = ScriptAuth::get_script_auth_witness(0); - - parameters_script_auth.add_program_input( - &mut ft, - first_parameters_nft_utxo, - Box::new(parameters_witness.clone()), - ); - parameters_script_auth.add_program_input( - &mut ft, - second_parameters_nft_utxo, - Box::new(parameters_witness), - ); - - ft.add_input( - borrower_nft_input.partial_input().clone(), - borrower_nft_input.required_sig().clone(), - ); - - let mut total_principal_input_amount = 0; - let principal_script_pubkey = principal_inputs.first().unwrap().utxo_script_pubkey(); - - for principal_input in principal_inputs { - ft.add_input( - principal_input.partial_input().clone(), - principal_input.required_sig().clone(), - ); - - total_principal_input_amount += principal_input.explicit_amount(); - } - - ft.add_output(collateral_output); - - let principal_with_interest = lending_parameters - .offer_parameters - .calculate_principal_with_interest(); - let lender_principal_asset_auth = lending_parameters.get_lender_principal_asset_auth(); - - if total_principal_input_amount < principal_with_interest { - return Err(LendingTransactionError::NotEnoughPrincipalToRepay { - expected: principal_with_interest, - actual: total_principal_input_amount, - }); - } - - lender_principal_asset_auth.add_program_output( - &mut ft, - lending_parameters.principal_asset_id, - principal_with_interest, - ); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - first_parameters_nft_amount, - lending_parameters.first_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - second_parameters_nft_amount, - lending_parameters.second_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - 1, - lending_parameters.borrower_nft_asset_id, - )); - - if total_principal_input_amount > principal_with_interest { - ft.add_output(PartialOutput::new( - principal_script_pubkey, - total_principal_input_amount - principal_with_interest, - lending_parameters.principal_asset_id, - )); - } - - Ok(ft) -} diff --git a/crates/contracts/src/transactions/mod.rs b/crates/contracts/src/transactions/mod.rs deleted file mode 100644 index ebc6d5f..0000000 --- a/crates/contracts/src/transactions/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod asset_auth; -pub mod core; -pub mod lending; -pub mod ownable_script_auth; -pub mod pre_lock; -pub mod script_auth; -pub mod utility; diff --git a/crates/contracts/src/transactions/ownable_script_auth/creation.rs b/crates/contracts/src/transactions/ownable_script_auth/creation.rs deleted file mode 100644 index 80916f9..0000000 --- a/crates/contracts/src/transactions/ownable_script_auth/creation.rs +++ /dev/null @@ -1,33 +0,0 @@ -use simplex::{ - simplicityhl::elements::{AssetId, Script}, - transaction::{FinalTransaction, PartialOutput}, -}; - -use crate::{ - programs::{OwnableScriptAuth, OwnableScriptAuthParameters, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn create_ownable_script_auth( - input_to_lock: &SimplexInput, - amount_to_lock: u64, - parameters: OwnableScriptAuthParameters, -) -> (FinalTransaction, OwnableScriptAuth) { - let ownable_script_auth = OwnableScriptAuth::new(parameters); - let mut ft = FinalTransaction::new(); - - ft.add_input( - input_to_lock.partial_input().clone(), - input_to_lock.required_sig().clone(), - ); - - ownable_script_auth.add_program_output(&mut ft, input_to_lock.explicit_asset(), amount_to_lock); - - ft.add_output(PartialOutput::new( - Script::new_op_return(parameters.owner_pubkey.serialize().as_slice()), - 0, - AssetId::default(), - )); - - (ft, ownable_script_auth) -} diff --git a/crates/contracts/src/transactions/ownable_script_auth/mod.rs b/crates/contracts/src/transactions/ownable_script_auth/mod.rs deleted file mode 100644 index 29df1f5..0000000 --- a/crates/contracts/src/transactions/ownable_script_auth/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod creation; -pub mod ownership_transfer; -pub mod script_auth_unlock; - -pub use creation::*; -pub use ownership_transfer::*; -pub use script_auth_unlock::*; diff --git a/crates/contracts/src/transactions/ownable_script_auth/ownership_transfer.rs b/crates/contracts/src/transactions/ownable_script_auth/ownership_transfer.rs deleted file mode 100644 index b8a603b..0000000 --- a/crates/contracts/src/transactions/ownable_script_auth/ownership_transfer.rs +++ /dev/null @@ -1,45 +0,0 @@ -use simplex::{ - simplicityhl::elements::{AssetId, Script, schnorr::XOnlyPublicKey}, - transaction::{FinalTransaction, PartialOutput, UTXO}, -}; - -use crate::programs::{OwnableScriptAuth, OwnableScriptAuthBranch, program::SimplexProgram}; - -pub fn ownership_transfer( - program_utxo: UTXO, - new_owner: XOnlyPublicKey, - ownable_script_auth: &mut OwnableScriptAuth, -) -> FinalTransaction { - let mut ft = FinalTransaction::new(); - - let parameters = *ownable_script_auth.get_ownable_script_auth_parameters(); - let witness = OwnableScriptAuth::get_ownable_script_auth_witness( - &OwnableScriptAuthBranch::OwnershipTransfer { - current_owner: parameters.owner_pubkey, - new_owner, - program_output_index: 0, - }, - ); - - let locked_asset = program_utxo.explicit_asset(); - let locked_amount = program_utxo.explicit_amount(); - - ownable_script_auth.add_program_input_with_signature( - &mut ft, - program_utxo, - Box::new(witness), - "SIGNATURE".into(), - ); - - ownable_script_auth.apply_ownership_transfer(new_owner); - - ownable_script_auth.add_program_output(&mut ft, locked_asset, locked_amount); - - ft.add_output(PartialOutput::new( - Script::new_op_return(new_owner.serialize().as_slice()), - 0, - AssetId::default(), - )); - - ft -} diff --git a/crates/contracts/src/transactions/ownable_script_auth/script_auth_unlock.rs b/crates/contracts/src/transactions/ownable_script_auth/script_auth_unlock.rs deleted file mode 100644 index 09e6479..0000000 --- a/crates/contracts/src/transactions/ownable_script_auth/script_auth_unlock.rs +++ /dev/null @@ -1,49 +0,0 @@ -use simplex::{ - simplicityhl::elements::Script, - transaction::{FinalTransaction, PartialOutput, UTXO}, -}; - -use crate::{ - programs::{OwnableScriptAuth, OwnableScriptAuthBranch, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn ownable_script_auth_unlock( - program_utxo: &UTXO, - auth_input: &SimplexInput, - program_output_script: Script, - ownable_script_auth: &OwnableScriptAuth, -) -> FinalTransaction { - let mut ft = FinalTransaction::new(); - - let parameters = *ownable_script_auth.get_ownable_script_auth_parameters(); - let witness = OwnableScriptAuth::get_ownable_script_auth_witness( - &OwnableScriptAuthBranch::ScriptAuthUnlock { - owner: parameters.owner_pubkey, - input_script_index: 1, - }, - ); - - let locked_asset = program_utxo.explicit_asset(); - let locked_amount = program_utxo.explicit_amount(); - - ownable_script_auth.add_program_input_with_signature( - &mut ft, - program_utxo.clone(), - Box::new(witness), - "SIGNATURE".into(), - ); - ft.add_input( - auth_input.partial_input().clone(), - auth_input.required_sig().clone(), - ); - - ft.add_output(PartialOutput::new( - program_output_script, - locked_amount, - locked_asset, - )); - ft.add_output(auth_input.new_partial_output()); - - ft -} diff --git a/crates/contracts/src/transactions/pre_lock/cancellation.rs b/crates/contracts/src/transactions/pre_lock/cancellation.rs deleted file mode 100644 index c13d23d..0000000 --- a/crates/contracts/src/transactions/pre_lock/cancellation.rs +++ /dev/null @@ -1,80 +0,0 @@ -use simplex::simplicityhl::elements::Script; -use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; - -use crate::programs::{PreLock, PreLockBranch, ScriptAuth, program::SimplexProgram}; - -pub fn cancel_pre_lock( - pre_lock_utxo: UTXO, - first_parameters_nft_utxo: UTXO, - second_parameters_nft_utxo: UTXO, - borrower_nft_utxo: UTXO, - lender_nft_utxo: UTXO, - collateral_output: PartialOutput, - pre_lock: PreLock, -) -> FinalTransaction { - let pre_lock_parameters = pre_lock.get_pre_lock_parameters(); - let mut ft = FinalTransaction::new(); - - let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); - let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); - - let pre_lock_witness = PreLock::get_pre_lock_witness(&PreLockBranch::PreLockCancellation); - pre_lock.add_program_input_with_signature( - &mut ft, - pre_lock_utxo, - Box::new(pre_lock_witness), - "SIGNATURE".into(), - ); - - let utility_nfts_script_auth = ScriptAuth::from_simplex_program(&pre_lock); - let utility_nfts_witness = ScriptAuth::get_script_auth_witness(0); - - utility_nfts_script_auth.add_program_input( - &mut ft, - first_parameters_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - second_parameters_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - borrower_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - lender_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - - ft.add_output(collateral_output); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - first_parameters_nft_amount, - pre_lock_parameters.first_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - second_parameters_nft_amount, - pre_lock_parameters.second_parameters_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - 1, - pre_lock_parameters.borrower_nft_asset_id, - )); - - ft.add_output(PartialOutput::new( - Script::new_op_return(b"burn"), - 1, - pre_lock_parameters.lender_nft_asset_id, - )); - - ft -} diff --git a/crates/contracts/src/transactions/pre_lock/creation.rs b/crates/contracts/src/transactions/pre_lock/creation.rs deleted file mode 100644 index d3b3112..0000000 --- a/crates/contracts/src/transactions/pre_lock/creation.rs +++ /dev/null @@ -1,69 +0,0 @@ -use simplex::simplicityhl::elements::{AssetId, Script}; -use simplex::transaction::{FinalTransaction, PartialOutput}; - -use crate::programs::PreLockParameters; -use crate::programs::{PreLock, ScriptAuth, program::SimplexProgram}; -use crate::transactions::core::SimplexInput; - -pub fn create_pre_lock( - collateral_input: &SimplexInput, - first_parameters_nft_input: &SimplexInput, - second_parameters_nft_input: &SimplexInput, - borrower_nft_input: &SimplexInput, - lender_nft_input: &SimplexInput, - parameters: PreLockParameters, -) -> (FinalTransaction, PreLock) { - let mut ft = FinalTransaction::new(); - - ft.add_input( - collateral_input.partial_input().clone(), - collateral_input.required_sig().clone(), - ); - ft.add_input( - first_parameters_nft_input.partial_input().clone(), - first_parameters_nft_input.required_sig().clone(), - ); - ft.add_input( - second_parameters_nft_input.partial_input().clone(), - second_parameters_nft_input.required_sig().clone(), - ); - ft.add_input( - borrower_nft_input.partial_input().clone(), - borrower_nft_input.required_sig().clone(), - ); - ft.add_input( - lender_nft_input.partial_input().clone(), - lender_nft_input.required_sig().clone(), - ); - - let pre_lock = PreLock::new(parameters); - let utility_nfts_script_auth = ScriptAuth::from_simplex_program(&pre_lock); - - pre_lock.add_program_output( - &mut ft, - parameters.collateral_asset_id, - parameters.offer_parameters.collateral_amount, - ); - utility_nfts_script_auth.add_program_output( - &mut ft, - parameters.first_parameters_nft_asset_id, - first_parameters_nft_input.explicit_amount(), - ); - utility_nfts_script_auth.add_program_output( - &mut ft, - parameters.second_parameters_nft_asset_id, - second_parameters_nft_input.explicit_amount(), - ); - utility_nfts_script_auth.add_program_output(&mut ft, parameters.borrower_nft_asset_id, 1); - utility_nfts_script_auth.add_program_output(&mut ft, parameters.lender_nft_asset_id, 1); - - let op_return_data = pre_lock.encode_creation_op_return_data(); - - ft.add_output(PartialOutput::new( - Script::new_op_return(&op_return_data), - 0, - AssetId::default(), - )); - - (ft, pre_lock) -} diff --git a/crates/contracts/src/transactions/pre_lock/error.rs b/crates/contracts/src/transactions/pre_lock/error.rs deleted file mode 100644 index 59dbd65..0000000 --- a/crates/contracts/src/transactions/pre_lock/error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use simplex::{provider::ProviderError, simplicityhl::elements::Txid}; - -use crate::programs::PreLockError; - -#[derive(thiserror::Error, Debug)] -pub enum PreLockTransactionError { - #[error("Confidential assets currently are not supported")] - ConfidentialAssetsAreNotSupported(), - - #[error("Passed transaction is not a pre lock creation transaction")] - NotAPreLockCreationTx(Txid), - - #[error("Failed to extract pre lock parameters: {0}")] - PreLock(#[from] PreLockError), - - #[error(transparent)] - SimplexProvider(#[from] ProviderError), -} diff --git a/crates/contracts/src/transactions/pre_lock/lending_creation.rs b/crates/contracts/src/transactions/pre_lock/lending_creation.rs deleted file mode 100644 index 32d0270..0000000 --- a/crates/contracts/src/transactions/pre_lock/lending_creation.rs +++ /dev/null @@ -1,93 +0,0 @@ -use simplex::simplicityhl::elements::Script; -use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; - -use crate::programs::{Lending, PreLock, PreLockBranch, ScriptAuth, program::SimplexProgram}; -use crate::transactions::core::SimplexInput; - -#[allow(clippy::too_many_arguments)] -pub fn create_lending_from_pre_lock( - pre_lock_utxo: UTXO, - first_parameters_nft_utxo: UTXO, - second_parameters_nft_utxo: UTXO, - borrower_nft_utxo: UTXO, - lender_nft_utxo: UTXO, - principal_inputs: Vec<&SimplexInput>, - lender_nft_output: PartialOutput, - borrower_output_script: Script, - pre_lock: PreLock, -) -> (FinalTransaction, Lending) { - let pre_lock_parameters = pre_lock.get_pre_lock_parameters(); - let mut ft = FinalTransaction::new(); - - let first_parameters_nft_amount = first_parameters_nft_utxo.explicit_amount(); - let second_parameters_nft_amount = second_parameters_nft_utxo.explicit_amount(); - - let pre_lock_witness = PreLock::get_pre_lock_witness(&PreLockBranch::LendingCreation); - pre_lock.add_program_input(&mut ft, pre_lock_utxo, Box::new(pre_lock_witness)); - - let utility_nfts_script_auth = ScriptAuth::from_simplex_program(&pre_lock); - let utility_nfts_witness = ScriptAuth::get_script_auth_witness(0); - - utility_nfts_script_auth.add_program_input( - &mut ft, - first_parameters_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - second_parameters_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - borrower_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - utility_nfts_script_auth.add_program_input( - &mut ft, - lender_nft_utxo, - Box::new(utility_nfts_witness.clone()), - ); - - for principal_input in principal_inputs { - ft.add_input( - principal_input.partial_input().clone(), - principal_input.required_sig().clone(), - ); - } - - let lending = Lending::new(pre_lock_parameters.into()); - let parameter_nfts_script_auth = pre_lock_parameters.get_parameter_nfts_script_auth(); - - lending.add_program_output( - &mut ft, - pre_lock_parameters.collateral_asset_id, - pre_lock_parameters.offer_parameters.collateral_amount, - ); - - ft.add_output(PartialOutput::new( - borrower_output_script.clone(), - pre_lock_parameters.offer_parameters.principal_amount, - pre_lock_parameters.principal_asset_id, - )); - - parameter_nfts_script_auth.add_program_output( - &mut ft, - pre_lock_parameters.first_parameters_nft_asset_id, - first_parameters_nft_amount, - ); - parameter_nfts_script_auth.add_program_output( - &mut ft, - pre_lock_parameters.second_parameters_nft_asset_id, - second_parameters_nft_amount, - ); - - ft.add_output(PartialOutput::new( - borrower_output_script, - 1, - pre_lock_parameters.borrower_nft_asset_id, - )); - ft.add_output(lender_nft_output); - - (ft, lending) -} diff --git a/crates/contracts/src/transactions/pre_lock/mod.rs b/crates/contracts/src/transactions/pre_lock/mod.rs deleted file mode 100644 index 6e7d9ea..0000000 --- a/crates/contracts/src/transactions/pre_lock/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod cancellation; -pub mod creation; -pub mod error; -pub mod lending_creation; -pub mod parameters; - -pub use cancellation::*; -pub use creation::*; -pub use error::*; -pub use lending_creation::*; -pub use parameters::*; diff --git a/crates/contracts/src/transactions/pre_lock/parameters.rs b/crates/contracts/src/transactions/pre_lock/parameters.rs deleted file mode 100644 index de46605..0000000 --- a/crates/contracts/src/transactions/pre_lock/parameters.rs +++ /dev/null @@ -1,86 +0,0 @@ -use simplex::{provider::ProviderTrait, simplicityhl::elements::Transaction, utils::hash_script}; - -use crate::{ - programs::{PreLock, PreLockParameters}, - transactions::pre_lock::PreLockTransactionError, - utils::{FirstNFTParameters, LendingOfferParameters, SecondNFTParameters}, -}; - -pub fn extract_pre_lock_parameters_from_tx( - tx: &Transaction, - provider: &impl ProviderTrait, -) -> Result { - if tx.input.len() < 5 || tx.output.len() < 7 || !tx.output[5].is_null_data() { - return Err(PreLockTransactionError::NotAPreLockCreationTx(tx.txid())); - } - - let collateral_asset_id = tx.output[0] - .asset - .explicit() - .ok_or_else(PreLockTransactionError::ConfidentialAssetsAreNotSupported)?; - let first_parameters_nft_asset_id = tx.output[1] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let second_parameters_nft_asset_id = tx.output[2] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let borrower_nft_asset_id = tx.output[3] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - let lender_nft_asset_id = tx.output[4] - .asset - .explicit() - .expect("Utility NFT must be explicit"); - - let first_parameters_nft_amount = tx.output[1] - .value - .explicit() - .expect("Parameter NFT must have explicit amount"); - let second_parameters_nft_amount = tx.output[2] - .value - .explicit() - .expect("Parameter NFT must have explicit amount"); - - let offer_parameters = LendingOfferParameters::build_from_parameters_nfts( - &FirstNFTParameters::decode(first_parameters_nft_amount), - &SecondNFTParameters::decode(second_parameters_nft_amount), - ); - - let prev_collateral_outpoint = tx.input[0].previous_output; - let pre_collateral_tx = provider.fetch_transaction(&prev_collateral_outpoint.txid)?; - let collateral_script_hash = hash_script( - &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 (borrower_pubkey, principal_asset_id) = - PreLock::decode_creation_op_return_data(op_return_bytes.to_vec())?; - - let pre_lock_parameters = PreLockParameters { - collateral_asset_id, - 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_output_script_hash: collateral_script_hash, - network: *provider.get_network(), - }; - - Ok(pre_lock_parameters) -} diff --git a/crates/contracts/src/transactions/script_auth/creation.rs b/crates/contracts/src/transactions/script_auth/creation.rs deleted file mode 100644 index 7b76f41..0000000 --- a/crates/contracts/src/transactions/script_auth/creation.rs +++ /dev/null @@ -1,33 +0,0 @@ -use simplex::transaction::FinalTransaction; - -use crate::{ - programs::{ScriptAuth, ScriptAuthParameters, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn create_script_auth( - input_to_lock: &SimplexInput, - parameters: ScriptAuthParameters, -) -> (FinalTransaction, ScriptAuth) { - let amount_to_lock = input_to_lock.explicit_amount(); - - create_script_auth_with_amount(input_to_lock, amount_to_lock, parameters) -} - -pub fn create_script_auth_with_amount( - input_to_lock: &SimplexInput, - amount_to_lock: u64, - parameters: ScriptAuthParameters, -) -> (FinalTransaction, ScriptAuth) { - let script_auth = ScriptAuth::new(parameters); - let mut ft = FinalTransaction::new(); - - ft.add_input( - input_to_lock.partial_input().clone(), - input_to_lock.required_sig().clone(), - ); - - script_auth.add_program_output(&mut ft, input_to_lock.explicit_asset(), amount_to_lock); - - (ft, script_auth) -} diff --git a/crates/contracts/src/transactions/script_auth/mod.rs b/crates/contracts/src/transactions/script_auth/mod.rs deleted file mode 100644 index 1c6cc2b..0000000 --- a/crates/contracts/src/transactions/script_auth/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod creation; -pub mod unlocking; - -pub use creation::*; -pub use unlocking::*; diff --git a/crates/contracts/src/transactions/script_auth/unlocking.rs b/crates/contracts/src/transactions/script_auth/unlocking.rs deleted file mode 100644 index 32022b9..0000000 --- a/crates/contracts/src/transactions/script_auth/unlocking.rs +++ /dev/null @@ -1,28 +0,0 @@ -use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; - -use crate::{ - programs::{ScriptAuth, program::SimplexProgram}, - transactions::core::SimplexInput, -}; - -pub fn unlock_script_auth( - program_utxo: UTXO, - auth_input: &SimplexInput, - unlocked_output: PartialOutput, - script_auth: ScriptAuth, -) -> FinalTransaction { - let mut ft = FinalTransaction::new(); - - let witness = ScriptAuth::get_script_auth_witness(1); - - script_auth.add_program_input(&mut ft, program_utxo, Box::new(witness)); - - ft.add_input( - auth_input.partial_input().clone(), - auth_input.required_sig().clone(), - ); - - ft.add_output(unlocked_output); - - ft -} diff --git a/crates/contracts/src/transactions/utility/error.rs b/crates/contracts/src/transactions/utility/error.rs deleted file mode 100644 index 87c7631..0000000 --- a/crates/contracts/src/transactions/utility/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::utils::ParametersError; - -#[derive(thiserror::Error, Debug)] -pub enum UtilityTransactionError { - #[error("Invalid issuance inputs count: expected - {expected_count}, actual - {actual_count}")] - InvalidIssuanceInputsCount { - expected_count: usize, - actual_count: usize, - }, - - #[error(transparent)] - OfferParameters(#[from] ParametersError), -} diff --git a/crates/contracts/src/transactions/utility/issuance_preparation.rs b/crates/contracts/src/transactions/utility/issuance_preparation.rs deleted file mode 100644 index 83e8917..0000000 --- a/crates/contracts/src/transactions/utility/issuance_preparation.rs +++ /dev/null @@ -1,41 +0,0 @@ -use simplex::simplicityhl::elements::{AssetId, Script}; -use simplex::{ - provider::SimplicityNetwork, - transaction::{FinalTransaction, PartialOutput, partial_input::IssuanceInput}, -}; - -use crate::transactions::core::SimplexInput; -use crate::{transactions::utility::UTILITY_NFTS_COUNT, utils::get_random_seed}; - -pub const PREPARATION_UTXO_ASSET_AMOUNT: u64 = 10; - -pub fn issue_preparation_utxos( - issuance_input: &SimplexInput, - issuance_utxos_output_script: Script, - network: SimplicityNetwork, -) -> (FinalTransaction, AssetId) { - let mut ft = FinalTransaction::new(); - - let total_asset_amount = PREPARATION_UTXO_ASSET_AMOUNT * UTILITY_NFTS_COUNT as u64; - let asset_entropy = get_random_seed(); - - let asset_id = ft.add_issuance_input( - issuance_input.partial_input().clone(), - IssuanceInput::new(total_asset_amount, asset_entropy), - issuance_input.required_sig().clone(), - ); - - for _ in 0..UTILITY_NFTS_COUNT { - ft.add_output(PartialOutput::new( - issuance_utxos_output_script.clone(), - PREPARATION_UTXO_ASSET_AMOUNT, - asset_id, - )); - } - - if issuance_input.explicit_asset() != network.policy_asset() { - ft.add_output(issuance_input.new_partial_output()); - } - - (ft, asset_id) -} diff --git a/crates/contracts/src/transactions/utility/mod.rs b/crates/contracts/src/transactions/utility/mod.rs deleted file mode 100644 index 87cf510..0000000 --- a/crates/contracts/src/transactions/utility/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod error; -pub mod issuance_preparation; -pub mod utility_nfts_issuance; - -pub use error::*; -pub use issuance_preparation::*; -pub use utility_nfts_issuance::*; diff --git a/crates/contracts/src/transactions/utility/utility_nfts_issuance.rs b/crates/contracts/src/transactions/utility/utility_nfts_issuance.rs deleted file mode 100644 index 40c4811..0000000 --- a/crates/contracts/src/transactions/utility/utility_nfts_issuance.rs +++ /dev/null @@ -1,60 +0,0 @@ -use simplex::simplicityhl::elements::{AssetId, Script}; -use simplex::transaction::{FinalTransaction, PartialOutput, partial_input::IssuanceInput}; - -use crate::{ - transactions::{core::SimplexInput, utility::UtilityTransactionError}, - utils::LendingOfferParameters, -}; - -pub const UTILITY_NFTS_COUNT: usize = 4; - -pub fn issue_utility_nfts( - issuance_inputs: Vec, - utility_nfts_output_script: Script, - lending_offer_params: &LendingOfferParameters, - amounts_decimals: u8, - issuance_asset_entropy: [u8; 32], -) -> Result { - let mut ft = FinalTransaction::new(); - - if issuance_inputs.len() != UTILITY_NFTS_COUNT { - return Err(UtilityTransactionError::InvalidIssuanceInputsCount { - expected_count: UTILITY_NFTS_COUNT, - actual_count: issuance_inputs.len(), - }); - } - - let (first_parameters_nft_amount, second_parameters_nft_amount) = - lending_offer_params.encode_parameters_nft_amounts(amounts_decimals)?; - - let utility_nfts_amounts = [ - first_parameters_nft_amount, - second_parameters_nft_amount, - 1, - 1, - ]; - let mut asset_ids: Vec = Vec::with_capacity(UTILITY_NFTS_COUNT); - - for (index, input) in issuance_inputs.iter().enumerate() { - let asset_id = ft.add_issuance_input( - input.partial_input().clone(), - IssuanceInput::new(utility_nfts_amounts[index], issuance_asset_entropy), - input.required_sig().clone(), - ); - asset_ids.push(asset_id); - } - - for (index, asset_id) in asset_ids.into_iter().enumerate() { - ft.add_output(PartialOutput::new( - utility_nfts_output_script.clone(), - utility_nfts_amounts[index], - asset_id, - )); - } - - for input in issuance_inputs { - ft.add_output(input.new_partial_output()); - } - - Ok(ft) -} diff --git a/crates/contracts/tests/asset_auth/burn.rs b/crates/contracts/tests/asset_auth/burn.rs deleted file mode 100644 index 43d8245..0000000 --- a/crates/contracts/tests/asset_auth/burn.rs +++ /dev/null @@ -1,37 +0,0 @@ -use lending_contracts::programs::AssetAuthParameters; - -use super::common::asserts::assert_burn_output; -use super::common::issuance::issue_asset; -use super::common::wallet::split_first_signer_utxo; -use super::happy_path::{create_asset_auth_tx, unlock_asset_auth_tx}; - -#[simplex::test] -fn creates_and_unlocks_asset_auth_with_burn(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let txid = split_first_signer_utxo(&context, vec![1000]); - provider.wait(&txid)?; - - let asset_amount = 1; - let (txid, asset_id) = issue_asset(&context, asset_amount)?; - provider.wait(&txid)?; - - let asset_auth_parameters = AssetAuthParameters { - asset_id, - asset_amount, - with_asset_burn: true, - network: *context.get_network(), - }; - - let (txid, asset_auth) = create_asset_auth_tx(&context, asset_auth_parameters)?; - provider.wait(&txid)?; - - let txid = unlock_asset_auth_tx(&context, asset_auth)?; - provider.wait(&txid)?; - - let tx = provider.fetch_transaction(&txid)?; - let burn_output = tx.output[1].clone(); - assert_burn_output(&burn_output, asset_id, asset_amount); - - Ok(()) -} diff --git a/crates/contracts/tests/asset_auth/happy_path.rs b/crates/contracts/tests/asset_auth/happy_path.rs deleted file mode 100644 index 225d188..0000000 --- a/crates/contracts/tests/asset_auth/happy_path.rs +++ /dev/null @@ -1,91 +0,0 @@ -use lending_contracts::programs::program::SimplexProgram; -use lending_contracts::programs::{AssetAuth, AssetAuthParameters}; -use lending_contracts::transactions::asset_auth::{create_asset_auth, unlock_asset_auth}; - -use lending_contracts::transactions::core::SimplexInput; -use simplex::simplicityhl::elements::Txid; -use simplex::transaction::{PartialOutput, RequiredSignature}; - -use crate::asset_auth_tests::common::tx_steps::finalize_and_broadcast; - -use super::common::issuance::issue_asset; -use super::common::wallet::{filter_signer_utxos_by_asset_id, split_first_signer_utxo}; - -pub(super) fn create_asset_auth_tx( - context: &simplex::TestContext, - parameters: AssetAuthParameters, -) -> anyhow::Result<(Txid, AssetAuth)> { - let network = context.get_network(); - let signer = context.get_default_signer(); - - let policy_utxos = filter_signer_utxos_by_asset_id(signer, network.policy_asset()); - let utxo_to_lock = policy_utxos.first().unwrap(); - - let (ft, asset_auth) = create_asset_auth( - &SimplexInput::new(utxo_to_lock, RequiredSignature::NativeEcdsa), - parameters, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok((txid, asset_auth)) -} - -pub(super) fn unlock_asset_auth_tx( - context: &simplex::TestContext, - asset_auth: AssetAuth, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let found_asset_auth_utxos = - provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?; - let asset_auth_utxo = found_asset_auth_utxos.first().unwrap(); - - let asset_auth_parameters = asset_auth.get_asset_auth_parameters(); - let auth_utxos = filter_signer_utxos_by_asset_id(signer, asset_auth_parameters.asset_id); - let auth_utxo = auth_utxos.first().unwrap(); - - let signer_script_pubkey = signer.get_address().script_pubkey(); - let ft = unlock_asset_auth( - asset_auth_utxo.clone(), - &SimplexInput::new(auth_utxo, RequiredSignature::NativeEcdsa), - PartialOutput::new( - signer_script_pubkey, - asset_auth_utxo.explicit_amount(), - asset_auth_utxo.explicit_asset(), - ), - asset_auth, - ); - - finalize_and_broadcast(context, &ft) -} - -#[simplex::test] -fn creates_and_unlocks_asset_auth_without_burn( - context: simplex::TestContext, -) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let txid = split_first_signer_utxo(&context, vec![1000]); - provider.wait(&txid)?; - - let asset_amount = 1; - let (txid, asset_id) = issue_asset(&context, asset_amount)?; - provider.wait(&txid)?; - - let asset_auth_parameters = AssetAuthParameters { - asset_id, - asset_amount, - with_asset_burn: false, - network: *context.get_network(), - }; - - let (txid, asset_auth) = create_asset_auth_tx(&context, asset_auth_parameters)?; - provider.wait(&txid)?; - - let txid = unlock_asset_auth_tx(&context, asset_auth)?; - provider.wait(&txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/asset_auth/mod.rs b/crates/contracts/tests/asset_auth/mod.rs index 9a4bab8..56abbcf 100644 --- a/crates/contracts/tests/asset_auth/mod.rs +++ b/crates/contracts/tests/asset_auth/mod.rs @@ -1,5 +1,6 @@ #[path = "../common/mod.rs"] mod common; -mod burn; -mod happy_path; +mod setup; +mod unlock_failure_flows; +mod unlock_success_flows; diff --git a/crates/contracts/tests/asset_auth/setup.rs b/crates/contracts/tests/asset_auth/setup.rs new file mode 100644 index 0000000..b1f20b5 --- /dev/null +++ b/crates/contracts/tests/asset_auth/setup.rs @@ -0,0 +1,48 @@ +use lending_contracts::programs::asset_auth::{AssetAuth, AssetAuthParameters}; + +use simplex::transaction::FinalTransaction; + +use crate::asset_auth_tests::common::tx_steps::finalize_and_broadcast; + +use super::common::issuance::issue_asset; +use super::common::wallet::split_first_signer_utxo; + +pub(super) fn setup_asset_auth( + context: &simplex::TestContext, + asset_amount: u64, + with_asset_burn: bool, +) -> anyhow::Result<(AssetAuth, AssetAuthParameters)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let txid = split_first_signer_utxo(context, vec![1000]); + provider.wait(&txid)?; + + let (txid, asset_id) = issue_asset(context, asset_amount)?; + provider.wait(&txid)?; + + let asset_auth_parameters = AssetAuthParameters { + asset_id, + asset_amount, + with_asset_burn, + network: *context.get_network(), + }; + + let signer_utxos = signer.get_utxos_asset(provider.get_network().policy_asset())?; + let utxo_to_lock = signer_utxos.first().unwrap(); + + let mut ft = FinalTransaction::new(); + let asset_auth = AssetAuth::new(asset_auth_parameters); + + asset_auth.attach_creation( + &mut ft, + utxo_to_lock.explicit_asset(), + utxo_to_lock.explicit_amount(), + ); + + let txid = finalize_and_broadcast(context, &ft)?; + + provider.wait(&txid)?; + + Ok((asset_auth, asset_auth_parameters)) +} diff --git a/crates/contracts/tests/asset_auth/unlock_failure_flows.rs b/crates/contracts/tests/asset_auth/unlock_failure_flows.rs new file mode 100644 index 0000000..31575a6 --- /dev/null +++ b/crates/contracts/tests/asset_auth/unlock_failure_flows.rs @@ -0,0 +1,73 @@ +use lending_contracts::programs::asset_auth::{AssetAuthParameters, AssetAuthWitnessParams}; +use lending_contracts::programs::program::SimplexProgram; + +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; + +use crate::asset_auth_tests::common::tx_steps::finalize_and_broadcast; +use crate::asset_auth_tests::common::wallet::get_split_utxo_ft; + +use super::setup::setup_asset_auth; + +fn split_auth_utxo( + context: &simplex::TestContext, + amounts: Vec, + asset_auth_parameters: AssetAuthParameters, +) -> anyhow::Result<()> { + let signer = context.get_default_signer(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let ft = get_split_utxo_ft(auth_utxo, amounts, signer, *context.get_network()); + + let txid = finalize_and_broadcast(context, &ft)?; + context.get_default_provider().wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn fails_to_unlock_when_auth_input_amount_is_invalid( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let asset_amount = 1000; + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, asset_amount, false)?; + + let mut ft = FinalTransaction::new(); + + let new_amounts = vec![450, 550]; + split_auth_utxo(&context, new_amounts, asset_auth_parameters)?; + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input( + PartialInput::new(auth_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_utxo.explicit_amount(), + asset_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + auth_utxo.explicit_amount(), + auth_utxo.explicit_asset(), + )); + + // TODO: Update Simplex to write more fail test cases + // let result = signer.finalize(&ft); + + // assert!(result.is_err(), "Must fail but it does not"); + + Ok(()) +} diff --git a/crates/contracts/tests/asset_auth/unlock_success_flows.rs b/crates/contracts/tests/asset_auth/unlock_success_flows.rs new file mode 100644 index 0000000..75a5b52 --- /dev/null +++ b/crates/contracts/tests/asset_auth/unlock_success_flows.rs @@ -0,0 +1,218 @@ +use lending_contracts::programs::asset_auth::AssetAuthWitnessParams; +use lending_contracts::programs::program::SimplexProgram; + +use simplex::simplicityhl::elements::Script; +use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::setup::setup_asset_auth; + +#[simplex::test] +fn unlocks_without_burn_with_one_explicit_output( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, 1, false)?; + + let mut ft = FinalTransaction::new(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_utxo.explicit_amount(), + asset_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_parameters.asset_amount, + asset_auth_parameters.asset_id, + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn unlocks_without_burn_with_multiple_explicit_outputs( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, 1, false)?; + + let mut ft = FinalTransaction::new(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 0); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_parameters.asset_amount, + asset_auth_parameters.asset_id, + )); + + let first_locked_output_amount = asset_auth_utxo.explicit_amount() / 2; + let second_locked_output_amount = + asset_auth_utxo.explicit_amount() - first_locked_output_amount; + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + first_locked_output_amount, + asset_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + second_locked_output_amount, + asset_auth_utxo.explicit_asset(), + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn unlocks_without_burn_with_confidential_output( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, 1, false)?; + + let mut ft = FinalTransaction::new(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + ft.add_output( + PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_utxo.explicit_amount(), + asset_auth_utxo.explicit_asset(), + ) + .with_blinding_key(signer.get_blinding_public_key()), + ); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_parameters.asset_amount, + asset_auth_parameters.asset_id, + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn unlocks_with_burn_with_one_explicit_output(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, 1, true)?; + + let mut ft = FinalTransaction::new(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + asset_auth_utxo.explicit_amount(), + asset_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + asset_auth_parameters.asset_amount, + asset_auth_parameters.asset_id, + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn unlocks_with_burn_with_multiple_explicit_outputs( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (asset_auth, asset_auth_parameters) = setup_asset_auth(&context, 1, true)?; + + let mut ft = FinalTransaction::new(); + + let auth_utxo = signer.get_utxos_asset(asset_auth_parameters.asset_id)?[0].clone(); + + let asset_auth_utxo = + provider.fetch_scripthash_utxos(&asset_auth.get_script_pubkey())?[0].clone(); + let asset_auth_witness_params = AssetAuthWitnessParams::new(1, 1); + + asset_auth.attach_unlocking(&mut ft, asset_auth_utxo.clone(), asset_auth_witness_params); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + let first_locked_output_amount = asset_auth_utxo.explicit_amount() / 2; + let second_locked_output_amount = + asset_auth_utxo.explicit_amount() - first_locked_output_amount; + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + first_locked_output_amount, + asset_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + Script::new_op_return(b"burn"), + asset_auth_parameters.asset_amount, + asset_auth_parameters.asset_id, + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + second_locked_output_amount, + asset_auth_utxo.explicit_asset(), + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/common/asserts.rs b/crates/contracts/tests/common/asserts.rs deleted file mode 100644 index 7b47ad2..0000000 --- a/crates/contracts/tests/common/asserts.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(dead_code)] -use simplex::simplicityhl::elements::{AssetId, TxOut}; - -pub fn assert_burn_output(output: &TxOut, asset_id: AssetId, amount: u64) { - assert!(output.is_null_data()); - assert_eq!(output.asset.explicit().unwrap(), asset_id); - assert_eq!(output.value.explicit().unwrap(), amount); -} diff --git a/crates/contracts/tests/common/flows/mod.rs b/crates/contracts/tests/common/flows/mod.rs deleted file mode 100644 index 3842eb9..0000000 --- a/crates/contracts/tests/common/flows/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod pre_lock_flow; diff --git a/crates/contracts/tests/common/flows/pre_lock_flow.rs b/crates/contracts/tests/common/flows/pre_lock_flow.rs deleted file mode 100644 index 942cbda..0000000 --- a/crates/contracts/tests/common/flows/pre_lock_flow.rs +++ /dev/null @@ -1,272 +0,0 @@ -#![allow(dead_code)] -use lending_contracts::{ - programs::{Lending, PreLock, PreLockParameters}, - transactions::{ - core::SimplexInput, - pre_lock::{create_lending_from_pre_lock, create_pre_lock}, - }, - utils::LendingOfferParameters, -}; -use simplex::transaction::{PartialInput, PartialOutput, RequiredSignature, UTXO}; -use simplex::{ - simplicityhl::elements::{AssetId, OutPoint, Txid}, - utils::hash_script, -}; - -use super::super::issuance::{issue_asset, issue_preparation_utxos_tx, issue_utility_nfts_tx}; -use super::super::tx_steps::{finalize_and_broadcast, finalize_strict_and_broadcast, wait_for_tx}; -use super::super::wallet::{ - AmountFilter, filter_signer_utxos_by_asset_and_amount, filter_signer_utxos_by_asset_id, - filter_utxos_by_amount, get_split_utxo_ft, split_first_signer_utxo, -}; - -pub struct PreLockFixture { - pub pre_lock_txid: Txid, - pub pre_lock: PreLock, - pub offer_parameters: LendingOfferParameters, - pub principal_asset_id: AssetId, -} - -pub struct LendingFixture { - pub pre_lock_txid: Txid, - pub lending_txid: Txid, - pub lending: Lending, -} - -pub fn create_pre_lock_tx( - context: &simplex::TestContext, - offer_parameters: &LendingOfferParameters, - principal_asset_id: AssetId, - utility_nfts_issuance_txid: Txid, -) -> anyhow::Result<(Txid, PreLock)> { - let provider = context.get_default_provider(); - let network = context.get_network(); - let signer = context.get_default_signer(); - - let utility_nfts_tx = provider.fetch_transaction(&utility_nfts_issuance_txid)?; - let signer_schnorr_pubkey = signer.get_schnorr_public_key(); - let first_parameters_nft_asset_id = utility_nfts_tx.output[0].asset.explicit().unwrap(); - let second_parameters_nft_asset_id = utility_nfts_tx.output[1].asset.explicit().unwrap(); - let borrower_nft_asset_id = utility_nfts_tx.output[2].asset.explicit().unwrap(); - let lender_nft_asset_id = utility_nfts_tx.output[3].asset.explicit().unwrap(); - - let pre_lock_parameters = PreLockParameters { - collateral_asset_id: network.policy_asset(), - principal_asset_id, - first_parameters_nft_asset_id, - second_parameters_nft_asset_id, - borrower_nft_asset_id, - lender_nft_asset_id, - offer_parameters: *offer_parameters, - borrower_pubkey: signer_schnorr_pubkey, - borrower_output_script_hash: hash_script(&signer.get_address().script_pubkey()), - network: *network, - }; - - let collateral_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - network.policy_asset(), - offer_parameters.collateral_amount, - AmountFilter::GreaterThan, - ); - let collateral_utxos = - filter_utxos_by_amount(collateral_utxos, 100_000, AmountFilter::LessThan); - - assert!( - collateral_utxos.len() > 1, - "No UTXOs serving as collateral were found" - ); - let collateral_utxo = collateral_utxos.first().unwrap(); - - let (ft, pre_lock) = create_pre_lock( - &SimplexInput::new(collateral_utxo, RequiredSignature::NativeEcdsa), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 0), - txout: utility_nfts_tx.output[0].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 1), - txout: utility_nfts_tx.output[1].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 2), - txout: utility_nfts_tx.output[2].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - &SimplexInput::new( - &UTXO { - outpoint: OutPoint::new(utility_nfts_issuance_txid, 3), - txout: utility_nfts_tx.output[3].clone(), - secrets: None, - }, - RequiredSignature::NativeEcdsa, - ), - pre_lock_parameters, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - Ok((txid, pre_lock)) -} - -pub fn create_lending_from_pre_lock_tx( - context: &simplex::TestContext, - pre_lock: PreLock, - pre_lock_txid: Txid, -) -> anyhow::Result<(Txid, Lending)> { - let provider = context.get_default_provider(); - let network = context.get_network(); - let signer = context.get_default_signer(); - - let pre_lock_parameters = pre_lock.get_pre_lock_parameters(); - let principal_utxos = - filter_signer_utxos_by_asset_id(signer, pre_lock_parameters.principal_asset_id); - let utxo_to_split = principal_utxos.first().unwrap(); - let ft = get_split_utxo_ft( - utxo_to_split.clone(), - vec![pre_lock_parameters.offer_parameters.principal_amount], - signer, - *network, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - wait_for_tx(context, &txid)?; - - let principal_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - pre_lock_parameters.principal_asset_id, - pre_lock_parameters.offer_parameters.principal_amount, - AmountFilter::EqualTo, - ); - let principal_utxo = principal_utxos.first().unwrap(); - - let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_txid)?; - let (mut ft, lending) = create_lending_from_pre_lock( - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 0), - txout: pre_lock_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 1), - txout: pre_lock_creation_tx.output[1].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 2), - txout: pre_lock_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 3), - txout: pre_lock_creation_tx.output[3].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 4), - txout: pre_lock_creation_tx.output[4].clone(), - secrets: None, - }, - vec![&SimplexInput::new( - principal_utxo, - RequiredSignature::NativeEcdsa, - )], - PartialOutput::new( - signer.get_address().script_pubkey(), - 1, - pre_lock_parameters.lender_nft_asset_id, - ), - signer.get_address().script_pubkey(), - pre_lock, - ); - - let signer_policy_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - context.get_network().policy_asset(), - 100_000, - AmountFilter::LessThan, - ); - let fee_utxo = signer_policy_utxos.first().unwrap(); - - ft.add_input( - PartialInput::new(fee_utxo.clone()), - RequiredSignature::NativeEcdsa, - ); - - let txid = finalize_strict_and_broadcast(context, &ft)?; - Ok((txid, lending)) -} - -pub fn setup_pre_lock(context: &simplex::TestContext) -> anyhow::Result<(Txid, PreLock)> { - Ok(setup_pre_lock_fixture(context)?.into()) -} - -pub fn setup_pre_lock_fixture(context: &simplex::TestContext) -> anyhow::Result { - let txid = split_first_signer_utxo(context, vec![1000, 2000, 5000]); - wait_for_tx(context, &txid)?; - - let (txid, preparation_asset_id) = issue_preparation_utxos_tx(context)?; - wait_for_tx(context, &txid)?; - - let (txid, principal_asset_id) = issue_asset(context, 20000)?; - wait_for_tx(context, &txid)?; - - let current_height = context.get_default_provider().fetch_tip_height()?; - - let offer_parameters = LendingOfferParameters { - collateral_amount: 1000, - principal_amount: 5000, - loan_expiration_time: current_height + 10, - principal_interest_rate: 200, - }; - - let txid = issue_utility_nfts_tx(context, &offer_parameters, preparation_asset_id)?; - wait_for_tx(context, &txid)?; - - let (pre_lock_txid, pre_lock) = - create_pre_lock_tx(context, &offer_parameters, principal_asset_id, txid)?; - - Ok(PreLockFixture { - pre_lock_txid, - pre_lock, - offer_parameters, - principal_asset_id, - }) -} - -pub fn setup_lending_fixture(context: &simplex::TestContext) -> anyhow::Result { - let pre_lock_fixture = setup_pre_lock_fixture(context)?; - wait_for_tx(context, &pre_lock_fixture.pre_lock_txid)?; - - let PreLockFixture { - pre_lock_txid, - pre_lock, - offer_parameters: _offer_parameters, - principal_asset_id: _principal_asset_id, - } = pre_lock_fixture; - - let (lending_txid, lending) = - create_lending_from_pre_lock_tx(context, pre_lock, pre_lock_txid)?; - - Ok(LendingFixture { - pre_lock_txid, - lending_txid, - lending, - }) -} - -impl From for (Txid, PreLock) { - fn from(value: PreLockFixture) -> Self { - (value.pre_lock_txid, value.pre_lock) - } -} diff --git a/crates/contracts/tests/common/issuance.rs b/crates/contracts/tests/common/issuance.rs index cbbd6dc..fe34aaa 100644 --- a/crates/contracts/tests/common/issuance.rs +++ b/crates/contracts/tests/common/issuance.rs @@ -1,8 +1,5 @@ #![allow(dead_code)] -use lending_contracts::transactions::core::SimplexInput; -use lending_contracts::transactions::utility::{ - UTILITY_NFTS_COUNT, issue_preparation_utxos, issue_utility_nfts, -}; +use lending_contracts::programs::pre_lock::UTILITY_NFTS_COUNT; use lending_contracts::utils::{LendingOfferParameters, get_random_seed}; use simplex::simplicityhl::elements::{AssetId, Txid}; @@ -10,12 +7,9 @@ use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, RequiredSignature, partial_input::IssuanceInput, }; -use super::{ - tx_steps::{finalize_and_broadcast, finalize_strict_and_broadcast}, - wallet::{ - AmountFilter, filter_signer_utxos_by_asset_and_amount, filter_signer_utxos_by_asset_id, - }, -}; +use super::tx_steps::{finalize_and_broadcast, finalize_strict_and_broadcast}; + +pub const PREPARATION_UTXO_ASSET_AMOUNT: u64 = 10; pub fn issue_asset( context: &simplex::TestContext, @@ -25,13 +19,11 @@ pub fn issue_asset( let mut ft = FinalTransaction::new(); - let policy_utxos = - filter_signer_utxos_by_asset_id(signer, context.get_network().policy_asset()); - let first_utxo = policy_utxos.first().unwrap(); + let first_utxo = signer.get_utxos_asset(context.get_network().policy_asset())?[0].clone(); let asset_entropy = get_random_seed(); - let asset_id = ft.add_issuance_input( + let (asset_id, _) = ft.add_issuance_input( PartialInput::new(first_utxo.clone()), IssuanceInput::new(asset_amount, asset_entropy), RequiredSignature::NativeEcdsa, @@ -61,17 +53,35 @@ pub fn issue_preparation_utxos_tx( ) -> anyhow::Result<(Txid, AssetId)> { let signer = context.get_default_signer(); - let signer_script_pubkey = signer.get_address().script_pubkey(); + let first_utxo = signer.get_utxos()?[0].clone(); + + let mut ft = FinalTransaction::new(); - let signer_utxos = signer.get_utxos().unwrap(); - let first_utxo = signer_utxos.first().unwrap(); + let total_asset_amount = PREPARATION_UTXO_ASSET_AMOUNT * UTILITY_NFTS_COUNT as u64; + let asset_entropy = get_random_seed(); - let (ft, asset_id) = issue_preparation_utxos( - &SimplexInput::new(first_utxo, RequiredSignature::NativeEcdsa), - signer_script_pubkey, - *context.get_network(), + let (asset_id, _) = ft.add_issuance_input( + PartialInput::new(first_utxo.clone()), + IssuanceInput::new(total_asset_amount, asset_entropy), + RequiredSignature::NativeEcdsa, ); + for _ in 0..UTILITY_NFTS_COUNT { + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + PREPARATION_UTXO_ASSET_AMOUNT, + asset_id, + )); + } + + if first_utxo.explicit_asset() != context.get_network().policy_asset() { + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + first_utxo.explicit_amount(), + first_utxo.explicit_asset(), + )); + } + let txid = finalize_and_broadcast(context, &ft)?; Ok((txid, asset_id)) @@ -85,30 +95,58 @@ pub fn issue_utility_nfts_tx( let signer = context.get_default_signer(); let signer_script_pubkey = signer.get_address().script_pubkey(); - let issuance_utxos = filter_signer_utxos_by_asset_id(signer, preparation_asset_id); + let issuance_utxos = signer.get_utxos_asset(preparation_asset_id)?; assert_eq!(issuance_utxos.len(), UTILITY_NFTS_COUNT); - let issuance_inputs = issuance_utxos - .iter() - .map(|utxo| SimplexInput::new(utxo, RequiredSignature::NativeEcdsa)) - .collect(); + let mut ft = FinalTransaction::new(); - let issuance_asset_entropy = get_random_seed(); - let mut ft = issue_utility_nfts( - issuance_inputs, - signer_script_pubkey, - offer_params, + let (first_parameters_nft_amount, second_parameters_nft_amount) = + offer_params.encode_parameters_nft_amounts(1)?; + + let utility_nfts_amounts = [ + first_parameters_nft_amount, + second_parameters_nft_amount, 1, - issuance_asset_entropy, + 1, + ]; + let mut asset_ids: Vec = Vec::with_capacity(UTILITY_NFTS_COUNT); + + let issuance_asset_entropy = get_random_seed(); + + for (index, utxo) in issuance_utxos.iter().enumerate() { + let (asset_id, _) = ft.add_issuance_input( + PartialInput::new(utxo.clone()), + IssuanceInput::new(utility_nfts_amounts[index], issuance_asset_entropy), + RequiredSignature::NativeEcdsa, + ); + asset_ids.push(asset_id); + } + + for (index, asset_id) in asset_ids.into_iter().enumerate() { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + utility_nfts_amounts[index], + asset_id, + )); + } + + for utxo in issuance_utxos { + ft.add_output(PartialOutput::new( + signer_script_pubkey.clone(), + utxo.explicit_amount(), + utxo.explicit_asset(), + )); + } + + let signer_policy_utxos = signer.get_utxos_filter( + &|utxo| { + utxo.explicit_asset() == context.get_network().policy_asset() + && utxo.explicit_amount() <= 100_000 + }, + &|_| true, )?; - let signer_policy_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - context.get_network().policy_asset(), - 100_000, - AmountFilter::LessThan, - ); let fee_utxo = signer_policy_utxos.first().unwrap(); ft.add_input( diff --git a/crates/contracts/tests/common/mod.rs b/crates/contracts/tests/common/mod.rs index 08bd179..263bdbd 100644 --- a/crates/contracts/tests/common/mod.rs +++ b/crates/contracts/tests/common/mod.rs @@ -1,5 +1,3 @@ -pub mod asserts; -pub mod flows; pub mod issuance; pub mod tx_steps; pub mod wallet; diff --git a/crates/contracts/tests/common/wallet.rs b/crates/contracts/tests/common/wallet.rs index a3205fa..6b00ce4 100644 --- a/crates/contracts/tests/common/wallet.rs +++ b/crates/contracts/tests/common/wallet.rs @@ -2,71 +2,11 @@ use super::tx_steps::finalize_and_broadcast; use simplex::provider::SimplicityNetwork; use simplex::signer::Signer; -use simplex::simplicityhl::elements::{AssetId, Txid}; +use simplex::simplicityhl::elements::Txid; use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, }; -pub enum AmountFilter { - LessThan, - GreaterThan, - EqualTo, -} - -pub fn filter_signer_utxos_by_asset_and_amount( - signer: &Signer, - asset_id: AssetId, - amount: u64, - amount_filter: AmountFilter, -) -> Vec { - let signer_utxos = signer.get_utxos().unwrap(); - - let filtered_utxos = filter_utxos_by_asset_id(signer_utxos, asset_id); - filter_utxos_by_amount(filtered_utxos, amount, amount_filter) -} - -pub fn filter_signer_utxos_by_asset_id(signer: &Signer, asset_id: AssetId) -> Vec { - let signer_utxos = signer.get_utxos().unwrap(); - - filter_utxos_by_asset_id(signer_utxos, asset_id) -} - -pub fn filter_signer_utxos_by_amount( - signer: &Signer, - amount: u64, - amount_filter: AmountFilter, -) -> Vec { - let signer_utxos = signer.get_utxos().unwrap(); - - filter_utxos_by_amount(signer_utxos, amount, amount_filter) -} - -pub fn filter_utxos_by_amount( - utxos: Vec, - amount: u64, - amount_filter: AmountFilter, -) -> Vec { - let filtered_utxos: Vec = utxos - .into_iter() - .filter(|utxo| match amount_filter { - AmountFilter::LessThan => utxo.explicit_amount() < amount, - AmountFilter::GreaterThan => utxo.explicit_amount() > amount, - AmountFilter::EqualTo => utxo.explicit_amount() == amount, - }) - .collect(); - - filtered_utxos -} - -pub fn filter_utxos_by_asset_id(utxos: Vec, asset_id: AssetId) -> Vec { - let filtered_utxos: Vec = utxos - .into_iter() - .filter(|utxo| utxo.explicit_asset() == asset_id) - .collect(); - - filtered_utxos -} - pub fn get_split_utxo_ft( utxo: UTXO, amounts: Vec, diff --git a/crates/contracts/tests/lending/claim.rs b/crates/contracts/tests/lending/claim.rs deleted file mode 100644 index 3dc85f9..0000000 --- a/crates/contracts/tests/lending/claim.rs +++ /dev/null @@ -1,34 +0,0 @@ -use simplex::simplicityhl::elements::OutPoint; - -use crate::lending_tests::support::{claim_lender_principal, repay_lending_tx}; - -use super::support::setup_lending_fixture; - -#[simplex::test] -fn lender_principal_claim_flow(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - let fixture = setup_lending_fixture(&context)?; - - provider.wait(&fixture.lending_txid)?; - - let lending_parameters = *fixture.lending.get_lending_parameters(); - let txid = repay_lending_tx(&context, fixture.lending, fixture.lending_txid)?; - - provider.wait(&txid)?; - - let txid = claim_lender_principal(&context, &lending_parameters, txid)?; - - provider.wait(&txid)?; - - let principal_outpoint = OutPoint::new(txid, 0); - let signer_principal_utxos = context - .get_default_signer() - .get_utxos_filter(&|utxo| utxo.outpoint == principal_outpoint, &|_| true)?; - - assert!( - signer_principal_utxos.len() == 1, - "Failed to find claimed principal UTXO" - ); - - Ok(()) -} diff --git a/crates/contracts/tests/lending/liquidate.rs b/crates/contracts/tests/lending/liquidate.rs deleted file mode 100644 index 9bed344..0000000 --- a/crates/contracts/tests/lending/liquidate.rs +++ /dev/null @@ -1,67 +0,0 @@ -use simplex::simplicityhl::elements::OutPoint; - -use crate::lending_tests::{ - common::tx_steps::finalize_strict_and_broadcast, support::get_lending_liquidation_tx, -}; - -use super::support::{mine_until_height, setup_lending_fixture}; - -#[simplex::test] -fn happy_liquidation_flow(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - let fixture = setup_lending_fixture(&context)?; - - provider.wait(&fixture.lending_txid)?; - - let lending_parameters = fixture.lending.get_lending_parameters(); - mine_until_height( - &context, - lending_parameters.offer_parameters.loan_expiration_time + 1, - )?; - - println!("Current height - {}", provider.fetch_tip_height()?); - - let ft = get_lending_liquidation_tx(&context, fixture.lending, fixture.lending_txid)?; - let txid = finalize_strict_and_broadcast(&context, &ft)?; - - provider.wait(&txid)?; - - let collateral_outpoint = OutPoint::new(txid, 0); - let signer_collateral_utxos = context - .get_default_signer() - .get_utxos_filter(&|utxo| utxo.outpoint == collateral_outpoint, &|_| true)?; - - assert!( - signer_collateral_utxos.len() == 1, - "Failed to find collateral UTXO" - ); - - Ok(()) -} - -#[simplex::test] -fn failed_liquidation_flow(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - let fixture = setup_lending_fixture(&context)?; - - provider.wait(&fixture.lending_txid)?; - - let lending_parameters = fixture.lending.get_lending_parameters(); - - assert!( - provider.fetch_tip_height()? < lending_parameters.offer_parameters.loan_expiration_time - ); - - let ft = get_lending_liquidation_tx(&context, fixture.lending, fixture.lending_txid)?; - - let (tx, _) = signer.finalize_strict(&ft, 1).unwrap(); - let result = provider.broadcast_transaction(&tx); - - assert!( - result.is_err(), - "Expected liquidation to fail but it succeeded" - ); - - Ok(()) -} diff --git a/crates/contracts/tests/lending/loan_liquidation_failure_flows.rs b/crates/contracts/tests/lending/loan_liquidation_failure_flows.rs new file mode 100644 index 0000000..9791375 --- /dev/null +++ b/crates/contracts/tests/lending/loan_liquidation_failure_flows.rs @@ -0,0 +1,75 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::OutPoint; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; + +use super::setup::setup_lending; + +#[simplex::test] +fn fails_to_liquidate_loan_before_expiration(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let principal_asset_amount = 15000; + let current_height = provider.fetch_tip_height()?; + + let offer_parameters = LendingOfferParameters { + collateral_amount: 3000, + principal_amount: 10000, + loan_expiration_time: current_height + 15, + principal_interest_rate: 1000, + }; + + let (lending_creation_txid, lending, lending_parameters) = + setup_lending(&context, offer_parameters, principal_asset_amount)?; + + let lending_utxo = provider.fetch_scripthash_utxos(&lending.get_script_pubkey())?[0].clone(); + + let lending_creation_tx = provider.fetch_transaction(&lending_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_liquidation( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + let lender_nft_utxo = + signer.get_utxos_asset(lending_parameters.lender_nft_asset_id)?[0].clone(); + + ft.add_input( + PartialInput::new(lender_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + + let (tx, _) = signer.finalize(&ft)?; + let result = provider.broadcast_transaction(&tx); + + assert!( + result.is_err(), + "Expected liquidation to fail but it succeeded" + ); + + Ok(()) +} diff --git a/crates/contracts/tests/lending/loan_liquidation_success_flows.rs b/crates/contracts/tests/lending/loan_liquidation_success_flows.rs new file mode 100644 index 0000000..a34c92f --- /dev/null +++ b/crates/contracts/tests/lending/loan_liquidation_success_flows.rs @@ -0,0 +1,77 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::OutPoint; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::setup::{mine_until_height, setup_lending}; + +#[simplex::test] +fn liquidates_expired_loan(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let principal_asset_amount = 15000; + let current_height = provider.fetch_tip_height()?; + + let offer_parameters = LendingOfferParameters { + collateral_amount: 3000, + principal_amount: 10000, + loan_expiration_time: current_height + 15, + principal_interest_rate: 1000, + }; + + let (lending_creation_txid, lending, lending_parameters) = + setup_lending(&context, offer_parameters, principal_asset_amount)?; + + mine_until_height( + &context, + lending_parameters.offer_parameters.loan_expiration_time + 1, + )?; + + let lending_utxo = provider.fetch_scripthash_utxos(&lending.get_script_pubkey())?[0].clone(); + + let lending_creation_tx = provider.fetch_transaction(&lending_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_liquidation( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + let lender_nft_utxo = + signer.get_utxos_asset(lending_parameters.lender_nft_asset_id)?[0].clone(); + + ft.add_input( + PartialInput::new(lender_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/lending/loan_repayment_success_flows.rs b/crates/contracts/tests/lending/loan_repayment_success_flows.rs new file mode 100644 index 0000000..89b5545 --- /dev/null +++ b/crates/contracts/tests/lending/loan_repayment_success_flows.rs @@ -0,0 +1,289 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::OutPoint; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::common::wallet::get_split_utxo_ft; +use super::setup::setup_lending; + +#[simplex::test] +fn repays_loan_with_single_principal_input(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + 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, + }; + + let (lending_creation_txid, lending, lending_parameters) = + setup_lending(&context, offer_parameters, principal_asset_amount)?; + + let lending_utxo = provider.fetch_scripthash_utxos(&lending.get_script_pubkey())?[0].clone(); + + let lending_creation_tx = provider.fetch_transaction(&lending_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_repayment( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + let borrower_nft_utxo = + signer.get_utxos_asset(lending_parameters.borrower_nft_asset_id)?[0].clone(); + let principal_utxo = signer.get_utxos_asset(lending_parameters.principal_asset_id)?[0].clone(); + + ft.add_input( + PartialInput::new(borrower_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + + let principal_with_interest = lending_parameters + .offer_parameters + .calculate_principal_with_interest(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + principal_asset_amount - principal_with_interest, + lending_parameters.principal_asset_id, + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn repays_loan_with_multiple_principal_inputs(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let principal_asset_amount = 15000; + 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, + }; + + let (lending_creation_txid, lending, lending_parameters) = + setup_lending(&context, offer_parameters, principal_asset_amount)?; + + let principal_utxo = signer.get_utxos_asset(lending_parameters.principal_asset_id)?[0].clone(); + + let ft = get_split_utxo_ft( + principal_utxo, + vec![5000, 5000, 5000], + signer, + *provider.get_network(), + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + let lending_utxo = provider.fetch_scripthash_utxos(&lending.get_script_pubkey())?[0].clone(); + + let lending_creation_tx = provider.fetch_transaction(&lending_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_repayment( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + let borrower_nft_utxo = + signer.get_utxos_asset(lending_parameters.borrower_nft_asset_id)?[0].clone(); + let principal_utxos = signer.get_utxos_asset(lending_parameters.principal_asset_id)?; + + ft.add_input( + PartialInput::new(borrower_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + + for principal_utxo in principal_utxos { + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + } + + let principal_with_interest = lending_parameters + .offer_parameters + .calculate_principal_with_interest(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + principal_asset_amount - principal_with_interest, + lending_parameters.principal_asset_id, + )); + + let txid = finalize_and_broadcast(&context, &ft)?; + + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn repays_loan_with_confidential_principal_input_and_confidential_change( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let principal_asset_amount = 15000; + 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, + }; + + let (lending_creation_txid, lending, lending_parameters) = + setup_lending(&context, offer_parameters, principal_asset_amount)?; + + let principal_utxo = signer.get_utxos_asset(lending_parameters.principal_asset_id)?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + ft.add_output( + PartialOutput::new( + signer.get_address().script_pubkey(), + principal_asset_amount, + lending_parameters.principal_asset_id, + ) + .with_blinding_key(signer.get_blinding_public_key()), + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + let lending_utxo = provider.fetch_scripthash_utxos(&lending.get_script_pubkey())?[0].clone(); + + let lending_creation_tx = provider.fetch_transaction(&lending_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 1), + txout: lending_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(lending_creation_txid, 2), + txout: lending_creation_tx.output[2].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + lending_parameters.offer_parameters.collateral_amount, + lending_parameters.collateral_asset_id, + )); + + lending.attach_loan_repayment( + &mut ft, + lending_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + ); + + let borrower_nft_utxo = + signer.get_utxos_asset(lending_parameters.borrower_nft_asset_id)?[0].clone(); + let principal_utxo = signer.get_utxos_asset(lending_parameters.principal_asset_id)?[0].clone(); + + assert!( + principal_utxo.secrets.is_some(), + "Not a confidential principal UTXO" + ); + + ft.add_input( + PartialInput::new(borrower_nft_utxo), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + + let principal_with_interest = lending_parameters + .offer_parameters + .calculate_principal_with_interest(); + + ft.add_output( + PartialOutput::new( + signer.get_address().script_pubkey(), + principal_asset_amount - principal_with_interest, + lending_parameters.principal_asset_id, + ) + .with_blinding_key(signer.get_blinding_public_key()), + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/lending/mod.rs b/crates/contracts/tests/lending/mod.rs index 17bb20d..f7b9e59 100644 --- a/crates/contracts/tests/lending/mod.rs +++ b/crates/contracts/tests/lending/mod.rs @@ -1,7 +1,7 @@ #[path = "../common/mod.rs"] mod common; -mod claim; -mod liquidate; -mod repay; -mod support; +mod loan_liquidation_failure_flows; +mod loan_liquidation_success_flows; +mod loan_repayment_success_flows; +mod setup; diff --git a/crates/contracts/tests/lending/repay.rs b/crates/contracts/tests/lending/repay.rs deleted file mode 100644 index f1dd4be..0000000 --- a/crates/contracts/tests/lending/repay.rs +++ /dev/null @@ -1,29 +0,0 @@ -use simplex::simplicityhl::elements::OutPoint; - -use crate::lending_tests::support::repay_lending_tx; - -use super::support::setup_lending_fixture; - -#[simplex::test] -fn loan_repayment_flow(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - let fixture = setup_lending_fixture(&context)?; - - provider.wait(&fixture.lending_txid)?; - - let txid = repay_lending_tx(&context, fixture.lending, fixture.lending_txid)?; - - provider.wait(&txid)?; - - let collateral_outpoint = OutPoint::new(txid, 0); - let signer_collateral_utxos = context - .get_default_signer() - .get_utxos_filter(&|utxo| utxo.outpoint == collateral_outpoint, &|_| true)?; - - assert!( - signer_collateral_utxos.len() == 1, - "Failed to find collateral UTXO" - ); - - Ok(()) -} diff --git a/crates/contracts/tests/lending/setup.rs b/crates/contracts/tests/lending/setup.rs new file mode 100644 index 0000000..2720569 --- /dev/null +++ b/crates/contracts/tests/lending/setup.rs @@ -0,0 +1,101 @@ +use lending_contracts::programs::lending::Lending; +use lending_contracts::{programs::lending::LendingParameters, utils::LendingOfferParameters}; +use simplex::simplicityhl::elements::Txid; +use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature}; + +use super::common::issuance::{issue_asset, issue_preparation_utxos_tx, issue_utility_nfts_tx}; +use super::common::tx_steps::{finalize_and_broadcast, mine_blocks_with_self_send}; +use super::common::wallet::split_first_signer_utxo; + +pub(super) fn mine_until_height( + context: &simplex::TestContext, + target_height: u32, +) -> anyhow::Result<()> { + let current_height = context.get_default_provider().fetch_tip_height()?; + if current_height < target_height { + let blocks_to_mine = target_height - current_height; + let _ = mine_blocks_with_self_send(context, blocks_to_mine, 1_000)?; + } + + Ok(()) +} + +pub(super) fn setup_lending( + context: &simplex::TestContext, + offer_parameters: LendingOfferParameters, + principal_asset_amount: u64, +) -> anyhow::Result<(Txid, Lending, LendingParameters)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let txid = split_first_signer_utxo( + context, + vec![1000, 2000, offer_parameters.collateral_amount], + ); + provider.wait(&txid)?; + + let (txid, principal_asset_id) = issue_asset(context, principal_asset_amount)?; + provider.wait(&txid)?; + + let (txid, preparation_asset_id) = issue_preparation_utxos_tx(context)?; + provider.wait(&txid)?; + + let txid = issue_utility_nfts_tx(context, &offer_parameters, preparation_asset_id)?; + provider.wait(&txid)?; + + let utility_nfts_creation_tx = provider.fetch_transaction(&txid)?; + + let first_parameters_nft_asset_id = + utility_nfts_creation_tx.output[0].asset.explicit().unwrap(); + let second_parameters_nft_asset_id = + utility_nfts_creation_tx.output[1].asset.explicit().unwrap(); + let borrower_nft_asset_id = utility_nfts_creation_tx.output[2].asset.explicit().unwrap(); + let lender_nft_asset_id = utility_nfts_creation_tx.output[3].asset.explicit().unwrap(); + + let lending_parameters = LendingParameters { + collateral_asset_id: provider.get_network().policy_asset(), + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + offer_parameters, + principal_asset_id, + network: *provider.get_network(), + }; + + let lending = Lending::new(lending_parameters); + + let collateral_utxo = signer.get_utxos_filter( + &|utxo| { + utxo.explicit_asset() == lending_parameters.collateral_asset_id + && utxo.explicit_amount() >= lending_parameters.offer_parameters.collateral_amount + }, + &|_| true, + )?[0] + .clone(); + + let first_parameters_utxo = signer.get_utxos_asset(first_parameters_nft_asset_id)?[0].clone(); + let second_parameters_utxo = signer.get_utxos_asset(second_parameters_nft_asset_id)?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(collateral_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(first_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(second_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + lending.attach_creation(&mut ft, first_parameters_utxo, second_parameters_utxo); + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok((txid, lending, lending_parameters)) +} diff --git a/crates/contracts/tests/lending/support.rs b/crates/contracts/tests/lending/support.rs deleted file mode 100644 index ef627ec..0000000 --- a/crates/contracts/tests/lending/support.rs +++ /dev/null @@ -1,216 +0,0 @@ -use lending_contracts::{ - programs::{Lending, LendingParameters}, - transactions::{ - asset_auth::unlock_asset_auth, - core::SimplexInput, - lending::{liquidate_loan, repay_loan}, - }, -}; -use simplex::{ - TestContext, - simplicityhl::elements::{OutPoint, Txid}, - transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO}, -}; - -use crate::lending_tests::common::{ - tx_steps::{finalize_and_broadcast, finalize_strict_and_broadcast, mine_blocks_with_self_send}, - wallet::{AmountFilter, filter_signer_utxos_by_asset_and_amount}, -}; - -pub(super) use super::common::flows::pre_lock_flow::setup_lending_fixture; - -pub(super) fn mine_until_height(context: &TestContext, target_height: u32) -> anyhow::Result<()> { - let current_height = context.get_default_provider().fetch_tip_height()?; - if current_height < target_height { - let blocks_to_mine = target_height - current_height; - let _ = mine_blocks_with_self_send(context, blocks_to_mine, 1_000)?; - } - - Ok(()) -} - -pub(super) fn repay_lending_tx( - context: &TestContext, - lending: Lending, - lending_txid: Txid, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let lending_parameters = lending.get_lending_parameters(); - let lending_creation_tx = provider.fetch_transaction(&lending_txid)?; - - let borrower_nft_utxos = signer.get_utxos_asset(lending_parameters.borrower_nft_asset_id)?; - - assert!( - borrower_nft_utxos.len() == 1, - "Invalid BorrowerNFT UTXOs count" - ); - - let borrower_nft_utxo = borrower_nft_utxos.first().unwrap(); - - let principal_utxos = signer.get_utxos_asset(lending_parameters.principal_asset_id)?; - - let mut principal_inputs: Vec = Vec::new(); - let mut total_inputs_amount = 0; - - let principal_with_interest = lending_parameters - .offer_parameters - .calculate_principal_with_interest(); - - for utxo in principal_utxos { - let input = SimplexInput::new(&utxo, RequiredSignature::NativeEcdsa); - - total_inputs_amount += input.explicit_amount(); - principal_inputs.push(input); - - if total_inputs_amount >= principal_with_interest { - break; - } - } - - let mut ft = repay_loan( - UTXO { - outpoint: OutPoint::new(lending_txid, 0), - txout: lending_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_txid, 2), - txout: lending_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_txid, 3), - txout: lending_creation_tx.output[3].clone(), - secrets: None, - }, - &SimplexInput::new(borrower_nft_utxo, RequiredSignature::NativeEcdsa), - principal_inputs, - PartialOutput::new( - signer.get_address().script_pubkey(), - lending_parameters.offer_parameters.collateral_amount, - lending_parameters.collateral_asset_id, - ), - lending, - )?; - - let signer_policy_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - context.get_network().policy_asset(), - 100_000, - AmountFilter::LessThan, - ); - let fee_utxo = signer_policy_utxos.first().unwrap(); - - ft.add_input( - PartialInput::new(fee_utxo.clone()), - RequiredSignature::NativeEcdsa, - ); - - finalize_strict_and_broadcast(context, &ft) -} - -pub(super) fn get_lending_liquidation_tx( - context: &TestContext, - lending: Lending, - lending_txid: Txid, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let lending_parameters = lending.get_lending_parameters(); - let lending_creation_tx = provider.fetch_transaction(&lending_txid)?; - - let lender_nft_utxos = signer.get_utxos_asset(lending_parameters.lender_nft_asset_id)?; - - assert!( - lender_nft_utxos.len() == 1, - "Invalid BorrowerNFT UTXOs count" - ); - - let lender_nft_utxo = lender_nft_utxos.first().unwrap(); - - let mut ft = liquidate_loan( - UTXO { - outpoint: OutPoint::new(lending_txid, 0), - txout: lending_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_txid, 2), - txout: lending_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(lending_txid, 3), - txout: lending_creation_tx.output[3].clone(), - secrets: None, - }, - &SimplexInput::new(lender_nft_utxo, RequiredSignature::NativeEcdsa), - PartialOutput::new( - signer.get_address().script_pubkey(), - lending_parameters.offer_parameters.collateral_amount, - lending_parameters.collateral_asset_id, - ), - lending, - )?; - - let signer_policy_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - context.get_network().policy_asset(), - 100_000, - AmountFilter::LessThan, - ); - let fee_utxo = signer_policy_utxos.first().unwrap(); - - ft.add_input( - PartialInput::new(fee_utxo.clone()), - RequiredSignature::NativeEcdsa, - ); - - Ok(ft) -} - -pub(super) fn claim_lender_principal( - context: &TestContext, - lending_parameters: &LendingParameters, - repayment_txid: Txid, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let loan_repayment_tx = provider.fetch_transaction(&repayment_txid)?; - - let lender_nft_utxos = signer.get_utxos_asset(lending_parameters.lender_nft_asset_id)?; - - assert!( - lender_nft_utxos.len() == 1, - "Invalid BorrowerNFT UTXOs count" - ); - - let lender_nft_utxo = lender_nft_utxos.first().unwrap(); - - let principal_asset_auth = lending_parameters.get_lender_principal_asset_auth(); - - let principal_with_interest = lending_parameters - .offer_parameters - .calculate_principal_with_interest(); - - let ft = unlock_asset_auth( - UTXO { - outpoint: OutPoint::new(repayment_txid, 1), - txout: loan_repayment_tx.output[1].clone(), - secrets: None, - }, - &SimplexInput::new(lender_nft_utxo, RequiredSignature::NativeEcdsa), - PartialOutput::new( - signer.get_address().script_pubkey(), - principal_with_interest, - lending_parameters.principal_asset_id, - ), - principal_asset_auth, - ); - - finalize_and_broadcast(context, &ft) -} diff --git a/crates/contracts/tests/ownable_script_auth.rs b/crates/contracts/tests/ownable_script_auth.rs index 77db16b..9805816 100644 --- a/crates/contracts/tests/ownable_script_auth.rs +++ b/crates/contracts/tests/ownable_script_auth.rs @@ -1,2 +1,2 @@ #[path = "ownable_script_auth/mod.rs"] -mod script_auth_tests; +mod ownable_script_auth_tests; diff --git a/crates/contracts/tests/ownable_script_auth/mod.rs b/crates/contracts/tests/ownable_script_auth/mod.rs index 5219d0a..97e1c01 100644 --- a/crates/contracts/tests/ownable_script_auth/mod.rs +++ b/crates/contracts/tests/ownable_script_auth/mod.rs @@ -1,6 +1,6 @@ #[path = "../common/mod.rs"] mod common; -mod ownership_transfer; -mod script_auth_unlock; -mod support; +mod ownership_transfer_success_flows; +mod setup; +mod unlock_success_flows; diff --git a/crates/contracts/tests/ownable_script_auth/ownership_transfer.rs b/crates/contracts/tests/ownable_script_auth/ownership_transfer.rs deleted file mode 100644 index 8b0c016..0000000 --- a/crates/contracts/tests/ownable_script_auth/ownership_transfer.rs +++ /dev/null @@ -1,27 +0,0 @@ -use simplex::utils::hash_script; - -use crate::script_auth_tests::{ - common::{tx_steps::wait_for_tx, wallet::split_first_signer_utxo}, - support::{create_ownable_script_auth_tx, ownership_transfer_tx}, -}; - -#[simplex::test] -fn ownable_script_auth_ownership_transfer(context: simplex::TestContext) -> anyhow::Result<()> { - let alice = context.get_default_signer(); - let bob = context - .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); - - let txid = split_first_signer_utxo(&context, vec![1000, 5000, 10000]); - wait_for_tx(&context, &txid)?; - - let signer_script_pubkey = alice.get_address().script_pubkey(); - let signer_script_hash = hash_script(&signer_script_pubkey); - - let (txid, ownable_script_auth) = create_ownable_script_auth_tx(&context, signer_script_hash)?; - wait_for_tx(&context, &txid)?; - - let txid = ownership_transfer_tx(&context, bob.get_schnorr_public_key(), ownable_script_auth)?; - wait_for_tx(&context, &txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/ownable_script_auth/ownership_transfer_success_flows.rs b/crates/contracts/tests/ownable_script_auth/ownership_transfer_success_flows.rs new file mode 100644 index 0000000..653a042 --- /dev/null +++ b/crates/contracts/tests/ownable_script_auth/ownership_transfer_success_flows.rs @@ -0,0 +1,59 @@ +use lending_contracts::programs::program::SimplexProgram; +use simplex::transaction::FinalTransaction; + +use super::common::tx_steps::finalize_and_broadcast; +use super::setup::setup_ownable_script_auth; + +#[simplex::test] +fn transfers_ownership_several_times(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + let bob = context + .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); + + let txid = alice.send(bob.get_address().script_pubkey(), 500)?; + provider.wait(&txid)?; + + let (mut ownable_script_auth, _) = setup_ownable_script_auth(&context)?; + + let ownable_script_auth_utxo = + provider.fetch_scripthash_utxos(&ownable_script_auth.get_script_pubkey())?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ownable_script_auth.attach_ownership_transfer( + &mut ft, + ownable_script_auth_utxo, + bob.get_schnorr_public_key(), + ); + + assert!( + ownable_script_auth.get_parameters().owner_pubkey == bob.get_schnorr_public_key(), + "Failed to transfer ownership" + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + let ownable_script_auth_utxo = + provider.fetch_scripthash_utxos(&ownable_script_auth.get_script_pubkey())?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ownable_script_auth.attach_ownership_transfer( + &mut ft, + ownable_script_auth_utxo, + alice.get_schnorr_public_key(), + ); + + assert!( + ownable_script_auth.get_parameters().owner_pubkey == alice.get_schnorr_public_key(), + "Failed to transfer ownership" + ); + + let (tx, _) = bob.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/ownable_script_auth/script_auth_unlock.rs b/crates/contracts/tests/ownable_script_auth/script_auth_unlock.rs deleted file mode 100644 index 508eed4..0000000 --- a/crates/contracts/tests/ownable_script_auth/script_auth_unlock.rs +++ /dev/null @@ -1,25 +0,0 @@ -use simplex::utils::hash_script; - -use crate::script_auth_tests::{ - common::{tx_steps::wait_for_tx, wallet::split_first_signer_utxo}, - support::{create_ownable_script_auth_tx, script_auth_unlock_tx}, -}; - -#[simplex::test] -fn ownable_script_auth_unlock(context: simplex::TestContext) -> anyhow::Result<()> { - let alice = context.get_default_signer(); - - let txid = split_first_signer_utxo(&context, vec![1000, 5000, 10000]); - wait_for_tx(&context, &txid)?; - - let signer_script_pubkey = alice.get_address().script_pubkey(); - let signer_script_hash = hash_script(&signer_script_pubkey); - - let (txid, ownable_script_auth) = create_ownable_script_auth_tx(&context, signer_script_hash)?; - wait_for_tx(&context, &txid)?; - - let txid = script_auth_unlock_tx(&context, ownable_script_auth)?; - wait_for_tx(&context, &txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/ownable_script_auth/setup.rs b/crates/contracts/tests/ownable_script_auth/setup.rs new file mode 100644 index 0000000..aca0f93 --- /dev/null +++ b/crates/contracts/tests/ownable_script_auth/setup.rs @@ -0,0 +1,52 @@ +use lending_contracts::programs::ownable_script_auth::{ + OwnableScriptAuth, OwnableScriptAuthParameters, +}; +use simplex::{ + transaction::{FinalTransaction, PartialInput, RequiredSignature}, + utils::hash_script, +}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::common::wallet::split_first_signer_utxo; + +pub(super) fn setup_ownable_script_auth( + context: &simplex::TestContext, +) -> anyhow::Result<(OwnableScriptAuth, OwnableScriptAuthParameters)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let txid = split_first_signer_utxo(context, vec![1000, 5000, 10000]); + provider.wait(&txid)?; + + let signer_script_pubkey = signer.get_address().script_pubkey(); + let signer_script_hash = hash_script(&signer_script_pubkey); + + let ownable_script_auth_parameters = OwnableScriptAuthParameters { + script_hash: signer_script_hash, + owner_pubkey: signer.get_schnorr_public_key(), + network: *context.get_network(), + }; + + let signer_utxos = signer.get_utxos_asset(provider.get_network().policy_asset())?; + let utxo_to_lock = signer_utxos.first().unwrap(); + + let ownable_script_auth = OwnableScriptAuth::new(ownable_script_auth_parameters); + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(utxo_to_lock.clone()), + RequiredSignature::NativeEcdsa, + ); + + ownable_script_auth.attach_creation( + &mut ft, + utxo_to_lock.explicit_asset(), + utxo_to_lock.explicit_amount(), + ); + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok((ownable_script_auth, ownable_script_auth_parameters)) +} diff --git a/crates/contracts/tests/ownable_script_auth/support.rs b/crates/contracts/tests/ownable_script_auth/support.rs deleted file mode 100644 index 3b6724f..0000000 --- a/crates/contracts/tests/ownable_script_auth/support.rs +++ /dev/null @@ -1,86 +0,0 @@ -use lending_contracts::programs::program::SimplexProgram; -use lending_contracts::programs::{OwnableScriptAuth, OwnableScriptAuthParameters}; -use lending_contracts::transactions::core::SimplexInput; -use lending_contracts::transactions::ownable_script_auth::{ - create_ownable_script_auth, ownable_script_auth_unlock, ownership_transfer, -}; - -use simplex::simplicityhl::elements::Txid; -use simplex::simplicityhl::elements::schnorr::XOnlyPublicKey; -use simplex::transaction::RequiredSignature; - -use super::common::tx_steps::finalize_and_broadcast; - -pub(super) fn create_ownable_script_auth_tx( - context: &simplex::TestContext, - script_hash: [u8; 32], -) -> anyhow::Result<(Txid, OwnableScriptAuth)> { - let signer = context.get_default_signer(); - - let signer_utxos = signer.get_utxos().unwrap(); - let first_utxo = signer_utxos.first().unwrap(); - - let (ft, script_auth) = create_ownable_script_auth( - &SimplexInput::new(first_utxo, RequiredSignature::NativeEcdsa), - first_utxo.explicit_amount(), - OwnableScriptAuthParameters { - script_hash, - owner_pubkey: signer.get_schnorr_public_key(), - network: *context.get_network(), - }, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok((txid, script_auth)) -} - -pub(super) fn ownership_transfer_tx( - context: &simplex::TestContext, - new_owner_pubkey: XOnlyPublicKey, - ownable_script_auth: OwnableScriptAuth, -) -> anyhow::Result { - let provider = context.get_default_provider(); - - let found_ownable_script_auth_utxos = - provider.fetch_scripthash_utxos(&ownable_script_auth.get_script_pubkey())?; - let ownable_script_auth_utxo = found_ownable_script_auth_utxos.first().unwrap(); - - let mut ownable_script_auth = ownable_script_auth; - - let ft = ownership_transfer( - ownable_script_auth_utxo.clone(), - new_owner_pubkey, - &mut ownable_script_auth, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok(txid) -} - -pub(super) fn script_auth_unlock_tx( - context: &simplex::TestContext, - ownable_script_auth: OwnableScriptAuth, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let found_ownable_script_auth_utxos = - provider.fetch_scripthash_utxos(&ownable_script_auth.get_script_pubkey())?; - let ownable_script_auth_utxo = found_ownable_script_auth_utxos.first().unwrap(); - - let signer_utxos = signer.get_utxos()?; - let auth_utxo = signer_utxos.first().unwrap(); - - let ft = ownable_script_auth_unlock( - ownable_script_auth_utxo, - &SimplexInput::new(auth_utxo, RequiredSignature::NativeEcdsa), - signer.get_address().script_pubkey(), - &ownable_script_auth, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok(txid) -} diff --git a/crates/contracts/tests/ownable_script_auth/unlock_success_flows.rs b/crates/contracts/tests/ownable_script_auth/unlock_success_flows.rs new file mode 100644 index 0000000..042a33e --- /dev/null +++ b/crates/contracts/tests/ownable_script_auth/unlock_success_flows.rs @@ -0,0 +1,33 @@ +use lending_contracts::programs::program::SimplexProgram; +use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::setup::setup_ownable_script_auth; + +#[simplex::test] +fn unlocks_with_one_explicit_output(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + let bob = context + .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); + + let txid = alice.send(bob.get_address().script_pubkey(), 500)?; + provider.wait(&txid)?; + + let (ownable_script_auth, _) = setup_ownable_script_auth(&context)?; + + let ownable_script_auth_utxo = + provider.fetch_scripthash_utxos(&ownable_script_auth.get_script_pubkey())?[0].clone(); + let auth_utxo = alice.get_utxos()?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ft.add_input(PartialInput::new(auth_utxo), RequiredSignature::NativeEcdsa); + + ownable_script_auth.attach_unlocking(&mut ft, ownable_script_auth_utxo, 0); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/pre_lock/cancel.rs b/crates/contracts/tests/pre_lock/cancel.rs deleted file mode 100644 index 3420bd9..0000000 --- a/crates/contracts/tests/pre_lock/cancel.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::support::{cancel_pre_lock_tx, setup_pre_lock}; - -#[simplex::test] -fn cancels_pre_lock(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let (txid, pre_lock) = setup_pre_lock(&context)?; - provider.wait(&txid)?; - - let txid = cancel_pre_lock_tx(&context, pre_lock, txid)?; - provider.wait(&txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/pre_lock/cancellation_success_flow.rs b/crates/contracts/tests/pre_lock/cancellation_success_flow.rs new file mode 100644 index 0000000..f5f1ae4 --- /dev/null +++ b/crates/contracts/tests/pre_lock/cancellation_success_flow.rs @@ -0,0 +1,73 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::OutPoint; +use simplex::transaction::{FinalTransaction, PartialOutput, UTXO}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::setup::setup_pre_lock; + +#[simplex::test] +fn cancels_pre_lock_successfully(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + 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, + }; + + let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = + setup_pre_lock(&context, offer_parameters, principal_asset_amount)?; + + let pre_lock_utxo = provider.fetch_scripthash_utxos(&pre_lock.get_script_pubkey())?[0].clone(); + + let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 1), + txout: pre_lock_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 2), + txout: pre_lock_creation_tx.output[2].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 3), + txout: pre_lock_creation_tx.output[3].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 4), + txout: pre_lock_creation_tx.output[4].clone(), + secrets: None, + }; + + let mut ft = FinalTransaction::new(); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + pre_lock_parameters.offer_parameters.collateral_amount, + pre_lock_parameters.collateral_asset_id, + )); + + pre_lock.attach_cancellation( + &mut ft, + pre_lock_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + borrower_nft_utxo, + lender_nft_utxo, + ); + + let txid = finalize_and_broadcast(&context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/pre_lock/create.rs b/crates/contracts/tests/pre_lock/create.rs deleted file mode 100644 index 9e6408a..0000000 --- a/crates/contracts/tests/pre_lock/create.rs +++ /dev/null @@ -1,11 +0,0 @@ -use super::support::setup_pre_lock; - -#[simplex::test] -fn creates_pre_lock(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let (txid, _) = setup_pre_lock(&context)?; - provider.wait(&txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/pre_lock/create_lending.rs b/crates/contracts/tests/pre_lock/create_lending.rs deleted file mode 100644 index 3cc7cc6..0000000 --- a/crates/contracts/tests/pre_lock/create_lending.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::support::{create_lending_from_pre_lock_tx, setup_pre_lock}; - -#[simplex::test] -fn creates_lending_from_pre_lock(context: simplex::TestContext) -> anyhow::Result<()> { - let provider = context.get_default_provider(); - - let (txid, pre_lock) = setup_pre_lock(&context)?; - provider.wait(&txid)?; - - let (txid, _) = create_lending_from_pre_lock_tx(&context, pre_lock, txid)?; - provider.wait(&txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/pre_lock/lending_creation_success_flow.rs b/crates/contracts/tests/pre_lock/lending_creation_success_flow.rs new file mode 100644 index 0000000..48f98e8 --- /dev/null +++ b/crates/contracts/tests/pre_lock/lending_creation_success_flow.rs @@ -0,0 +1,284 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::{Address, AssetId, OutPoint}; +use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, +}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::common::wallet::get_split_utxo_ft; +use super::setup::setup_pre_lock; + +fn fund_bob_address( + context: &simplex::TestContext, + principal_asset_id: AssetId, + principal_asset_amount: u64, + bob_address: Address, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + + let txid = alice.send(bob_address.script_pubkey(), 500)?; + provider.wait(&txid)?; + + let principal_utxo = alice.get_utxos_asset(principal_asset_id)?[0].clone(); + let utxo_amount = principal_utxo.explicit_amount(); + + assert!( + utxo_amount >= principal_asset_amount, + "Not enough principal tokens" + ); + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + bob_address.script_pubkey(), + principal_asset_amount, + principal_asset_id, + )); + + if utxo_amount > principal_asset_amount { + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + utxo_amount - principal_asset_amount, + principal_asset_id, + )); + } + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn creates_lending_with_single_principal_input( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + let bob = context + .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); + + 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, + }; + + let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = + setup_pre_lock(&context, offer_parameters, principal_asset_amount)?; + + let pre_lock_utxo = provider.fetch_scripthash_utxos(&pre_lock.get_script_pubkey())?[0].clone(); + + let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 1), + txout: pre_lock_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 2), + txout: pre_lock_creation_tx.output[2].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 3), + txout: pre_lock_creation_tx.output[3].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 4), + txout: pre_lock_creation_tx.output[4].clone(), + secrets: None, + }; + + fund_bob_address( + &context, + pre_lock_parameters.principal_asset_id, + principal_asset_amount / 2, + bob.get_address(), + )?; + + let principal_utxo = bob.get_utxos_asset(pre_lock_parameters.principal_asset_id)?[0].clone(); + + let mut ft = FinalTransaction::new(); + + pre_lock.attach_lending_creation( + &mut ft, + pre_lock_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + borrower_nft_utxo, + lender_nft_utxo, + ); + + ft.add_input( + PartialInput::new(principal_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + 1, + pre_lock_parameters.borrower_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + bob.get_address().script_pubkey(), + 1, + pre_lock_parameters.lender_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + + if principal_utxo.explicit_amount() > pre_lock_parameters.offer_parameters.principal_amount { + ft.add_output(PartialOutput::new( + bob.get_address().script_pubkey(), + principal_utxo.explicit_amount() + - pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + } + + let (tx, _) = bob.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + Ok(()) +} + +#[simplex::test] +fn creates_lending_with_multiple_principal_inputs( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let alice = context.get_default_signer(); + let bob = context + .create_signer("sing slogan bar group gauge sphere rescue fossil loyal vital model desert"); + + let principal_asset_amount = 20000; + let current_height = provider.fetch_tip_height()?; + + let offer_parameters = LendingOfferParameters { + collateral_amount: 3000, + principal_amount: 7000, + loan_expiration_time: current_height + 60, + principal_interest_rate: 1000, + }; + + let (pre_lock_creation_txid, pre_lock, pre_lock_parameters) = + setup_pre_lock(&context, offer_parameters, principal_asset_amount)?; + + let pre_lock_utxo = provider.fetch_scripthash_utxos(&pre_lock.get_script_pubkey())?[0].clone(); + + let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_creation_txid)?; + + let first_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 1), + txout: pre_lock_creation_tx.output[1].clone(), + secrets: None, + }; + let second_parameters_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 2), + txout: pre_lock_creation_tx.output[2].clone(), + secrets: None, + }; + let borrower_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 3), + txout: pre_lock_creation_tx.output[3].clone(), + secrets: None, + }; + let lender_nft_utxo = UTXO { + outpoint: OutPoint::new(pre_lock_creation_txid, 4), + txout: pre_lock_creation_tx.output[4].clone(), + secrets: None, + }; + + let bob_principal_amount = principal_asset_amount / 2; + fund_bob_address( + &context, + pre_lock_parameters.principal_asset_id, + bob_principal_amount, + bob.get_address(), + )?; + + let principal_utxo = bob.get_utxos_asset(pre_lock_parameters.principal_asset_id)?[0].clone(); + + let ft = get_split_utxo_ft( + principal_utxo, + vec![5000, 3000, 2000], + &bob, + *provider.get_network(), + ); + + let (tx, _) = bob.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + let mut ft = FinalTransaction::new(); + + pre_lock.attach_lending_creation( + &mut ft, + pre_lock_utxo, + first_parameters_nft_utxo, + second_parameters_nft_utxo, + borrower_nft_utxo, + lender_nft_utxo, + ); + + let principal_utxos = bob.get_utxos_asset(pre_lock_parameters.principal_asset_id)?; + + for principal_utxo in principal_utxos { + ft.add_input( + PartialInput::new(principal_utxo), + RequiredSignature::NativeEcdsa, + ); + } + + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + 1, + pre_lock_parameters.borrower_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + bob.get_address().script_pubkey(), + 1, + pre_lock_parameters.lender_nft_asset_id, + )); + + ft.add_output(PartialOutput::new( + alice.get_address().script_pubkey(), + pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + + if bob_principal_amount > pre_lock_parameters.offer_parameters.principal_amount { + ft.add_output(PartialOutput::new( + bob.get_address().script_pubkey(), + bob_principal_amount - pre_lock_parameters.offer_parameters.principal_amount, + pre_lock_parameters.principal_asset_id, + )); + } + + let (tx, _) = bob.finalize(&ft).unwrap(); + let txid = provider.broadcast_transaction(&tx).unwrap(); + provider.wait(&txid)?; + + Ok(()) +} diff --git a/crates/contracts/tests/pre_lock/mod.rs b/crates/contracts/tests/pre_lock/mod.rs index cac2739..1dc8ef2 100644 --- a/crates/contracts/tests/pre_lock/mod.rs +++ b/crates/contracts/tests/pre_lock/mod.rs @@ -1,7 +1,6 @@ #[path = "../common/mod.rs"] mod common; -mod cancel; -mod create; -mod create_lending; -mod support; +mod cancellation_success_flow; +mod lending_creation_success_flow; +mod setup; diff --git a/crates/contracts/tests/pre_lock/setup.rs b/crates/contracts/tests/pre_lock/setup.rs new file mode 100644 index 0000000..d480531 --- /dev/null +++ b/crates/contracts/tests/pre_lock/setup.rs @@ -0,0 +1,103 @@ +use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; +use lending_contracts::utils::LendingOfferParameters; +use simplex::simplicityhl::elements::Txid; +use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature}; +use simplex::utils::hash_script; + +use super::common::issuance::{issue_asset, issue_preparation_utxos_tx, issue_utility_nfts_tx}; +use super::common::tx_steps::finalize_and_broadcast; +use super::common::wallet::split_first_signer_utxo; + +pub(super) fn setup_pre_lock( + context: &simplex::TestContext, + offer_parameters: LendingOfferParameters, + principal_asset_amount: u64, +) -> anyhow::Result<(Txid, PreLock, PreLockParameters)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let txid = split_first_signer_utxo( + context, + vec![1000, 2000, offer_parameters.collateral_amount], + ); + provider.wait(&txid)?; + + let (txid, principal_asset_id) = issue_asset(context, principal_asset_amount)?; + provider.wait(&txid)?; + + let (txid, preparation_asset_id) = issue_preparation_utxos_tx(context)?; + provider.wait(&txid)?; + + let txid = issue_utility_nfts_tx(context, &offer_parameters, preparation_asset_id)?; + provider.wait(&txid)?; + + let utility_nfts_creation_tx = provider.fetch_transaction(&txid)?; + + let first_parameters_nft_asset_id = + utility_nfts_creation_tx.output[0].asset.explicit().unwrap(); + let second_parameters_nft_asset_id = + utility_nfts_creation_tx.output[1].asset.explicit().unwrap(); + let borrower_nft_asset_id = utility_nfts_creation_tx.output[2].asset.explicit().unwrap(); + let lender_nft_asset_id = utility_nfts_creation_tx.output[3].asset.explicit().unwrap(); + + let borrower_output_script_hash = hash_script(&signer.get_address().script_pubkey()); + + let pre_lock_parameters = PreLockParameters { + collateral_asset_id: provider.get_network().policy_asset(), + first_parameters_nft_asset_id, + second_parameters_nft_asset_id, + borrower_nft_asset_id, + lender_nft_asset_id, + offer_parameters, + principal_asset_id, + borrower_pubkey: signer.get_schnorr_public_key(), + borrower_output_script_hash, + network: *provider.get_network(), + }; + + let pre_lock = PreLock::new(pre_lock_parameters); + + let collateral_utxo = signer.get_utxos_filter( + &|utxo| { + utxo.explicit_asset() == pre_lock_parameters.collateral_asset_id + && utxo.explicit_amount() >= pre_lock_parameters.offer_parameters.collateral_amount + }, + &|_| true, + )?[0] + .clone(); + + let first_parameters_utxo = signer.get_utxos_asset(first_parameters_nft_asset_id)?[0].clone(); + let second_parameters_utxo = signer.get_utxos_asset(second_parameters_nft_asset_id)?[0].clone(); + let borrower_nft_utxo = signer.get_utxos_asset(borrower_nft_asset_id)?[0].clone(); + let lender_nft_utxo = signer.get_utxos_asset(lender_nft_asset_id)?[0].clone(); + + let mut ft = FinalTransaction::new(); + + ft.add_input( + PartialInput::new(collateral_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(first_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(second_parameters_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(borrower_nft_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + ft.add_input( + PartialInput::new(lender_nft_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + pre_lock.attach_creation(&mut ft, 1); + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok((txid, pre_lock, pre_lock_parameters)) +} diff --git a/crates/contracts/tests/pre_lock/support.rs b/crates/contracts/tests/pre_lock/support.rs deleted file mode 100644 index cc14ef5..0000000 --- a/crates/contracts/tests/pre_lock/support.rs +++ /dev/null @@ -1,71 +0,0 @@ -use lending_contracts::{programs::PreLock, transactions::pre_lock::cancel_pre_lock}; -use simplex::simplicityhl::elements::{OutPoint, Txid}; -use simplex::transaction::{PartialInput, PartialOutput, RequiredSignature, UTXO}; - -use super::common::flows::pre_lock_flow; -use super::common::tx_steps::finalize_strict_and_broadcast; -use super::common::wallet::{AmountFilter, filter_signer_utxos_by_asset_and_amount}; - -pub(super) use pre_lock_flow::{create_lending_from_pre_lock_tx, setup_pre_lock}; - -pub(super) fn cancel_pre_lock_tx( - context: &simplex::TestContext, - pre_lock: PreLock, - pre_lock_txid: Txid, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let network = context.get_network(); - let signer = context.get_default_signer(); - - let pre_lock_parameters = pre_lock.get_pre_lock_parameters(); - let pre_lock_creation_tx = provider.fetch_transaction(&pre_lock_txid)?; - - let mut ft = cancel_pre_lock( - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 0), - txout: pre_lock_creation_tx.output[0].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 1), - txout: pre_lock_creation_tx.output[1].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 2), - txout: pre_lock_creation_tx.output[2].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 3), - txout: pre_lock_creation_tx.output[3].clone(), - secrets: None, - }, - UTXO { - outpoint: OutPoint::new(pre_lock_txid, 4), - txout: pre_lock_creation_tx.output[4].clone(), - secrets: None, - }, - PartialOutput::new( - signer.get_address().script_pubkey(), - pre_lock_parameters.offer_parameters.collateral_amount, - network.policy_asset(), - ), - pre_lock, - ); - - let signer_policy_utxos = filter_signer_utxos_by_asset_and_amount( - signer, - context.get_network().policy_asset(), - 100_000, - AmountFilter::LessThan, - ); - let fee_utxo = signer_policy_utxos.first().unwrap(); - - ft.add_input( - PartialInput::new(fee_utxo.clone()), - RequiredSignature::NativeEcdsa, - ); - - finalize_strict_and_broadcast(context, &ft) -} diff --git a/crates/contracts/tests/script_auth/happy_path.rs b/crates/contracts/tests/script_auth/happy_path.rs deleted file mode 100644 index f41d427..0000000 --- a/crates/contracts/tests/script_auth/happy_path.rs +++ /dev/null @@ -1,85 +0,0 @@ -use lending_contracts::programs::{ScriptAuth, ScriptAuthParameters, program::SimplexProgram}; -use lending_contracts::transactions::core::SimplexInput; -use lending_contracts::transactions::script_auth::{create_script_auth, unlock_script_auth}; - -use simplex::simplicityhl::elements::Txid; -use simplex::transaction::{PartialOutput, RequiredSignature}; -use simplex::utils::hash_script; - -use super::common::tx_steps::{finalize_and_broadcast, wait_for_tx}; -use super::common::wallet::split_first_signer_utxo; - -pub(super) fn create_script_auth_tx( - context: &simplex::TestContext, - script_hash: [u8; 32], -) -> anyhow::Result<(Txid, ScriptAuth)> { - let signer = context.get_default_signer(); - - let signer_utxos = signer.get_utxos().unwrap(); - let first_utxo = signer_utxos.first().unwrap(); - - let (ft, script_auth) = create_script_auth( - &SimplexInput::new(first_utxo, RequiredSignature::NativeEcdsa), - ScriptAuthParameters { - script_hash, - network: *context.get_network(), - }, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok((txid, script_auth)) -} - -pub(super) fn unlock_script_auth_tx( - context: &simplex::TestContext, - script_auth: ScriptAuth, -) -> anyhow::Result { - let provider = context.get_default_provider(); - let signer = context.get_default_signer(); - - let signer_utxos = signer.get_utxos().unwrap(); - let first_utxo = signer_utxos.first().unwrap(); - let auth_input = SimplexInput::new(first_utxo, RequiredSignature::NativeEcdsa); - - let found_script_auth_utxos = - provider.fetch_scripthash_utxos(&script_auth.get_script_pubkey())?; - let script_auth_utxo = found_script_auth_utxos.first().unwrap(); - - let signer_script = signer.get_address().script_pubkey(); - let unlocked_output = PartialOutput::new( - signer_script, - script_auth_utxo.explicit_amount(), - script_auth_utxo.explicit_asset(), - ); - - let ft = unlock_script_auth( - script_auth_utxo.clone(), - &auth_input, - unlocked_output, - script_auth, - ); - - let txid = finalize_and_broadcast(context, &ft)?; - - Ok(txid) -} - -#[simplex::test] -fn creates_and_unlocks_script_auth(context: simplex::TestContext) -> anyhow::Result<()> { - let signer = context.get_default_signer(); - - let txid = split_first_signer_utxo(&context, vec![1000, 5000, 10000]); - wait_for_tx(&context, &txid)?; - - let signer_script_pubkey = signer.get_address().script_pubkey(); - let signer_script_hash = hash_script(&signer_script_pubkey); - - let (txid, script_auth) = create_script_auth_tx(&context, signer_script_hash)?; - wait_for_tx(&context, &txid)?; - - let txid = unlock_script_auth_tx(&context, script_auth)?; - wait_for_tx(&context, &txid)?; - - Ok(()) -} diff --git a/crates/contracts/tests/script_auth/mod.rs b/crates/contracts/tests/script_auth/mod.rs index 52ad1f3..06edba8 100644 --- a/crates/contracts/tests/script_auth/mod.rs +++ b/crates/contracts/tests/script_auth/mod.rs @@ -1,4 +1,4 @@ #[path = "../common/mod.rs"] mod common; -mod happy_path; +mod unlock_success_flows; diff --git a/crates/contracts/tests/script_auth/unlock_success_flows.rs b/crates/contracts/tests/script_auth/unlock_success_flows.rs new file mode 100644 index 0000000..6928cee --- /dev/null +++ b/crates/contracts/tests/script_auth/unlock_success_flows.rs @@ -0,0 +1,176 @@ +use lending_contracts::programs::program::SimplexProgram; +use lending_contracts::programs::script_auth::{ + ScriptAuth, ScriptAuthParameters, ScriptAuthWitnessParams, +}; +use simplex::transaction::PartialOutput; +use simplex::{ + transaction::{FinalTransaction, PartialInput, RequiredSignature}, + utils::hash_script, +}; + +use super::common::tx_steps::finalize_and_broadcast; +use super::common::wallet::split_first_signer_utxo; + +fn setup_script_auth( + context: &simplex::TestContext, +) -> anyhow::Result<(ScriptAuth, ScriptAuthParameters)> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let txid = split_first_signer_utxo(context, vec![1000, 5000, 10000]); + provider.wait(&txid)?; + + let signer_script_pubkey = signer.get_address().script_pubkey(); + let signer_script_hash = hash_script(&signer_script_pubkey); + + let script_auth_parameters = ScriptAuthParameters { + script_hash: signer_script_hash, + network: *context.get_network(), + }; + + let signer_utxos = signer.get_utxos_asset(provider.get_network().policy_asset())?; + let utxo_to_lock = signer_utxos.first().unwrap(); + + let mut ft = FinalTransaction::new(); + let script_auth = ScriptAuth::new(script_auth_parameters); + + script_auth.attach_creation( + &mut ft, + utxo_to_lock.explicit_asset(), + utxo_to_lock.explicit_amount(), + ); + + let txid = finalize_and_broadcast(context, &ft)?; + provider.wait(&txid)?; + + Ok((script_auth, script_auth_parameters)) +} + +#[simplex::test] +fn unlocks_with_one_explicit_output(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (script_auth, _) = setup_script_auth(&context)?; + + let script_auth_utxo = + provider.fetch_scripthash_utxos(&script_auth.get_script_pubkey())?[0].clone(); + let script_auth_witness_params = ScriptAuthWitnessParams::new(1); + + let auth_utxo = signer.get_utxos()?[0].clone(); + + let mut ft = FinalTransaction::new(); + + script_auth.attach_unlocking( + &mut ft, + script_auth_utxo.clone(), + script_auth_witness_params, + ); + ft.add_input( + PartialInput::new(auth_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + script_auth_utxo.explicit_amount(), + script_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + auth_utxo.explicit_amount(), + auth_utxo.explicit_asset(), + )); + + Ok(()) +} + +#[simplex::test] +fn unlocks_with_multiple_explicit_outputs(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (script_auth, _) = setup_script_auth(&context)?; + + let script_auth_utxo = + provider.fetch_scripthash_utxos(&script_auth.get_script_pubkey())?[0].clone(); + let script_auth_witness_params = ScriptAuthWitnessParams::new(1); + + let auth_utxo = signer.get_utxos()?[0].clone(); + + let mut ft = FinalTransaction::new(); + + script_auth.attach_unlocking( + &mut ft, + script_auth_utxo.clone(), + script_auth_witness_params, + ); + ft.add_input( + PartialInput::new(auth_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + let first_locked_output_amount = script_auth_utxo.explicit_amount() / 2; + let second_locked_output_amount = + script_auth_utxo.explicit_amount() - first_locked_output_amount; + + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + first_locked_output_amount, + script_auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + auth_utxo.explicit_amount(), + auth_utxo.explicit_asset(), + )); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + second_locked_output_amount, + script_auth_utxo.explicit_asset(), + )); + + Ok(()) +} + +#[simplex::test] +fn unlocks_with_confidential_output(context: simplex::TestContext) -> anyhow::Result<()> { + let provider = context.get_default_provider(); + let signer = context.get_default_signer(); + + let (script_auth, _) = setup_script_auth(&context)?; + + let script_auth_utxo = + provider.fetch_scripthash_utxos(&script_auth.get_script_pubkey())?[0].clone(); + let script_auth_witness_params = ScriptAuthWitnessParams::new(1); + + let auth_utxo = signer.get_utxos()?[0].clone(); + + let mut ft = FinalTransaction::new(); + + script_auth.attach_unlocking( + &mut ft, + script_auth_utxo.clone(), + script_auth_witness_params, + ); + ft.add_input( + PartialInput::new(auth_utxo.clone()), + RequiredSignature::NativeEcdsa, + ); + + ft.add_output( + PartialOutput::new( + signer.get_address().script_pubkey(), + script_auth_utxo.explicit_amount(), + script_auth_utxo.explicit_asset(), + ) + .with_blinding_key(signer.get_blinding_public_key()), + ); + ft.add_output(PartialOutput::new( + signer.get_address().script_pubkey(), + auth_utxo.explicit_amount(), + auth_utxo.explicit_asset(), + )); + + Ok(()) +} diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml index a2c6575..7299a56 100644 --- a/crates/indexer/Cargo.toml +++ b/crates/indexer/Cargo.toml @@ -32,7 +32,7 @@ tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } serde = { version= "1", features = ["derive"]} hex = { workspace = true } -simplex = { workspace = true } +smplx-std = { workspace = true } lending-contracts = { path = "../contracts" } diff --git a/crates/indexer/Dockerfile b/crates/indexer/Dockerfile index 6348e2a..3a2ebf3 100644 --- a/crates/indexer/Dockerfile +++ b/crates/indexer/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.90-bookworm AS builder +FROM rust:1.93-bookworm AS builder RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* diff --git a/crates/indexer/configuration/base.yaml b/crates/indexer/configuration/base.yaml index e9dcd12..1789744 100644 --- a/crates/indexer/configuration/base.yaml +++ b/crates/indexer/configuration/base.yaml @@ -11,4 +11,4 @@ esplora: timeout: 10 indexer: interval: 10000 - last_indexed_height: 2375530 + last_indexed_height: 2408530 diff --git a/crates/indexer/src/indexer/handlers/lending_creation.rs b/crates/indexer/src/indexer/handlers/lending_creation.rs index ffcef68..3c4be3f 100644 --- a/crates/indexer/src/indexer/handlers/lending_creation.rs +++ b/crates/indexer/src/indexer/handlers/lending_creation.rs @@ -52,11 +52,11 @@ pub async fn handle_lending_creation( } pub fn is_lending_creation_tx(tx: &Transaction, expected_principal_asset: &[u8]) -> bool { - if tx.output.len() < 7 || tx.input.len() < 7 { + if tx.output.len() < 7 || tx.input.len() < 6 { return false; } - if let Some(asset_id) = tx.output[1].asset.explicit() { + if let Some(asset_id) = tx.output[5].asset.explicit() { return asset_id.into_inner().0.to_vec() == expected_principal_asset; } diff --git a/crates/indexer/src/indexer/handlers/loan_liquidation.rs b/crates/indexer/src/indexer/handlers/loan_liquidation.rs index 1fda15b..a7a1c4a 100644 --- a/crates/indexer/src/indexer/handlers/loan_liquidation.rs +++ b/crates/indexer/src/indexer/handlers/loan_liquidation.rs @@ -46,8 +46,5 @@ pub async fn handle_loan_liquidation( } pub fn is_loan_liquidation_tx(tx: &Transaction) -> bool { - tx.output[1].is_null_data() - && tx.output[2].is_null_data() - && tx.output[3].is_null_data() - && !tx.output[4].is_null_data() + tx.output[1].is_null_data() && tx.output[2].is_null_data() && tx.output[3].is_null_data() } diff --git a/crates/indexer/src/indexer/handlers/pre_lock.rs b/crates/indexer/src/indexer/handlers/pre_lock.rs index 569b359..05a3d84 100644 --- a/crates/indexer/src/indexer/handlers/pre_lock.rs +++ b/crates/indexer/src/indexer/handlers/pre_lock.rs @@ -1,7 +1,7 @@ +use lending_contracts::programs::program::SimplexProgram; use simplex::simplicityhl::elements::{OutPoint, Transaction, hashes::Hash}; -use lending_contracts::programs::{PreLock, PreLockParameters, program::SimplexProgram}; -use lending_contracts::transactions::pre_lock::extract_pre_lock_parameters_from_tx; +use lending_contracts::programs::pre_lock::{PreLock, PreLockParameters}; use crate::esplora_client::EsploraClient; use crate::indexer::{cache::UtxoCache, db}; @@ -104,15 +104,13 @@ pub fn is_pre_lock_creation_tx( tx: &Transaction, client: &EsploraClient, ) -> Option { - let pre_lock_parameters = - extract_pre_lock_parameters_from_tx(tx, &client.to_simplex_provider()).ok()?; + let pre_lock = PreLock::try_from_tx(tx, &client.to_simplex_provider()).ok()?; - let pre_lock = PreLock::new(pre_lock_parameters); let pre_lock_script_pubkey = pre_lock.get_script_pubkey(); if tx.output.first().unwrap().script_pubkey != pre_lock_script_pubkey { return None; } - Some(pre_lock_parameters) + Some(*pre_lock.get_parameters()) } diff --git a/crates/indexer/src/models/offer.rs b/crates/indexer/src/models/offer.rs index 4acd028..55acb25 100644 --- a/crates/indexer/src/models/offer.rs +++ b/crates/indexer/src/models/offer.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use simplex::simplicityhl::elements::{Txid, hashes::Hash}; -use lending_contracts::programs::PreLockParameters; +use lending_contracts::programs::pre_lock::PreLockParameters; use crate::models::{ParticipantType, UtxoType}; diff --git a/web/Dockerfile b/web/Dockerfile index f4e5b46..8ac430e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -24,7 +24,7 @@ RUN npm install --no-audit --no-fund # Copy application sources WORKDIR /app COPY web ./web -COPY crates/contracts/src ./crates/contracts/src +COPY crates/contracts/simf ./crates/contracts/simf WORKDIR /app/web ARG VITE_API_URL diff --git a/web/simplicity-covenants.config.json b/web/simplicity-covenants.config.json index 4775698..85f016c 100644 --- a/web/simplicity-covenants.config.json +++ b/web/simplicity-covenants.config.json @@ -1,9 +1,9 @@ { "covenants": [ { "id": "p2pk", "path": "web/src/simplicity/sources/p2pk.simf" }, - { "id": "pre_lock", "path": "crates/contracts/src/pre_lock/source_simf/pre_lock.simf" }, - { "id": "lending", "path": "crates/contracts/src/lending/source_simf/lending.simf" }, - { "id": "asset_auth", "path": "crates/contracts/src/asset_auth/source_simf/asset_auth.simf" }, - { "id": "script_auth", "path": "crates/contracts/src/script_auth/source_simf/script_auth.simf" } + { "id": "pre_lock", "path": "crates/contracts/simf/pre_lock.simf" }, + { "id": "lending", "path": "crates/contracts/simf/lending.simf" }, + { "id": "asset_auth", "path": "crates/contracts/simf/asset_auth.simf" }, + { "id": "script_auth", "path": "crates/contracts/simf/script_auth.simf" } ] } diff --git a/web/src/simplicity/covenants/preLock.ts b/web/src/simplicity/covenants/preLock.ts index 50fbb86..fcaa75f 100644 --- a/web/src/simplicity/covenants/preLock.ts +++ b/web/src/simplicity/covenants/preLock.ts @@ -68,7 +68,7 @@ export interface BuildPreLockWitnessParams { } /** - * Build PreLock witness. PATH = Left(()) for LendingCreation, Right(signature) for PreLockCancellation. + * Build PreLock witness. PATH = Left(()) for LendingCreation, Right(Signature) for PreLockCancellation. * Type PATH is Either<(), Signature>. */ export function buildPreLockWitness( @@ -79,14 +79,20 @@ export function buildPreLockWitness( const pathType = SimplicityType.fromString('Either<(), Signature>') - const pathValue = - params.branch === 'LendingCreation' - ? SimplicityTypedValue.parse('Left(())', pathType) - : SimplicityTypedValue.parse(`Right(0x${params.cancellationSignatureHex ?? ''})`, pathType) + let pathExpr = 'Left(())' + if (params.branch === 'PreLockCancellation') { + if (!params.cancellationSignatureHex) { + throw new Error('cancellationSignatureHex is required for PreLockCancellation branch') + } + const sigHex = params.cancellationSignatureHex.startsWith('0x') + ? params.cancellationSignatureHex + : `0x${params.cancellationSignatureHex}` + pathExpr = `Right(${sigHex})` + } + const pathValue = SimplicityTypedValue.parse(pathExpr, pathType) let witness = new SimplicityWitnessValues() - const next = witness.addValue('PATH', pathValue) - witness = next + witness = witness.addValue('PATH', pathValue) return witness } diff --git a/web/src/tx/acceptOffer/buildAcceptOfferTx.ts b/web/src/tx/acceptOffer/buildAcceptOfferTx.ts index dce0de3..e985386 100644 --- a/web/src/tx/acceptOffer/buildAcceptOfferTx.ts +++ b/web/src/tx/acceptOffer/buildAcceptOfferTx.ts @@ -159,11 +159,11 @@ export async function buildAcceptOfferTx( // Outputs (same order as Rust builder). api.addOutputWithScript(lendingScriptPubkeyHex, preLockValue, preLockAssetHex) - api.addOutputWithScript(borrowerScriptPubkeyHex, principalValue, principalAssetHex) api.addOutputWithScript(parametersNftScriptPubkeyHex, firstParamsValue, firstParamsAssetHex) api.addOutputWithScript(parametersNftScriptPubkeyHex, secondParamsValue, secondParamsAssetHex) api.addOutputWithScript(borrowerScriptPubkeyHex, borrowerNftValue, borrowerNftAssetHex) api.addOutputWithScript(lenderNftOutputScriptHex, lenderNftValue, lenderNftAssetHex) + api.addOutputWithScript(borrowerScriptPubkeyHex, principalValue, principalAssetHex) const feeChange = feeValue - feeAmount if (feeChange > 0n) { diff --git a/web/src/tx/loanLiquidation/buildLoanLiquidationTx.ts b/web/src/tx/loanLiquidation/buildLoanLiquidationTx.ts index ef5eff7..d8074be 100644 --- a/web/src/tx/loanLiquidation/buildLoanLiquidationTx.ts +++ b/web/src/tx/loanLiquidation/buildLoanLiquidationTx.ts @@ -29,7 +29,7 @@ export interface LoanLiquidationUtxo { } export interface BuildLoanLiquidationTxParams { - /** Lending creation tx (accept-offer tx). Vouts: 0=Lending, 2=FirstParams, 3=SecondParams; vout 4=Borrower NFT (asset only). */ + /** Lending creation tx (accept-offer tx). Vouts: 0=Lending, 1=FirstParams, 2=SecondParams; vout 3=Borrower NFT (asset only). */ lendingTx: EsploraTx /** Current Lender NFT UTXO (from indexer participants — may have moved since accept-offer). */ lenderNftUtxo: LoanLiquidationUtxo @@ -82,10 +82,10 @@ export async function buildLoanLiquidationTx( if (feeAmount <= 0n) throw new Error('Fee amount must be at least 1') - // Lending creation tx vouts: 0=Lending, 2=First params, 3=Second params, 4=Borrower NFT (asset id only). Lender NFT from lenderNftUtxo (indexer). + // Lending creation tx vouts: 0=Lending, 1=First params, 2=Second params, 3=Borrower NFT (asset id only). Lender NFT from lenderNftUtxo (indexer). const lendingPrevout = requireVout(lendingTx, 0, 'Lending', 'lending tx') - const firstParamsPrevout = requireVout(lendingTx, 2, 'First parameters NFT', 'lending tx') - const secondParamsPrevout = requireVout(lendingTx, 3, 'Second parameters NFT', 'lending tx') + const firstParamsPrevout = requireVout(lendingTx, 1, 'First parameters NFT', 'lending tx') + const secondParamsPrevout = requireVout(lendingTx, 2, 'Second parameters NFT', 'lending tx') const lenderNftPrevout = lenderNftUtxo.prevout const lendingAssetHex = requireAssetHex(lendingPrevout, 'Lending') @@ -112,8 +112,8 @@ export async function buildLoanLiquidationTx( throw new Error(`Fee UTXO value ${feeValue} is less than fee ${feeAmount}`) } - // Borrower NFT asset from same tx (vout 4) for Lending args - const borrowerNftPrevout = requireVout(lendingTx, 4, 'Borrower NFT', 'lending tx') + // Borrower NFT asset from same tx (vout 3) for Lending args + const borrowerNftPrevout = requireVout(lendingTx, 3, 'Borrower NFT', 'lending tx') const borrowerNftAssetHex = requireAssetHex(borrowerNftPrevout, 'Borrower NFT') const lendingParams = { @@ -177,8 +177,8 @@ export async function buildLoanLiquidationTx( const txid = lendingTx.txid.trim() // All inputs need sequence enabling locktime so nLockTime is enforced (mirrors pst.rs ENABLE_LOCKTIME_NO_RBF for every input). api.addInputWithLocktimeSequence({ txid, vout: 0 }, lendingPrevout) - api.addInputWithLocktimeSequence({ txid, vout: 2 }, firstParamsPrevout) - api.addInputWithLocktimeSequence({ txid, vout: 3 }, secondParamsPrevout) + api.addInputWithLocktimeSequence({ txid, vout: 1 }, firstParamsPrevout) + api.addInputWithLocktimeSequence({ txid, vout: 2 }, secondParamsPrevout) api.addInputWithLocktimeSequence(lenderNftUtxo.outpoint, lenderNftPrevout) api.addInputWithLocktimeSequence(feeUtxo.outpoint, feeUtxo.prevout) diff --git a/web/src/tx/loanRepayment/buildLoanRepaymentTx.ts b/web/src/tx/loanRepayment/buildLoanRepaymentTx.ts index a1239b0..cb542ae 100644 --- a/web/src/tx/loanRepayment/buildLoanRepaymentTx.ts +++ b/web/src/tx/loanRepayment/buildLoanRepaymentTx.ts @@ -95,10 +95,10 @@ export async function buildLoanRepaymentTx( ) const lendingPrevout = requireVout(lendingTx, 0, 'Lending', 'lending tx') - const firstParamsPrevout = requireVout(lendingTx, 2, 'First parameters NFT', 'lending tx') - const secondParamsPrevout = requireVout(lendingTx, 3, 'Second parameters NFT', 'lending tx') + const firstParamsPrevout = requireVout(lendingTx, 1, 'First parameters NFT', 'lending tx') + const secondParamsPrevout = requireVout(lendingTx, 2, 'Second parameters NFT', 'lending tx') const borrowerNftPrevout = borrowerNftUtxo.prevout - const lenderNftPrevout = requireVout(lendingTx, 5, 'Lender NFT', 'lending tx') + const lenderNftPrevout = requireVout(lendingTx, 4, 'Lender NFT', 'lending tx') const lendingAssetHex = requireAssetHex(lendingPrevout, 'Lending') const lendingValue = requireValue(lendingPrevout, 'Lending') @@ -197,8 +197,8 @@ export async function buildLoanRepaymentTx( const txid = lendingTx.txid.trim() api.addInput({ txid, vout: 0 }, lendingPrevout) - api.addInput({ txid, vout: 2 }, firstParamsPrevout) - api.addInput({ txid, vout: 3 }, secondParamsPrevout) + api.addInput({ txid, vout: 1 }, firstParamsPrevout) + api.addInput({ txid, vout: 2 }, secondParamsPrevout) api.addInput(borrowerNftUtxo.outpoint, borrowerNftPrevout) for (const u of principalUtxos) { api.addInput(u.outpoint, u.prevout) diff --git a/web/src/tx/psetBuilder.ts b/web/src/tx/psetBuilder.ts index 94ee56c..3eead27 100644 --- a/web/src/tx/psetBuilder.ts +++ b/web/src/tx/psetBuilder.ts @@ -118,16 +118,16 @@ export async function createPsetBuilder(network: PsetNetwork): Promise unknown }).enableLocktimeNoRbf?.() ?? (TxSequence as { enable_locktime_no_rbf?: () => unknown }).enable_locktime_no_rbf?.() - const psetInput = - seq != null && - typeof (seq as { to_consensus_u32?: unknown }).to_consensus_u32 === 'function' - ? PsetInputBuilder.fromPrevout(op) - .witnessUtxo(txOut) - .sequence( - seq as Parameters['sequence']>[0] - ) - .build() - : PsetInputBuilder.fromPrevout(op).witnessUtxo(txOut).build() + if (seq == null) { + throw new Error( + 'LWK TxSequence locktime helper is unavailable (expected enableLocktimeNoRbf/enable_locktime_no_rbf). Refusing to add input without locktime-enabled sequence.' + ) + } + + const psetInput = PsetInputBuilder.fromPrevout(op) + .witnessUtxo(txOut) + .sequence(seq as Parameters['sequence']>[0]) + .build() builder = builder.addInput(psetInput) },