diff --git a/programs/descro/src/error.rs b/programs/descro/src/error.rs index 54a46bd..0851561 100644 --- a/programs/descro/src/error.rs +++ b/programs/descro/src/error.rs @@ -18,4 +18,6 @@ pub enum EscrowError { InsufficientVaultBalance, #[msg("Amount must be at least rent-exempt minimum for the vault (~890880 lamports)")] AmountBelowRentMinimum, + #[msg("Resolver must co-sign this transaction (SignatureGated acceptance policy)")] + ResolverSignatureRequired, } diff --git a/programs/descro/src/instructions.rs b/programs/descro/src/instructions.rs index ac0dd20..529e80c 100644 --- a/programs/descro/src/instructions.rs +++ b/programs/descro/src/instructions.rs @@ -1,19 +1,21 @@ #![allow(ambiguous_glob_reexports)] +pub mod buyer_create_escrow; pub mod cancel; pub mod complete; pub mod create_escrow; -pub mod create_escrow_prefunded; pub mod deposit; pub mod dispute; pub mod emergency_resolve; pub mod resolve; +pub mod seller_confirm; +pub use buyer_create_escrow::*; pub use cancel::*; pub use complete::*; pub use create_escrow::*; -pub use create_escrow_prefunded::*; pub use deposit::*; pub use dispute::*; pub use emergency_resolve::*; pub use resolve::*; +pub use seller_confirm::*; diff --git a/programs/descro/src/instructions/create_escrow_prefunded.rs b/programs/descro/src/instructions/buyer_create_escrow.rs similarity index 60% rename from programs/descro/src/instructions/create_escrow_prefunded.rs rename to programs/descro/src/instructions/buyer_create_escrow.rs index 622765f..9d75f57 100644 --- a/programs/descro/src/instructions/create_escrow_prefunded.rs +++ b/programs/descro/src/instructions/buyer_create_escrow.rs @@ -1,26 +1,27 @@ -use anchor_lang::prelude::*; use crate::state::{EscrowAccount, EscrowState}; -use crate::error::EscrowError; +use crate::EscrowError; +use anchor_lang::prelude::*; +use anchor_lang::system_program::{self, Transfer}; #[derive(Accounts)] #[instruction(amount: u64, dispute_resolver: Option, escrow_id: u64)] -pub struct CreateEscrowPrefunded<'info> { +pub struct BuyerCreateEscrow<'info> { #[account(mut)] - pub seller: Signer<'info>, + pub buyer: Signer<'info>, /// CHECK: Only stored as pubkey, no ownership check needed - pub buyer: UncheckedAccount<'info>, + pub seller: UncheckedAccount<'info>, #[account( init, - payer = seller, + payer = buyer, 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 + /// CHECK: PDA vault for holding escrow SOL; created implicitly on deposit #[account( mut, seeds = [b"vault", escrow_account.key().as_ref()], @@ -32,20 +33,31 @@ pub struct CreateEscrowPrefunded<'info> { } pub fn handler( - ctx: Context, + 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 rent_min = Rent::get()?.minimum_balance(0); + require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum); + + system_program::transfer( + CpiContext::new( + system_program::ID, + Transfer { + from: ctx.accounts.buyer.to_account_info(), + to: ctx.accounts.vault.to_account_info(), + }, + ), + amount, + )?; 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.state = EscrowState::AwaitingSellerConfirm; escrow.bump = ctx.bumps.escrow_account; escrow.vault_bump = ctx.bumps.vault; escrow.escrow_id = escrow_id; diff --git a/programs/descro/src/instructions/cancel.rs b/programs/descro/src/instructions/cancel.rs index 540d6f3..8a49c00 100644 --- a/programs/descro/src/instructions/cancel.rs +++ b/programs/descro/src/instructions/cancel.rs @@ -1,25 +1,66 @@ use anchor_lang::prelude::*; +use anchor_lang::system_program::{self, Transfer}; use crate::state::{EscrowAccount, EscrowState}; use crate::error::EscrowError; #[derive(Accounts)] pub struct Cancel<'info> { #[account(mut)] - pub seller: Signer<'info>, + pub canceller: Signer<'info>, + + /// CHECK: Buyer receives vault refund if funds are present + #[account( + mut, + constraint = buyer.key() == escrow_account.buyer @ EscrowError::Unauthorized, + )] + pub buyer: UncheckedAccount<'info>, #[account( mut, seeds = [b"escrow", escrow_account.seller.as_ref(), &escrow_account.escrow_id.to_le_bytes()], bump = escrow_account.bump, - constraint = seller.key() == escrow_account.seller @ EscrowError::Unauthorized, - constraint = escrow_account.state == EscrowState::AwaitingDeposit @ EscrowError::InvalidState, - close = seller, + constraint = ( + canceller.key() == escrow_account.buyer || + canceller.key() == escrow_account.seller + ) @ EscrowError::Unauthorized, + constraint = ( + escrow_account.state == EscrowState::AwaitingDeposit || + escrow_account.state == EscrowState::AwaitingSellerConfirm + ) @ EscrowError::InvalidState, + close = canceller, )] pub escrow_account: Account<'info, EscrowAccount>, + /// CHECK: Vault PDA — may hold funds (AwaitingSellerConfirm) or be empty (AwaitingDeposit) + #[account( + mut, + seeds = [b"vault", escrow_account.key().as_ref()], + bump = escrow_account.vault_bump, + )] + pub vault: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, } -pub fn handler(_ctx: Context) -> Result<()> { +pub fn handler(ctx: Context) -> Result<()> { + let vault_lamports = ctx.accounts.vault.to_account_info().lamports(); + + if vault_lamports > 0 { + let escrow_key = ctx.accounts.escrow_account.key(); + let vault_bump = ctx.accounts.escrow_account.vault_bump; + + system_program::transfer( + CpiContext::new_with_signer( + system_program::ID, + Transfer { + from: ctx.accounts.vault.to_account_info(), + to: ctx.accounts.buyer.to_account_info(), + }, + &[&[b"vault", escrow_key.as_ref(), &[vault_bump]]], + ), + vault_lamports, + )?; + } + Ok(()) } diff --git a/programs/descro/src/instructions/create_escrow.rs b/programs/descro/src/instructions/create_escrow.rs index a8ab1df..c1f09ee 100644 --- a/programs/descro/src/instructions/create_escrow.rs +++ b/programs/descro/src/instructions/create_escrow.rs @@ -11,6 +11,9 @@ pub struct CreateEscrow<'info> { /// CHECK: Only stored as pubkey, no ownership check needed pub buyer: UncheckedAccount<'info>, + /// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated + pub resolver: UncheckedAccount<'info>, + #[account( init, payer = seller, @@ -28,6 +31,9 @@ pub struct CreateEscrow<'info> { )] pub vault: UncheckedAccount<'info>, + /// CHECK: Optional resolver registry entry — read to determine AcceptancePolicy + pub resolver_entry: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, } @@ -40,6 +46,23 @@ pub fn handler( let rent_min = Rent::get()?.minimum_balance(0); require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum); + if dispute_resolver.is_some() && !ctx.accounts.resolver_entry.data_is_empty() { + let entry_data = ctx.accounts.resolver_entry.try_borrow_data()?; + let entry = descro_ext_resolvers::state::ResolverEntry::try_deserialize( + &mut entry_data.as_ref(), + )?; + match entry.acceptance_policy { + descro_ext_resolvers::state::AcceptancePolicy::Open => {} + descro_ext_resolvers::state::AcceptancePolicy::SignatureGated => { + require!( + ctx.accounts.resolver.is_signer, + EscrowError::ResolverSignatureRequired + ); + } + descro_ext_resolvers::state::AcceptancePolicy::ProgramGated => {} + } + } + let escrow = &mut ctx.accounts.escrow_account; escrow.seller = ctx.accounts.seller.key(); escrow.buyer = ctx.accounts.buyer.key(); diff --git a/programs/descro/src/instructions/seller_confirm.rs b/programs/descro/src/instructions/seller_confirm.rs new file mode 100644 index 0000000..a0ffdf7 --- /dev/null +++ b/programs/descro/src/instructions/seller_confirm.rs @@ -0,0 +1,47 @@ +use anchor_lang::prelude::*; +use crate::state::{EscrowAccount, EscrowState}; +use crate::error::EscrowError; + +#[derive(Accounts)] +pub struct SellerConfirm<'info> { + pub seller: Signer<'info>, + + /// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated + pub resolver: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [b"escrow", seller.key().as_ref(), &escrow_account.escrow_id.to_le_bytes()], + bump = escrow_account.bump, + constraint = seller.key() == escrow_account.seller @ EscrowError::Unauthorized, + constraint = escrow_account.state == EscrowState::AwaitingSellerConfirm @ EscrowError::InvalidState, + )] + pub escrow_account: Account<'info, EscrowAccount>, + + /// CHECK: Optional resolver registry entry — read to determine AcceptancePolicy + pub resolver_entry: UncheckedAccount<'info>, +} + +pub fn handler(ctx: Context) -> Result<()> { + if ctx.accounts.escrow_account.dispute_resolver.is_some() + && !ctx.accounts.resolver_entry.data_is_empty() + { + let entry_data = ctx.accounts.resolver_entry.try_borrow_data()?; + let entry = descro_ext_resolvers::state::ResolverEntry::try_deserialize( + &mut entry_data.as_ref(), + )?; + match entry.acceptance_policy { + descro_ext_resolvers::state::AcceptancePolicy::Open => {} + descro_ext_resolvers::state::AcceptancePolicy::SignatureGated => { + require!( + ctx.accounts.resolver.is_signer, + EscrowError::ResolverSignatureRequired + ); + } + descro_ext_resolvers::state::AcceptancePolicy::ProgramGated => {} + } + } + + ctx.accounts.escrow_account.state = EscrowState::Active; + Ok(()) +} diff --git a/programs/descro/src/lib.rs b/programs/descro/src/lib.rs index 5cb209c..1b0cd96 100644 --- a/programs/descro/src/lib.rs +++ b/programs/descro/src/lib.rs @@ -26,13 +26,13 @@ pub mod descro { create_escrow::handler(ctx, amount, dispute_resolver, escrow_id) } - pub fn create_escrow_prefunded( - ctx: Context, + pub fn buyer_create_escrow( + ctx: Context, amount: u64, dispute_resolver: Option, escrow_id: u64, ) -> Result<()> { - create_escrow_prefunded::handler(ctx, amount, dispute_resolver, escrow_id) + buyer_create_escrow::handler(ctx, amount, dispute_resolver, escrow_id) } pub fn deposit(ctx: Context) -> Result<()> { @@ -51,6 +51,10 @@ pub mod descro { resolve::handler(ctx, winner) } + pub fn seller_confirm(ctx: Context) -> Result<()> { + seller_confirm::handler(ctx) + } + pub fn cancel(ctx: Context) -> Result<()> { cancel::handler(ctx) } diff --git a/programs/descro/src/state.rs b/programs/descro/src/state.rs index 8502580..582b059 100644 --- a/programs/descro/src/state.rs +++ b/programs/descro/src/state.rs @@ -17,6 +17,7 @@ pub struct EscrowAccount { #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)] pub enum EscrowState { AwaitingDeposit, + AwaitingSellerConfirm, Active, Disputed, Complete, diff --git a/programs/descro/tests/common/mod.rs b/programs/descro/tests/common/mod.rs index e505bb5..a156ae9 100644 --- a/programs/descro/tests/common/mod.rs +++ b/programs/descro/tests/common/mod.rs @@ -81,6 +81,24 @@ pub fn ix_create_escrow( amount: u64, dispute_resolver: Option, escrow_id: u64, +) -> Instruction { + // Default: derive the resolver_entry PDA from the resolver (won't exist → empty account → policy check skipped) + let dummy = Pubkey::new_from_array([1u8; 32]); + let (resolver, resolver_entry) = match dispute_resolver { + Some(r) => (r, resolver_entry_pda(&r)), + None => (dummy, dummy), + }; + ix_create_escrow_with_resolver(seller, buyer, amount, dispute_resolver, escrow_id, &resolver, &resolver_entry) +} + +pub fn ix_create_escrow_with_resolver( + seller: &Pubkey, + buyer: &Pubkey, + amount: u64, + dispute_resolver: Option, + escrow_id: u64, + resolver: &Pubkey, + resolver_entry: &Pubkey, ) -> Instruction { let escrow = escrow_pda(seller, escrow_id); let vault = vault_pda(&escrow); @@ -95,8 +113,10 @@ pub fn ix_create_escrow( descro::accounts::CreateEscrow { seller: *seller, buyer: *buyer, + resolver: *resolver, escrow_account: escrow, vault, + resolver_entry: *resolver_entry, system_program: system_program::ID, } .to_account_metas(None), @@ -182,20 +202,66 @@ pub fn ix_resolve( ) } -pub fn ix_cancel(seller: &Pubkey, escrow_id: u64) -> Instruction { +pub fn ix_cancel(canceller: &Pubkey, buyer: &Pubkey, seller: &Pubkey, escrow_id: u64) -> Instruction { let escrow = escrow_pda(seller, escrow_id); + let vault = vault_pda(&escrow); Instruction::new_with_bytes( descro::id(), &descro::instruction::Cancel {}.data(), descro::accounts::Cancel { + canceller: *canceller, + buyer: *buyer, + escrow_account: escrow, + vault, + system_program: system_program::ID, + } + .to_account_metas(None), + ) +} + +pub fn ix_buyer_create_escrow( + buyer: &Pubkey, + seller: &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::BuyerCreateEscrow { + amount, + dispute_resolver, + escrow_id, + } + .data(), + descro::accounts::BuyerCreateEscrow { + buyer: *buyer, seller: *seller, escrow_account: escrow, + vault, system_program: system_program::ID, } .to_account_metas(None), ) } +pub fn ix_seller_confirm(seller: &Pubkey, resolver: &Pubkey, resolver_entry: &Pubkey, escrow_id: u64) -> Instruction { + let escrow = escrow_pda(seller, escrow_id); + Instruction::new_with_bytes( + descro::id(), + &descro::instruction::SellerConfirm {}.data(), + descro::accounts::SellerConfirm { + seller: *seller, + resolver: *resolver, + escrow_account: escrow, + resolver_entry: *resolver_entry, + } + .to_account_metas(None), + ) +} + pub fn read_escrow(svm: &LiteSVM, seller: &Pubkey, escrow_id: u64) -> EscrowAccount { let escrow = escrow_pda(seller, escrow_id); let account = svm.get_account(&escrow).expect("escrow account not found"); @@ -247,30 +313,3 @@ pub fn ix_emergency_resolve( ) } -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_buyer_flow.rs b/programs/descro/tests/test_buyer_flow.rs new file mode 100644 index 0000000..ea47bf8 --- /dev/null +++ b/programs/descro/tests/test_buyer_flow.rs @@ -0,0 +1,74 @@ +mod common; +use common::*; +use anchor_lang::solana_program::system_program; +use solana_signer::Signer; + +#[test] +fn buyer_can_create_and_fund_escrow() { + let (mut svm, seller, buyer, _resolver) = setup(); + + send( + &mut svm, + ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID), + &buyer, + ); + + let escrow = read_escrow(&svm, &seller.pubkey(), ESCROW_ID); + assert_eq!(escrow.state, EscrowState::AwaitingSellerConfirm); + assert_eq!(escrow.buyer, buyer.pubkey()); + assert_eq!(escrow.seller, seller.pubkey()); + assert_eq!(escrow.amount, AMOUNT); + + let vault = vault_pda(&escrow_pda(&seller.pubkey(), ESCROW_ID)); + let vault_lamports = svm.get_account(&vault).map(|a| a.lamports).unwrap_or(0); + assert_eq!(vault_lamports, AMOUNT); +} + +#[test] +fn buyer_create_escrow_fails_below_rent_minimum() { + let (mut svm, seller, buyer, _resolver) = setup(); + + assert!(!try_send( + &mut svm, + ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), 0, None, ESCROW_ID), + &buyer, + )); +} + +#[test] +fn seller_can_confirm_buyer_escrow() { + let (mut svm, seller, buyer, _resolver) = setup(); + + send( + &mut svm, + ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID), + &buyer, + ); + + send( + &mut svm, + ix_seller_confirm(&seller.pubkey(), &seller.pubkey(), &system_program::ID, ESCROW_ID), + &seller, + ); + + let escrow = read_escrow(&svm, &seller.pubkey(), ESCROW_ID); + assert_eq!(escrow.state, EscrowState::Active); +} + +#[test] +fn seller_confirm_fails_from_wrong_state() { + let (mut svm, seller, buyer, _resolver) = setup(); + + // Seller-initiated → AwaitingDeposit — seller_confirm must fail + send( + &mut svm, + ix_create_escrow(&seller.pubkey(), &buyer.pubkey(), AMOUNT, None, ESCROW_ID), + &seller, + ); + + assert!(!try_send( + &mut svm, + ix_seller_confirm(&seller.pubkey(), &seller.pubkey(), &system_program::ID, ESCROW_ID), + &seller, + )); +} diff --git a/programs/descro/tests/test_cancel.rs b/programs/descro/tests/test_cancel.rs index 9f5742a..b364d0e 100644 --- a/programs/descro/tests/test_cancel.rs +++ b/programs/descro/tests/test_cancel.rs @@ -3,7 +3,7 @@ use common::*; use solana_signer::Signer; #[test] -fn cancel_returns_rent_to_seller() { +fn seller_can_cancel_awaiting_deposit() { let (mut svm, seller, buyer, _resolver) = setup(); send( @@ -14,7 +14,7 @@ fn cancel_returns_rent_to_seller() { let seller_before = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); - send(&mut svm, ix_cancel(&seller.pubkey(), ESCROW_ID), &seller); + send(&mut svm, ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &seller); assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); let seller_after = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); @@ -22,7 +22,60 @@ fn cancel_returns_rent_to_seller() { } #[test] -fn cancel_after_deposit_fails() { +fn buyer_can_cancel_awaiting_deposit() { + let (mut svm, seller, buyer, _resolver) = setup(); + + send( + &mut svm, + ix_create_escrow(&seller.pubkey(), &buyer.pubkey(), AMOUNT, None, ESCROW_ID), + &seller, + ); + + send(&mut svm, ix_cancel(&buyer.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer); + + assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); +} + +#[test] +fn seller_can_cancel_awaiting_seller_confirm() { + let (mut svm, seller, buyer, _resolver) = setup(); + + send( + &mut svm, + ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID), + &buyer, + ); + + let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0); + + send(&mut svm, ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &seller); + + assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); + let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0); + assert!(buyer_after > buyer_before, "buyer should have received vault refund"); +} + +#[test] +fn buyer_can_cancel_awaiting_seller_confirm() { + let (mut svm, seller, buyer, _resolver) = setup(); + + send( + &mut svm, + ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID), + &buyer, + ); + + let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0); + + send(&mut svm, ix_cancel(&buyer.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer); + + assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); + let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0); + assert!(buyer_after > buyer_before, "buyer should have received vault refund"); +} + +#[test] +fn cannot_cancel_active_escrow() { let (mut svm, seller, buyer, _resolver) = setup(); send( @@ -34,7 +87,7 @@ fn cancel_after_deposit_fails() { assert!(!try_send( &mut svm, - ix_cancel(&seller.pubkey(), ESCROW_ID), + ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &seller, )); } diff --git a/programs/descro/tests/test_create_escrow_prefunded.rs b/programs/descro/tests/test_create_escrow_prefunded.rs deleted file mode 100644 index 4e51381..0000000 --- a/programs/descro/tests/test_create_escrow_prefunded.rs +++ /dev/null @@ -1,107 +0,0 @@ -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()); -} diff --git a/programs/descro/tests/test_resolve_with_registry.rs b/programs/descro/tests/test_resolve_with_registry.rs index 6129417..5ecefef 100644 --- a/programs/descro/tests/test_resolve_with_registry.rs +++ b/programs/descro/tests/test_resolve_with_registry.rs @@ -5,7 +5,7 @@ use common::*; use anchor_lang::InstructionData; use anchor_lang::ToAccountMetas; use anchor_lang::solana_program::{instruction::Instruction, system_program}; -use descro_ext_resolvers::{ResolverType, ResolverEntry}; +use descro_ext_resolvers::{AcceptancePolicy, ResolverType, ResolverEntry}; use anchor_lang::AccountDeserialize; use litesvm::LiteSVM; use solana_keypair::Keypair; @@ -37,6 +37,7 @@ fn ix_register_resolver(authority: &Pubkey) -> Instruction { descro_ext_resolvers::id(), &descro_ext_resolvers::instruction::RegisterResolver { resolver_type: ResolverType::CentralAuthority, + acceptance_policy: AcceptancePolicy::Open, name: "Test Resolver".to_string(), description: "Integration test resolver".to_string(), fee_bps: 100, diff --git a/programs/descro_ext_resolvers/src/instructions/register.rs b/programs/descro_ext_resolvers/src/instructions/register.rs index e3bb4c1..b79ce77 100644 --- a/programs/descro_ext_resolvers/src/instructions/register.rs +++ b/programs/descro_ext_resolvers/src/instructions/register.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use crate::state::{ResolverEntry, ResolverType}; +use crate::state::{AcceptancePolicy, ResolverEntry, ResolverType}; use crate::error::RegistryError; #[derive(Accounts)] @@ -22,6 +22,7 @@ pub struct RegisterResolver<'info> { pub fn handler( ctx: Context, resolver_type: ResolverType, + acceptance_policy: AcceptancePolicy, name: String, description: String, fee_bps: u16, @@ -34,6 +35,7 @@ pub fn handler( let entry = &mut ctx.accounts.resolver_entry; entry.authority = ctx.accounts.authority.key(); entry.resolver_type = resolver_type; + entry.acceptance_policy = acceptance_policy; entry.name = name; entry.description = description; entry.fee_bps = fee_bps; diff --git a/programs/descro_ext_resolvers/src/lib.rs b/programs/descro_ext_resolvers/src/lib.rs index 1369f16..03e50da 100644 --- a/programs/descro_ext_resolvers/src/lib.rs +++ b/programs/descro_ext_resolvers/src/lib.rs @@ -1,4 +1,5 @@ #![allow(clippy::diverging_sub_expression)] +#![allow(clippy::too_many_arguments)] pub mod error; pub mod instructions; @@ -23,6 +24,7 @@ pub mod descro_ext_resolvers { pub fn register_resolver( ctx: Context, resolver_type: ResolverType, + acceptance_policy: AcceptancePolicy, name: String, description: String, fee_bps: u16, @@ -32,6 +34,7 @@ pub mod descro_ext_resolvers { register::handler( ctx, resolver_type, + acceptance_policy, name, description, fee_bps, diff --git a/programs/descro_ext_resolvers/src/state.rs b/programs/descro_ext_resolvers/src/state.rs index 30f5df0..e4d8bdb 100644 --- a/programs/descro_ext_resolvers/src/state.rs +++ b/programs/descro_ext_resolvers/src/state.rs @@ -5,6 +5,7 @@ use anchor_lang::prelude::*; pub struct ResolverEntry { pub authority: Pubkey, pub resolver_type: ResolverType, + pub acceptance_policy: AcceptancePolicy, #[max_len(64)] pub name: String, #[max_len(256)] @@ -28,6 +29,13 @@ pub enum ResolverType { Multisig, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)] +pub enum AcceptancePolicy { + Open, + SignatureGated, + ProgramGated, +} + /// Passed to update_stats; mirrors the Escrow program's Winner enum. #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] pub enum Ruling { diff --git a/programs/descro_ext_resolvers/tests/common/mod.rs b/programs/descro_ext_resolvers/tests/common/mod.rs index 1abe366..0f624a3 100644 --- a/programs/descro_ext_resolvers/tests/common/mod.rs +++ b/programs/descro_ext_resolvers/tests/common/mod.rs @@ -1,4 +1,4 @@ -#![allow(dead_code, unused_imports)] +#![allow(dead_code, unused_imports, clippy::too_many_arguments)] pub use anchor_lang::prelude::Pubkey; use { @@ -6,7 +6,7 @@ use { solana_program::{instruction::Instruction, system_program}, AccountDeserialize, InstructionData, ToAccountMetas, }, - descro_ext_resolvers::{ResolverEntry, ResolverType}, + descro_ext_resolvers::{AcceptancePolicy, ResolverEntry, ResolverType}, litesvm::LiteSVM, solana_keypair::Keypair, solana_message::{Message, VersionedMessage}, @@ -51,6 +51,7 @@ pub fn try_send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) -> bool { pub fn ix_register( authority: &Pubkey, resolver_type: ResolverType, + acceptance_policy: AcceptancePolicy, name: &str, description: &str, fee_bps: u16, @@ -62,6 +63,7 @@ pub fn ix_register( descro_ext_resolvers::id(), &descro_ext_resolvers::instruction::RegisterResolver { resolver_type, + acceptance_policy, name: name.to_string(), description: description.to_string(), fee_bps, diff --git a/programs/descro_ext_resolvers/tests/test_register.rs b/programs/descro_ext_resolvers/tests/test_register.rs index d221c76..da2c749 100644 --- a/programs/descro_ext_resolvers/tests/test_register.rs +++ b/programs/descro_ext_resolvers/tests/test_register.rs @@ -1,6 +1,6 @@ mod common; use common::*; -use descro_ext_resolvers::ResolverType; +use descro_ext_resolvers::{AcceptancePolicy, ResolverType}; use solana_keypair::Keypair; use solana_signer::Signer; @@ -13,6 +13,7 @@ fn registers_entry_with_correct_state() { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "Acme Resolvers", "Fast and fair dispute resolution", 100, // 1% @@ -25,6 +26,7 @@ fn registers_entry_with_correct_state() { let entry = read_entry(&svm, &authority.pubkey()); assert_eq!(entry.authority, authority.pubkey()); assert_eq!(entry.resolver_type, ResolverType::CentralAuthority); + assert_eq!(entry.acceptance_policy, AcceptancePolicy::Open); assert_eq!(entry.name, "Acme Resolvers"); assert_eq!(entry.fee_bps, 100); assert_eq!(entry.total_resolved, 0); @@ -41,6 +43,7 @@ fn duplicate_register_fails() { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "Acme", "", 0, @@ -54,6 +57,7 @@ fn duplicate_register_fails() { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "Acme Again", "", 0, @@ -73,6 +77,7 @@ fn register_empty_name_fails() { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "", "", 0, @@ -92,6 +97,7 @@ fn register_invalid_fee_bps_fails() { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "Acme", "", 10_001, // > 100% @@ -110,12 +116,12 @@ fn different_authorities_have_separate_entries() { send( &mut svm, - ix_register(&auth1.pubkey(), ResolverType::CentralAuthority, "Auth1", "", 0, auth1.pubkey(), ""), + ix_register(&auth1.pubkey(), ResolverType::CentralAuthority, AcceptancePolicy::Open, "Auth1", "", 0, auth1.pubkey(), ""), &auth1, ); send( &mut svm, - ix_register(&auth2.pubkey(), ResolverType::JuryDAO, "Auth2", "", 50, auth2.pubkey(), ""), + ix_register(&auth2.pubkey(), ResolverType::JuryDAO, AcceptancePolicy::Open, "Auth2", "", 50, auth2.pubkey(), ""), &auth2, ); diff --git a/programs/descro_ext_resolvers/tests/test_update.rs b/programs/descro_ext_resolvers/tests/test_update.rs index 739da4b..895478f 100644 --- a/programs/descro_ext_resolvers/tests/test_update.rs +++ b/programs/descro_ext_resolvers/tests/test_update.rs @@ -1,6 +1,6 @@ mod common; use common::*; -use descro_ext_resolvers::ResolverType; +use descro_ext_resolvers::{AcceptancePolicy, ResolverType}; use solana_keypair::Keypair; use solana_signer::Signer; @@ -10,6 +10,7 @@ fn register_default(svm: &mut litesvm::LiteSVM, authority: &Keypair) { ix_register( &authority.pubkey(), ResolverType::CentralAuthority, + AcceptancePolicy::Open, "Original Name", "Original description", 100,