diff --git a/docs/superpowers/plans/2026-05-19-descro-buyer-flow.md b/docs/superpowers/plans/2026-05-19-descro-buyer-flow.md index bc66034..32718cc 100644 --- a/docs/superpowers/plans/2026-05-19-descro-buyer-flow.md +++ b/docs/superpowers/plans/2026-05-19-descro-buyer-flow.md @@ -37,6 +37,7 @@ From Active (unchanged): | Modify | `programs/descro/src/state.rs` | | Create | `programs/descro/src/instructions/buyer_create_escrow.rs` | | Create | `programs/descro/src/instructions/seller_confirm.rs` | +| Modify | `programs/descro/src/instructions/create_escrow.rs` | | Modify | `programs/descro/src/instructions/cancel.rs` | | Modify | `programs/descro/src/lib.rs` | | Delete | `programs/descro/src/instructions/create_escrow_prefunded.rs` | @@ -179,6 +180,8 @@ And add to the `cpi::accounts` module for CPI callers (solisting needs this): // but verify the IDL exports BuyerCreateEscrow accounts correctly after build) ``` +> **Note on AcceptancePolicy:** `buyer_create_escrow` intentionally has no AcceptancePolicy check. The resolver's primary relationship is with the seller (the seller selects and configures the resolver). The check therefore happens on the seller's transaction: `seller_confirm` in the buyer-initiated flow, `create_escrow` in the seller-initiated flow. `buyer_create_escrow` is the buyer's transaction — the seller is not yet involved and a resolver co-sign here would be premature. See Task 3 and Task 3b. + - [ ] **Step 3: Write test** In `programs/descro/tests/` (new file `test_buyer_flow.rs` or add to existing): @@ -273,6 +276,8 @@ pub fn handler(ctx: Context) -> Result<()> { > **Note:** Add `ResolverSignatureRequired` to `EscrowError` if not already present. +> **Why here and not at `buyer_create_escrow`:** The resolver co-signs the seller's transaction because the seller holds the primary relationship with the resolver (the seller chose and configured them). This is symmetric across both flows: `seller_confirm` is the seller's transaction in the buyer-initiated flow; `create_escrow` is the seller's transaction in the seller-initiated flow. See Task 3b for the seller-initiated counterpart. + - [ ] **Step 2: Register in `lib.rs`** ```rust @@ -315,6 +320,127 @@ cargo test --manifest-path programs/descro/Cargo.toml 2>&1 | tail -15 --- +## Task 3b: Extend `create_escrow` — Add AcceptancePolicy Check + +**File:** `programs/descro/src/instructions/create_escrow.rs` + +**Why here:** `create_escrow` is the seller's transaction in the seller-initiated flow — symmetric to `seller_confirm` in the buyer-initiated flow. The resolver co-signs alongside the seller, the party who chose and configured the resolver. + +- [ ] **Step 1: Add `resolver` and `resolver_entry` accounts and policy check** + +```rust +use anchor_lang::prelude::*; +use crate::state::{EscrowAccount, EscrowState}; +use crate::EscrowError; + +#[derive(Accounts)] +#[instruction(amount: u64, dispute_resolver: Option, escrow_id: u64)] +pub struct CreateEscrow<'info> { + #[account(mut)] + pub seller: Signer<'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: AccountInfo<'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: PDA vault for holding escrow SOL; created implicitly on deposit + #[account( + mut, + seeds = [b"vault", escrow_account.key().as_ref()], + bump + )] + pub vault: UncheckedAccount<'info>, + + /// CHECK: Optional resolver registry entry — read to determine AcceptancePolicy + pub resolver_entry: UncheckedAccount<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, + amount: u64, + dispute_resolver: Option, + escrow_id: u64, +) -> Result<()> { + let rent_min = Rent::get()?.minimum_balance(0); + require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum); + + // AcceptancePolicy check — only if resolver is set and has a registry entry + 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 => { + // Future: CPI to resolver program's accept_escrow instruction + } + } + } + + 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::AwaitingDeposit; + escrow.bump = ctx.bumps.escrow_account; + escrow.vault_bump = ctx.bumps.vault; + escrow.escrow_id = escrow_id; + escrow.dispute_raised_at = None; + Ok(()) +} +``` + +- [ ] **Step 2: Update existing tests that call `create_escrow`** + +The accounts struct now requires `resolver` and `resolver_entry`. For tests without a registered resolver, pass a dummy account for `resolver` (no signer needed for `Open` or absent resolver) and an empty/system account for `resolver_entry`. + +- [ ] **Step 3: Add policy tests** + +```rust +#[test] +fn create_escrow_open_resolver_no_cosign_needed() { + // Register resolver with AcceptancePolicy::Open + // create_escrow without resolver signing → should succeed +} + +#[test] +fn create_escrow_signature_gated_requires_resolver_cosign() { + // Register resolver with AcceptancePolicy::SignatureGated + // create_escrow WITHOUT resolver signing → must fail with ResolverSignatureRequired + // create_escrow WITH resolver co-signing → AwaitingDeposit +} +``` + +- [ ] **Step 4: Build and test** + +```bash +cargo build-sbf --manifest-path programs/descro/Cargo.toml && \ +cargo test --manifest-path programs/descro/Cargo.toml 2>&1 | tail -15 +``` + +--- + ## Task 4: Extend `cancel` — Symmetric Pre-Active Cancellation **File:** `programs/descro/src/instructions/cancel.rs`