buyer initiated flow

This commit is contained in:
thesn10
2026-05-26 21:33:32 +02:00
parent dd5ec25ff9
commit 9d9c7a8a43
19 changed files with 382 additions and 168 deletions

View File

@@ -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,
}

View File

@@ -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::*;

View File

@@ -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<Pubkey>, 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<CreateEscrowPrefunded>,
ctx: Context<BuyerCreateEscrow>,
amount: u64,
dispute_resolver: Option<Pubkey>,
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;

View File

@@ -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<Cancel>) -> Result<()> {
pub fn handler(ctx: Context<Cancel>) -> 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(())
}

View File

@@ -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();

View File

@@ -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<SellerConfirm>) -> 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(())
}

View File

@@ -26,13 +26,13 @@ pub mod descro {
create_escrow::handler(ctx, amount, dispute_resolver, escrow_id)
}
pub fn create_escrow_prefunded(
ctx: Context<CreateEscrowPrefunded>,
pub fn buyer_create_escrow(
ctx: Context<BuyerCreateEscrow>,
amount: u64,
dispute_resolver: Option<Pubkey>,
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<Deposit>) -> Result<()> {
@@ -51,6 +51,10 @@ pub mod descro {
resolve::handler(ctx, winner)
}
pub fn seller_confirm(ctx: Context<SellerConfirm>) -> Result<()> {
seller_confirm::handler(ctx)
}
pub fn cancel(ctx: Context<Cancel>) -> Result<()> {
cancel::handler(ctx)
}

View File

@@ -17,6 +17,7 @@ pub struct EscrowAccount {
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum EscrowState {
AwaitingDeposit,
AwaitingSellerConfirm,
Active,
Disputed,
Complete,

View File

@@ -81,6 +81,24 @@ pub fn ix_create_escrow(
amount: u64,
dispute_resolver: Option<Pubkey>,
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<Pubkey>,
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<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::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<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::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),
)
}

View File

@@ -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,
));
}

View File

@@ -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,
));
}

View File

@@ -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());
}

View File

@@ -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,

View File

@@ -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<RegisterResolver>,
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;

View File

@@ -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<RegisterResolver>,
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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,
);

View File

@@ -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,