descro resolvers extension
This commit is contained in:
@@ -22,6 +22,7 @@ custom-panic = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "1.0.2"
|
||||
descro_ext_resolvers = { path = "../descro_ext_resolvers", features = ["cpi"] }
|
||||
|
||||
[dev-dependencies]
|
||||
litesvm = "0.10.0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::{program::invoke_signed, system_instruction};
|
||||
use descro_ext_resolvers::state::Ruling;
|
||||
use crate::state::{EscrowAccount, EscrowState, Winner};
|
||||
use crate::error::EscrowError;
|
||||
|
||||
@@ -23,6 +24,8 @@ pub struct Resolve<'info> {
|
||||
seeds = [b"escrow", escrow_account.seller.as_ref(), &escrow_account.escrow_id.to_le_bytes()],
|
||||
bump = escrow_account.bump,
|
||||
constraint = escrow_account.state == EscrowState::Disputed @ EscrowError::InvalidState,
|
||||
constraint = escrow_account.dispute_resolver.is_some() @ EscrowError::NoResolverConfigured,
|
||||
constraint = resolver.key() == escrow_account.dispute_resolver.unwrap() @ EscrowError::UnauthorizedResolver,
|
||||
close = seller,
|
||||
)]
|
||||
pub escrow_account: Account<'info, EscrowAccount>,
|
||||
@@ -35,19 +38,32 @@ pub struct Resolve<'info> {
|
||||
)]
|
||||
pub vault: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Registry entry for this resolver; if initialized, stats are updated via CPI
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"resolver", escrow_account.dispute_resolver.unwrap_or_default().as_ref()],
|
||||
bump,
|
||||
seeds::program = descro_ext_resolvers::ID,
|
||||
)]
|
||||
pub resolver_entry: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Escrow program authority PDA — signs the update_stats CPI
|
||||
#[account(
|
||||
seeds = [b"escrow_authority"],
|
||||
bump,
|
||||
)]
|
||||
pub escrow_authority: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Registry program account; validated against descro_ext_resolvers::ID in handler
|
||||
pub registry_program: UncheckedAccount<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<Resolve>, winner: Winner) -> Result<()> {
|
||||
let escrow = &ctx.accounts.escrow_account;
|
||||
|
||||
require!(escrow.dispute_resolver.is_some(), EscrowError::NoResolverConfigured);
|
||||
require!(
|
||||
ctx.accounts.resolver.key() == escrow.dispute_resolver.unwrap(),
|
||||
EscrowError::UnauthorizedResolver
|
||||
);
|
||||
|
||||
let expected_winner = match winner {
|
||||
let expected_winner = match &winner {
|
||||
Winner::Buyer => escrow.buyer,
|
||||
Winner::Seller => escrow.seller,
|
||||
};
|
||||
@@ -75,5 +91,27 @@ pub fn handler(ctx: Context<Resolve>, winner: Winner) -> Result<()> {
|
||||
&[&[b"vault", escrow_key.as_ref(), &[vault_bump]]],
|
||||
)?;
|
||||
|
||||
// Update resolver stats if the entry is registered in the registry
|
||||
if !ctx.accounts.resolver_entry.data_is_empty()
|
||||
&& ctx.accounts.registry_program.key() == descro_ext_resolvers::ID
|
||||
{
|
||||
let ruling = match winner {
|
||||
Winner::Buyer => Ruling::Buyer,
|
||||
Winner::Seller => Ruling::Seller,
|
||||
};
|
||||
let escrow_authority_bump = ctx.bumps.escrow_authority;
|
||||
descro_ext_resolvers::cpi::update_stats(
|
||||
CpiContext::new_with_signer(
|
||||
descro_ext_resolvers::ID,
|
||||
descro_ext_resolvers::cpi::accounts::UpdateStats {
|
||||
escrow_authority: ctx.accounts.escrow_authority.to_account_info(),
|
||||
resolver_entry: ctx.accounts.resolver_entry.to_account_info(),
|
||||
},
|
||||
&[&[b"escrow_authority", &[escrow_authority_bump]]],
|
||||
),
|
||||
ruling,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ pub fn vault_pda(escrow: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"vault", escrow.as_ref()], &descro::id()).0
|
||||
}
|
||||
|
||||
pub fn escrow_authority_pda() -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"escrow_authority"], &descro::id()).0
|
||||
}
|
||||
|
||||
pub fn resolver_entry_pda(resolver_authority: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[b"resolver", resolver_authority.as_ref()],
|
||||
&descro_ext_resolvers::id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) {
|
||||
let blockhash = svm.latest_blockhash();
|
||||
let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
|
||||
@@ -128,15 +140,21 @@ pub fn ix_dispute(initiator: &Pubkey, seller: &Pubkey, escrow_id: u64) -> Instru
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a resolve instruction. Pass the actual registry program ID when testing with a
|
||||
/// registered resolver; for unregistered resolvers pass `system_program::ID` as
|
||||
/// `registry_program` (the handler skips the CPI if resolver_entry has no data).
|
||||
pub fn ix_resolve(
|
||||
resolver: &Pubkey,
|
||||
winner_pubkey: &Pubkey,
|
||||
seller: &Pubkey,
|
||||
escrow_id: u64,
|
||||
winner: Winner,
|
||||
registry_program: Pubkey,
|
||||
) -> Instruction {
|
||||
let escrow = escrow_pda(seller, escrow_id);
|
||||
let vault = vault_pda(&escrow);
|
||||
let resolver_entry = resolver_entry_pda(resolver);
|
||||
let escrow_authority = escrow_authority_pda();
|
||||
Instruction::new_with_bytes(
|
||||
descro::id(),
|
||||
&descro::instruction::Resolve { winner }.data(),
|
||||
@@ -146,6 +164,9 @@ pub fn ix_resolve(
|
||||
seller: *seller,
|
||||
escrow_account: escrow,
|
||||
vault,
|
||||
resolver_entry,
|
||||
escrow_authority,
|
||||
registry_program,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use anchor_lang::solana_program::system_program;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
@@ -33,7 +34,14 @@ fn resolver_can_rule_for_seller() {
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_resolve(&resolver.pubkey(), &seller.pubkey(), &seller.pubkey(), ESCROW_ID, EscrowWinner::Seller),
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
system_program::ID, // no registry
|
||||
),
|
||||
&resolver,
|
||||
);
|
||||
|
||||
@@ -51,7 +59,14 @@ fn resolver_can_rule_for_buyer() {
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_resolve(&resolver.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID, EscrowWinner::Buyer),
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&buyer.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Buyer,
|
||||
system_program::ID, // no registry
|
||||
),
|
||||
&resolver,
|
||||
);
|
||||
|
||||
@@ -73,7 +88,14 @@ fn resolve_without_configured_resolver_fails() {
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_resolve(&resolver.pubkey(), &seller.pubkey(), &seller.pubkey(), ESCROW_ID, EscrowWinner::Seller),
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
system_program::ID,
|
||||
),
|
||||
&resolver,
|
||||
));
|
||||
}
|
||||
@@ -87,7 +109,14 @@ fn wrong_resolver_fails() {
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_resolve(&wrong.pubkey(), &seller.pubkey(), &seller.pubkey(), ESCROW_ID, EscrowWinner::Seller),
|
||||
ix_resolve(
|
||||
&wrong.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
system_program::ID,
|
||||
),
|
||||
&wrong,
|
||||
));
|
||||
}
|
||||
@@ -109,10 +138,16 @@ fn resolve_before_dispute_fails() {
|
||||
);
|
||||
send(&mut svm, ix_deposit(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
|
||||
// State is Active, not Disputed
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_resolve(&resolver.pubkey(), &seller.pubkey(), &seller.pubkey(), ESCROW_ID, EscrowWinner::Seller),
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
system_program::ID,
|
||||
),
|
||||
&resolver,
|
||||
));
|
||||
}
|
||||
|
||||
177
programs/descro/tests/test_resolve_with_registry.rs
Normal file
177
programs/descro/tests/test_resolve_with_registry.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
/// Integration test: resolve() calls update_stats() via CPI when the resolver
|
||||
/// has a registered entry in the Resolver Registry.
|
||||
mod common;
|
||||
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 anchor_lang::AccountDeserialize;
|
||||
use litesvm::LiteSVM;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
fn setup_both_programs() -> (LiteSVM, Keypair, Keypair, Keypair) {
|
||||
let mut svm = LiteSVM::new();
|
||||
|
||||
let escrow_bytes = include_bytes!("../../../target/deploy/descro.so");
|
||||
svm.add_program(descro::id(), escrow_bytes).unwrap();
|
||||
|
||||
let registry_bytes = include_bytes!("../../../target/deploy/descro_ext_resolvers.so");
|
||||
svm.add_program(descro_ext_resolvers::id(), registry_bytes).unwrap();
|
||||
|
||||
let seller = Keypair::new();
|
||||
let buyer = Keypair::new();
|
||||
let resolver = Keypair::new();
|
||||
|
||||
svm.airdrop(&seller.pubkey(), 10_000_000_000).unwrap();
|
||||
svm.airdrop(&buyer.pubkey(), 10_000_000_000).unwrap();
|
||||
svm.airdrop(&resolver.pubkey(), 5_000_000_000).unwrap();
|
||||
|
||||
(svm, seller, buyer, resolver)
|
||||
}
|
||||
|
||||
fn ix_register_resolver(authority: &Pubkey) -> Instruction {
|
||||
let entry = resolver_entry_pda(authority);
|
||||
Instruction::new_with_bytes(
|
||||
descro_ext_resolvers::id(),
|
||||
&descro_ext_resolvers::instruction::RegisterResolver {
|
||||
resolver_type: ResolverType::CentralAuthority,
|
||||
name: "Test Resolver".to_string(),
|
||||
description: "Integration test resolver".to_string(),
|
||||
fee_bps: 100,
|
||||
fee_recipient: *authority,
|
||||
metadata_uri: "ipfs://test".to_string(),
|
||||
}
|
||||
.data(),
|
||||
descro_ext_resolvers::accounts::RegisterResolver {
|
||||
authority: *authority,
|
||||
resolver_entry: entry,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
fn read_resolver_entry(svm: &LiteSVM, authority: &Pubkey) -> ResolverEntry {
|
||||
let pda = resolver_entry_pda(authority);
|
||||
let account = svm.get_account(&pda).expect("resolver entry not found");
|
||||
ResolverEntry::try_deserialize(&mut account.data.as_slice()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_updates_stats_when_resolver_registered() {
|
||||
let (mut svm, seller, buyer, resolver) = setup_both_programs();
|
||||
|
||||
// Register the resolver in the Registry
|
||||
send(&mut svm, ix_register_resolver(&resolver.pubkey()), &resolver);
|
||||
|
||||
// Full escrow flow
|
||||
send(
|
||||
&mut svm,
|
||||
ix_create_escrow(
|
||||
&seller.pubkey(),
|
||||
&buyer.pubkey(),
|
||||
AMOUNT,
|
||||
Some(resolver.pubkey()),
|
||||
ESCROW_ID,
|
||||
),
|
||||
&seller,
|
||||
);
|
||||
send(&mut svm, ix_deposit(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
send(&mut svm, ix_dispute(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
|
||||
// Resolve with registry program provided
|
||||
send(
|
||||
&mut svm,
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
descro_ext_resolvers::id(), // pass registry program → triggers CPI
|
||||
),
|
||||
&resolver,
|
||||
);
|
||||
|
||||
let entry = read_resolver_entry(&svm, &resolver.pubkey());
|
||||
assert_eq!(entry.total_resolved, 1);
|
||||
assert_eq!(entry.ruled_for_seller, 1);
|
||||
assert_eq!(entry.ruled_for_buyer, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_skips_stats_when_resolver_not_registered() {
|
||||
let (mut svm, seller, buyer, resolver) = setup_both_programs();
|
||||
|
||||
// Do NOT register resolver — entry does not exist
|
||||
send(
|
||||
&mut svm,
|
||||
ix_create_escrow(
|
||||
&seller.pubkey(),
|
||||
&buyer.pubkey(),
|
||||
AMOUNT,
|
||||
Some(resolver.pubkey()),
|
||||
ESCROW_ID,
|
||||
),
|
||||
&seller,
|
||||
);
|
||||
send(&mut svm, ix_deposit(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
send(&mut svm, ix_dispute(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
|
||||
// Resolve without passing registry program — CPI skipped
|
||||
send(
|
||||
&mut svm,
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&seller.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Seller,
|
||||
system_program::ID, // no registry → CPI skipped
|
||||
),
|
||||
&resolver,
|
||||
);
|
||||
|
||||
// Escrow closed, resolver entry still absent
|
||||
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
|
||||
assert!(svm.get_account(&resolver_entry_pda(&resolver.pubkey())).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_buyer_wins_increments_ruled_for_buyer() {
|
||||
let (mut svm, seller, buyer, resolver) = setup_both_programs();
|
||||
|
||||
send(&mut svm, ix_register_resolver(&resolver.pubkey()), &resolver);
|
||||
send(
|
||||
&mut svm,
|
||||
ix_create_escrow(
|
||||
&seller.pubkey(),
|
||||
&buyer.pubkey(),
|
||||
AMOUNT,
|
||||
Some(resolver.pubkey()),
|
||||
ESCROW_ID,
|
||||
),
|
||||
&seller,
|
||||
);
|
||||
send(&mut svm, ix_deposit(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
send(&mut svm, ix_dispute(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
|
||||
send(
|
||||
&mut svm,
|
||||
ix_resolve(
|
||||
&resolver.pubkey(),
|
||||
&buyer.pubkey(),
|
||||
&seller.pubkey(),
|
||||
ESCROW_ID,
|
||||
EscrowWinner::Buyer,
|
||||
descro_ext_resolvers::id(),
|
||||
),
|
||||
&resolver,
|
||||
);
|
||||
|
||||
let entry = read_resolver_entry(&svm, &resolver.pubkey());
|
||||
assert_eq!(entry.total_resolved, 1);
|
||||
assert_eq!(entry.ruled_for_buyer, 1);
|
||||
assert_eq!(entry.ruled_for_seller, 0);
|
||||
}
|
||||
33
programs/descro_ext_resolvers/Cargo.toml
Normal file
33
programs/descro_ext_resolvers/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "descro_ext_resolvers"
|
||||
version = "0.1.0"
|
||||
description = "Resolver Registry for the descro escrow protocol"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "descro_ext_resolvers"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
cpi = ["no-entrypoint"]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
no-log-ix-name = []
|
||||
idl-build = ["anchor-lang/idl-build"]
|
||||
anchor-debug = []
|
||||
custom-heap = []
|
||||
custom-panic = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "1.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
litesvm = "0.10.0"
|
||||
solana-message = "3.0.1"
|
||||
solana-transaction = "3.0.2"
|
||||
solana-signer = "3.0.0"
|
||||
solana-keypair = "3.0.1"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
||||
13
programs/descro_ext_resolvers/src/error.rs
Normal file
13
programs/descro_ext_resolvers/src/error.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[error_code]
|
||||
pub enum RegistryError {
|
||||
#[msg("Signer is not the registered authority")]
|
||||
Unauthorized,
|
||||
#[msg("Caller is not the authorized escrow program")]
|
||||
UnauthorizedCaller,
|
||||
#[msg("Fee basis points must be <= 10000")]
|
||||
InvalidFeeBps,
|
||||
#[msg("Name must not be empty")]
|
||||
EmptyName,
|
||||
}
|
||||
7
programs/descro_ext_resolvers/src/instructions.rs
Normal file
7
programs/descro_ext_resolvers/src/instructions.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod register;
|
||||
pub mod update;
|
||||
pub mod update_stats;
|
||||
|
||||
pub use register::*;
|
||||
pub use update::*;
|
||||
pub use update_stats::*;
|
||||
47
programs/descro_ext_resolvers/src/instructions/register.rs
Normal file
47
programs/descro_ext_resolvers/src/instructions/register.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{ResolverEntry, ResolverType};
|
||||
use crate::error::RegistryError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct RegisterResolver<'info> {
|
||||
#[account(mut)]
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = authority,
|
||||
space = 8 + ResolverEntry::INIT_SPACE,
|
||||
seeds = [b"resolver", authority.key().as_ref()],
|
||||
bump
|
||||
)]
|
||||
pub resolver_entry: Account<'info, ResolverEntry>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<RegisterResolver>,
|
||||
resolver_type: ResolverType,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
fee_recipient: Pubkey,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
require!(!name.is_empty(), RegistryError::EmptyName);
|
||||
require!(fee_bps <= 10_000, RegistryError::InvalidFeeBps);
|
||||
|
||||
let entry = &mut ctx.accounts.resolver_entry;
|
||||
entry.authority = ctx.accounts.authority.key();
|
||||
entry.resolver_type = resolver_type;
|
||||
entry.name = name;
|
||||
entry.description = description;
|
||||
entry.fee_bps = fee_bps;
|
||||
entry.fee_recipient = fee_recipient;
|
||||
entry.metadata_uri = metadata_uri;
|
||||
entry.total_resolved = 0;
|
||||
entry.ruled_for_buyer = 0;
|
||||
entry.ruled_for_seller = 0;
|
||||
entry.registered_at = Clock::get()?.unix_timestamp;
|
||||
Ok(())
|
||||
}
|
||||
34
programs/descro_ext_resolvers/src/instructions/update.rs
Normal file
34
programs/descro_ext_resolvers/src/instructions/update.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::ResolverEntry;
|
||||
use crate::error::RegistryError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct UpdateResolver<'info> {
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"resolver", authority.key().as_ref()],
|
||||
bump,
|
||||
constraint = resolver_entry.authority == authority.key() @ RegistryError::Unauthorized,
|
||||
)]
|
||||
pub resolver_entry: Account<'info, ResolverEntry>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<UpdateResolver>,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
require!(!name.is_empty(), RegistryError::EmptyName);
|
||||
require!(fee_bps <= 10_000, RegistryError::InvalidFeeBps);
|
||||
|
||||
let entry = &mut ctx.accounts.resolver_entry;
|
||||
entry.name = name;
|
||||
entry.description = description;
|
||||
entry.fee_bps = fee_bps;
|
||||
entry.metadata_uri = metadata_uri;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{ResolverEntry, Ruling};
|
||||
use crate::error::RegistryError;
|
||||
use crate::ESCROW_PROGRAM_ID;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct UpdateStats<'info> {
|
||||
/// Must be the Escrow Program's authority PDA — only it can sign this.
|
||||
#[account(
|
||||
seeds = [b"escrow_authority"],
|
||||
bump,
|
||||
seeds::program = ESCROW_PROGRAM_ID,
|
||||
)]
|
||||
pub escrow_authority: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub resolver_entry: Account<'info, ResolverEntry>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<UpdateStats>, ruling: Ruling) -> Result<()> {
|
||||
// Verify the signer is truly derived from the escrow program (constraint handles this,
|
||||
// but log an explicit error if somehow reached with a wrong caller)
|
||||
let escrow_pda = Pubkey::find_program_address(&[b"escrow_authority"], &ESCROW_PROGRAM_ID).0;
|
||||
require!(
|
||||
ctx.accounts.escrow_authority.key() == escrow_pda,
|
||||
RegistryError::UnauthorizedCaller
|
||||
);
|
||||
|
||||
let entry = &mut ctx.accounts.resolver_entry;
|
||||
entry.total_resolved = entry.total_resolved.saturating_add(1);
|
||||
match ruling {
|
||||
Ruling::Buyer => entry.ruled_for_buyer = entry.ruled_for_buyer.saturating_add(1),
|
||||
Ruling::Seller => entry.ruled_for_seller = entry.ruled_for_seller.saturating_add(1),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
47
programs/descro_ext_resolvers/src/lib.rs
Normal file
47
programs/descro_ext_resolvers/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
pub mod error;
|
||||
pub mod instructions;
|
||||
pub mod state;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
pub use error::*;
|
||||
pub use instructions::*;
|
||||
pub use state::*;
|
||||
|
||||
// Replace with actual program ID after first deployment
|
||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||
|
||||
/// Escrow Program ID — only its PDA may call update_stats.
|
||||
pub const ESCROW_PROGRAM_ID: Pubkey =
|
||||
pubkey!("DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3");
|
||||
|
||||
#[program]
|
||||
pub mod descro_ext_resolvers {
|
||||
use super::*;
|
||||
|
||||
pub fn register_resolver(
|
||||
ctx: Context<RegisterResolver>,
|
||||
resolver_type: ResolverType,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
fee_recipient: Pubkey,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
register::handler(ctx, resolver_type, name, description, fee_bps, fee_recipient, metadata_uri)
|
||||
}
|
||||
|
||||
pub fn update_resolver(
|
||||
ctx: Context<UpdateResolver>,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
update::handler(ctx, name, description, fee_bps, metadata_uri)
|
||||
}
|
||||
|
||||
pub fn update_stats(ctx: Context<UpdateStats>, ruling: Ruling) -> Result<()> {
|
||||
update_stats::handler(ctx, ruling)
|
||||
}
|
||||
}
|
||||
36
programs/descro_ext_resolvers/src/state.rs
Normal file
36
programs/descro_ext_resolvers/src/state.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct ResolverEntry {
|
||||
pub authority: Pubkey,
|
||||
pub resolver_type: ResolverType,
|
||||
#[max_len(64)]
|
||||
pub name: String,
|
||||
#[max_len(256)]
|
||||
pub description: String,
|
||||
pub fee_bps: u16,
|
||||
pub fee_recipient: Pubkey,
|
||||
#[max_len(256)]
|
||||
pub metadata_uri: String,
|
||||
pub total_resolved: u64,
|
||||
pub ruled_for_buyer: u64,
|
||||
pub ruled_for_seller: u64,
|
||||
pub registered_at: i64,
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
|
||||
pub enum ResolverType {
|
||||
CentralAuthority,
|
||||
JuryDAO,
|
||||
MAD,
|
||||
Algorithmic,
|
||||
Multisig,
|
||||
}
|
||||
|
||||
/// Passed to update_stats; mirrors the Escrow program's Winner enum.
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
|
||||
pub enum Ruling {
|
||||
Buyer,
|
||||
Seller,
|
||||
}
|
||||
108
programs/descro_ext_resolvers/tests/common/mod.rs
Normal file
108
programs/descro_ext_resolvers/tests/common/mod.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use {
|
||||
anchor_lang::{
|
||||
solana_program::{instruction::Instruction, system_program},
|
||||
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||
},
|
||||
descro_ext_resolvers::{ResolverEntry, ResolverType},
|
||||
litesvm::LiteSVM,
|
||||
solana_keypair::Keypair,
|
||||
solana_message::{Message, VersionedMessage},
|
||||
solana_signer::Signer,
|
||||
solana_transaction::versioned::VersionedTransaction,
|
||||
};
|
||||
pub use anchor_lang::prelude::Pubkey;
|
||||
|
||||
pub fn setup() -> (LiteSVM, Keypair) {
|
||||
let program_id = descro_ext_resolvers::id();
|
||||
let mut svm = LiteSVM::new();
|
||||
let bytes = include_bytes!("../../../../target/deploy/descro_ext_resolvers.so");
|
||||
svm.add_program(program_id, bytes).unwrap();
|
||||
|
||||
let authority = Keypair::new();
|
||||
svm.airdrop(&authority.pubkey(), 10_000_000_000).unwrap();
|
||||
|
||||
(svm, authority)
|
||||
}
|
||||
|
||||
pub fn resolver_entry_pda(authority: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[b"resolver", authority.as_ref()],
|
||||
&descro_ext_resolvers::id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) {
|
||||
let blockhash = svm.latest_blockhash();
|
||||
let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
|
||||
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
|
||||
svm.send_transaction(tx).expect("transaction failed");
|
||||
}
|
||||
|
||||
pub fn try_send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) -> bool {
|
||||
let blockhash = svm.latest_blockhash();
|
||||
let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
|
||||
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
|
||||
svm.send_transaction(tx).is_ok()
|
||||
}
|
||||
|
||||
pub fn ix_register(
|
||||
authority: &Pubkey,
|
||||
resolver_type: ResolverType,
|
||||
name: &str,
|
||||
description: &str,
|
||||
fee_bps: u16,
|
||||
fee_recipient: Pubkey,
|
||||
metadata_uri: &str,
|
||||
) -> Instruction {
|
||||
let entry = resolver_entry_pda(authority);
|
||||
Instruction::new_with_bytes(
|
||||
descro_ext_resolvers::id(),
|
||||
&descro_ext_resolvers::instruction::RegisterResolver {
|
||||
resolver_type,
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
fee_bps,
|
||||
fee_recipient,
|
||||
metadata_uri: metadata_uri.to_string(),
|
||||
}
|
||||
.data(),
|
||||
descro_ext_resolvers::accounts::RegisterResolver {
|
||||
authority: *authority,
|
||||
resolver_entry: entry,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_update(
|
||||
authority: &Pubkey,
|
||||
name: &str,
|
||||
description: &str,
|
||||
fee_bps: u16,
|
||||
metadata_uri: &str,
|
||||
) -> Instruction {
|
||||
let entry = resolver_entry_pda(authority);
|
||||
Instruction::new_with_bytes(
|
||||
descro_ext_resolvers::id(),
|
||||
&descro_ext_resolvers::instruction::UpdateResolver {
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
fee_bps,
|
||||
metadata_uri: metadata_uri.to_string(),
|
||||
}
|
||||
.data(),
|
||||
descro_ext_resolvers::accounts::UpdateResolver {
|
||||
authority: *authority,
|
||||
resolver_entry: entry,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn read_entry(svm: &LiteSVM, authority: &Pubkey) -> ResolverEntry {
|
||||
let pda = resolver_entry_pda(authority);
|
||||
let account = svm.get_account(&pda).expect("resolver entry not found");
|
||||
ResolverEntry::try_deserialize(&mut account.data.as_slice()).unwrap()
|
||||
}
|
||||
127
programs/descro_ext_resolvers/tests/test_register.rs
Normal file
127
programs/descro_ext_resolvers/tests/test_register.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use descro_ext_resolvers::ResolverType;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
#[test]
|
||||
fn registers_entry_with_correct_state() {
|
||||
let (mut svm, authority) = setup();
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"Acme Resolvers",
|
||||
"Fast and fair dispute resolution",
|
||||
100, // 1%
|
||||
authority.pubkey(),
|
||||
"ipfs://QmTest",
|
||||
),
|
||||
&authority,
|
||||
);
|
||||
|
||||
let entry = read_entry(&svm, &authority.pubkey());
|
||||
assert_eq!(entry.authority, authority.pubkey());
|
||||
assert_eq!(entry.resolver_type, ResolverType::CentralAuthority);
|
||||
assert_eq!(entry.name, "Acme Resolvers");
|
||||
assert_eq!(entry.fee_bps, 100);
|
||||
assert_eq!(entry.total_resolved, 0);
|
||||
assert_eq!(entry.ruled_for_buyer, 0);
|
||||
assert_eq!(entry.ruled_for_seller, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_register_fails() {
|
||||
let (mut svm, authority) = setup();
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"Acme",
|
||||
"",
|
||||
0,
|
||||
authority.pubkey(),
|
||||
"",
|
||||
),
|
||||
&authority,
|
||||
);
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"Acme Again",
|
||||
"",
|
||||
0,
|
||||
authority.pubkey(),
|
||||
"",
|
||||
),
|
||||
&authority,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_empty_name_fails() {
|
||||
let (mut svm, authority) = setup();
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
authority.pubkey(),
|
||||
"",
|
||||
),
|
||||
&authority,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_invalid_fee_bps_fails() {
|
||||
let (mut svm, authority) = setup();
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"Acme",
|
||||
"",
|
||||
10_001, // > 100%
|
||||
authority.pubkey(),
|
||||
"",
|
||||
),
|
||||
&authority,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_authorities_have_separate_entries() {
|
||||
let (mut svm, auth1) = setup();
|
||||
let auth2 = Keypair::new();
|
||||
svm.airdrop(&auth2.pubkey(), 10_000_000_000).unwrap();
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_register(&auth1.pubkey(), ResolverType::CentralAuthority, "Auth1", "", 0, auth1.pubkey(), ""),
|
||||
&auth1,
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
ix_register(&auth2.pubkey(), ResolverType::JuryDAO, "Auth2", "", 50, auth2.pubkey(), ""),
|
||||
&auth2,
|
||||
);
|
||||
|
||||
let e1 = read_entry(&svm, &auth1.pubkey());
|
||||
let e2 = read_entry(&svm, &auth2.pubkey());
|
||||
assert_eq!(e1.name, "Auth1");
|
||||
assert_eq!(e2.name, "Auth2");
|
||||
assert_ne!(resolver_entry_pda(&auth1.pubkey()), resolver_entry_pda(&auth2.pubkey()));
|
||||
}
|
||||
83
programs/descro_ext_resolvers/tests/test_update.rs
Normal file
83
programs/descro_ext_resolvers/tests/test_update.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use descro_ext_resolvers::ResolverType;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
fn register_default(svm: &mut litesvm::LiteSVM, authority: &Keypair) {
|
||||
send(
|
||||
svm,
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
"Original Name",
|
||||
"Original description",
|
||||
100,
|
||||
authority.pubkey(),
|
||||
"ipfs://original",
|
||||
),
|
||||
authority,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_changes_metadata() {
|
||||
let (mut svm, authority) = setup();
|
||||
register_default(&mut svm, &authority);
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
ix_update(&authority.pubkey(), "New Name", "New description", 200, "ipfs://new"),
|
||||
&authority,
|
||||
);
|
||||
|
||||
let entry = read_entry(&svm, &authority.pubkey());
|
||||
assert_eq!(entry.name, "New Name");
|
||||
assert_eq!(entry.description, "New description");
|
||||
assert_eq!(entry.fee_bps, 200);
|
||||
assert_eq!(entry.metadata_uri, "ipfs://new");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_different_authority_cannot_modify_entry() {
|
||||
let (mut svm, authority) = setup();
|
||||
let other = Keypair::new();
|
||||
svm.airdrop(&other.pubkey(), 1_000_000_000).unwrap();
|
||||
register_default(&mut svm, &authority);
|
||||
|
||||
// `other` signs but claims to BE the authority → seeds derive other's (non-existent) PDA
|
||||
// → account has no data → Anchor rejects the uninitialized account
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_update(&other.pubkey(), "Hacked", "", 0, ""),
|
||||
&other,
|
||||
));
|
||||
|
||||
// `authority`'s entry is unchanged
|
||||
let entry = read_entry(&svm, &authority.pubkey());
|
||||
assert_eq!(entry.name, "Original Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_empty_name_fails() {
|
||||
let (mut svm, authority) = setup();
|
||||
register_default(&mut svm, &authority);
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_update(&authority.pubkey(), "", "", 0, ""),
|
||||
&authority,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_invalid_fee_bps_fails() {
|
||||
let (mut svm, authority) = setup();
|
||||
register_default(&mut svm, &authority);
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_update(&authority.pubkey(), "Name", "", 10_001, ""),
|
||||
&authority,
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user