From 0290cbb141fc6afb34a885c11395cc1bb631a4ee Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Tue, 19 May 2026 22:27:36 +0200 Subject: [PATCH] feat(descro): add create_escrow_prefunded instruction --- programs/descro/src/instructions.rs | 2 + .../instructions/create_escrow_prefunded.rs | 54 +++++++++ programs/descro/src/lib.rs | 9 ++ programs/descro/tests/common/mod.rs | 28 +++++ .../tests/test_create_escrow_prefunded.rs | 107 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 programs/descro/src/instructions/create_escrow_prefunded.rs create mode 100644 programs/descro/tests/test_create_escrow_prefunded.rs diff --git a/programs/descro/src/instructions.rs b/programs/descro/src/instructions.rs index d4b2e07..23ec34f 100644 --- a/programs/descro/src/instructions.rs +++ b/programs/descro/src/instructions.rs @@ -3,6 +3,7 @@ pub mod cancel; pub mod complete; pub mod create_escrow; +pub mod create_escrow_prefunded; pub mod deposit; pub mod dispute; pub mod resolve; @@ -10,6 +11,7 @@ pub mod resolve; pub use cancel::*; pub use complete::*; pub use create_escrow::*; +pub use create_escrow_prefunded::*; pub use deposit::*; pub use dispute::*; pub use resolve::*; diff --git a/programs/descro/src/instructions/create_escrow_prefunded.rs b/programs/descro/src/instructions/create_escrow_prefunded.rs new file mode 100644 index 0000000..622765f --- /dev/null +++ b/programs/descro/src/instructions/create_escrow_prefunded.rs @@ -0,0 +1,54 @@ +use anchor_lang::prelude::*; +use crate::state::{EscrowAccount, EscrowState}; +use crate::error::EscrowError; + +#[derive(Accounts)] +#[instruction(amount: u64, dispute_resolver: Option, escrow_id: u64)] +pub struct CreateEscrowPrefunded<'info> { + #[account(mut)] + pub seller: Signer<'info>, + + /// CHECK: Only stored as pubkey, no ownership check needed + pub buyer: UncheckedAccount<'info>, + + #[account( + init, + payer = seller, + space = 8 + EscrowAccount::INIT_SPACE, + seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()], + bump + )] + pub escrow_account: Account<'info, EscrowAccount>, + + /// CHECK: Pre-funded vault PDA — balance verified in handler + #[account( + mut, + seeds = [b"vault", escrow_account.key().as_ref()], + bump + )] + pub vault: UncheckedAccount<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, + amount: u64, + dispute_resolver: Option, + escrow_id: u64, +) -> Result<()> { + let vault_lamports = ctx.accounts.vault.to_account_info().lamports(); + require!(vault_lamports >= amount, EscrowError::InsufficientVaultBalance); + + let escrow = &mut ctx.accounts.escrow_account; + escrow.seller = ctx.accounts.seller.key(); + escrow.buyer = ctx.accounts.buyer.key(); + escrow.amount = amount; + escrow.dispute_resolver = dispute_resolver; + escrow.state = EscrowState::Active; + escrow.bump = ctx.bumps.escrow_account; + escrow.vault_bump = ctx.bumps.vault; + escrow.escrow_id = escrow_id; + escrow.dispute_raised_at = None; + Ok(()) +} diff --git a/programs/descro/src/lib.rs b/programs/descro/src/lib.rs index 55b6a84..0fbd11e 100644 --- a/programs/descro/src/lib.rs +++ b/programs/descro/src/lib.rs @@ -26,6 +26,15 @@ pub mod descro { create_escrow::handler(ctx, amount, dispute_resolver, escrow_id) } + pub fn create_escrow_prefunded( + ctx: Context, + amount: u64, + dispute_resolver: Option, + escrow_id: u64, + ) -> Result<()> { + create_escrow_prefunded::handler(ctx, amount, dispute_resolver, escrow_id) + } + pub fn deposit(ctx: Context) -> Result<()> { deposit::handler(ctx) } diff --git a/programs/descro/tests/common/mod.rs b/programs/descro/tests/common/mod.rs index 9b4462a..db0c1ab 100644 --- a/programs/descro/tests/common/mod.rs +++ b/programs/descro/tests/common/mod.rs @@ -199,3 +199,31 @@ pub fn read_escrow(svm: &LiteSVM, seller: &Pubkey, escrow_id: u64) -> EscrowAcco let account = svm.get_account(&escrow).expect("escrow account not found"); EscrowAccount::try_deserialize(&mut account.data.as_slice()).unwrap() } + +pub fn ix_create_escrow_prefunded( + seller: &Pubkey, + buyer: &Pubkey, + amount: u64, + dispute_resolver: Option, + escrow_id: u64, +) -> Instruction { + let escrow = escrow_pda(seller, escrow_id); + let vault = vault_pda(&escrow); + Instruction::new_with_bytes( + descro::id(), + &descro::instruction::CreateEscrowPrefunded { + amount, + dispute_resolver, + escrow_id, + } + .data(), + descro::accounts::CreateEscrowPrefunded { + seller: *seller, + buyer: *buyer, + escrow_account: escrow, + vault, + system_program: system_program::ID, + } + .to_account_metas(None), + ) +} diff --git a/programs/descro/tests/test_create_escrow_prefunded.rs b/programs/descro/tests/test_create_escrow_prefunded.rs new file mode 100644 index 0000000..4e51381 --- /dev/null +++ b/programs/descro/tests/test_create_escrow_prefunded.rs @@ -0,0 +1,107 @@ +mod common; +use common::*; +use solana_signer::Signer; + +#[test] +fn prefunded_creates_active_escrow() { + let (mut svm, seller, buyer, resolver) = setup(); + + // Pre-fund the vault PDA before the instruction + let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID); + let vault = vault_pda(&escrow); + svm.airdrop(&vault, AMOUNT).unwrap(); + + send( + &mut svm, + ix_create_escrow_prefunded( + &seller.pubkey(), + &buyer.pubkey(), + AMOUNT, + Some(resolver.pubkey()), + ESCROW_ID, + ), + &seller, + ); + + let escrow_account = read_escrow(&svm, &seller.pubkey(), ESCROW_ID); + assert_eq!(escrow_account.state, EscrowState::Active); + assert_eq!(escrow_account.seller, seller.pubkey()); + assert_eq!(escrow_account.buyer, buyer.pubkey()); + assert_eq!(escrow_account.amount, AMOUNT); + assert_eq!(escrow_account.dispute_raised_at, None); +} + +#[test] +fn prefunded_fails_if_vault_underfunded() { + let (mut svm, seller, buyer, resolver) = setup(); + + // Fund vault with less than AMOUNT + let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID); + let vault = vault_pda(&escrow); + svm.airdrop(&vault, AMOUNT - 1).unwrap(); + + assert!(!try_send( + &mut svm, + ix_create_escrow_prefunded( + &seller.pubkey(), + &buyer.pubkey(), + AMOUNT, + Some(resolver.pubkey()), + ESCROW_ID, + ), + &seller, + )); +} + +#[test] +fn prefunded_without_resolver_still_works() { + let (mut svm, seller, buyer, _resolver) = setup(); + + let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID); + let vault = vault_pda(&escrow); + svm.airdrop(&vault, AMOUNT).unwrap(); + + send( + &mut svm, + ix_create_escrow_prefunded( + &seller.pubkey(), + &buyer.pubkey(), + AMOUNT, + None, + ESCROW_ID, + ), + &seller, + ); + + let escrow_account = read_escrow(&svm, &seller.pubkey(), ESCROW_ID); + assert_eq!(escrow_account.state, EscrowState::Active); + assert_eq!(escrow_account.dispute_resolver, None); +} + +#[test] +fn prefunded_escrow_can_be_completed() { + let (mut svm, seller, buyer, _resolver) = setup(); + + let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID); + let vault = vault_pda(&escrow); + svm.airdrop(&vault, AMOUNT).unwrap(); + + send( + &mut svm, + ix_create_escrow_prefunded( + &seller.pubkey(), + &buyer.pubkey(), + AMOUNT, + None, + ESCROW_ID, + ), + &seller, + ); + + let seller_before = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); + send(&mut svm, ix_complete(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer); + + let seller_after = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); + assert!(seller_after > seller_before); + assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); +}