resolver picker

This commit is contained in:
thesn10
2026-06-27 14:52:11 +02:00
parent 0c86751d51
commit e799c0be9d
10 changed files with 155 additions and 41 deletions

View File

@@ -6,6 +6,7 @@ import { useWallet, useKitTransactionSigner } from '@solana/connector/react'
import { ListingDetail } from '@/components/ListingDetail' import { ListingDetail } from '@/components/ListingDetail'
import { useTx } from '@/hooks/useTx' import { useTx } from '@/hooks/useTx'
import { useListing } from '@/hooks/useListing' import { useListing } from '@/hooks/useListing'
import { useResolvers } from '@/hooks/useResolvers'
import { import {
getCloseListingInstruction, getCloseListingInstruction,
getCreateOrderInstructionAsync, getCreateOrderInstructionAsync,
@@ -13,6 +14,8 @@ import {
deriveEscrowId, deriveEscrowId,
} from '@solisting/sdk' } from '@solisting/sdk'
import { DESCRO_PROGRAM_ADDRESS } from '@descro/sdk' import { DESCRO_PROGRAM_ADDRESS } from '@descro/sdk'
import { ResolverType } from '@descro/sdk'
import { Button } from '@/components/ui/Button'
import type { Address } from '@solisting/sdk' import type { Address } from '@solisting/sdk'
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'> const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>
@@ -24,7 +27,10 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
const { signer } = useKitTransactionSigner() const { signer } = useKitTransactionSigner()
const sendTx = useTx() const sendTx = useTx()
const { data: listing } = useListing(pk as Address) const { data: listing } = useListing(pk as Address)
const { data: resolvers = [] } = useResolvers()
const [orderLoading, setOrderLoading] = useState(false) const [orderLoading, setOrderLoading] = useState(false)
const [showResolverPicker, setShowResolverPicker] = useState(false)
const [selectedResolver, setSelectedResolver] = useState<Address | null>(null)
async function handleClose() { async function handleClose() {
if (!signer) return if (!signer) return
@@ -33,8 +39,14 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
router.push('/listings') router.push('/listings')
} }
async function handlePlaceOrder() { function openPlaceOrder() {
setSelectedResolver(null)
setShowResolverPicker(true)
}
async function handleConfirmOrder() {
if (!signer || !account || !listing) return if (!signer || !account || !listing) return
setShowResolverPicker(false)
setOrderLoading(true) setOrderLoading(true)
try { try {
const orderId = BigInt(Date.now()) const orderId = BigInt(Date.now())
@@ -50,7 +62,7 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
descroProgram: DESCRO_PROGRAM_ADDRESS, descroProgram: DESCRO_PROGRAM_ADDRESS,
orderId, orderId,
escrowId, escrowId,
resolver: account as Address, resolver: selectedResolver,
paymentCurrency: { __kind: 'Sol' }, paymentCurrency: { __kind: 'Sol' },
expectedAmount: listing.data.price, expectedAmount: listing.data.price,
maxSlippageBps: 0, maxSlippageBps: 0,
@@ -67,13 +79,101 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
} }
} }
const acceptedResolvers = listing?.data.acceptedResolvers ?? []
const visibleResolvers = acceptedResolvers.length > 0
? resolvers.filter((r) => acceptedResolvers.includes(r.pda as Address))
: resolvers
const mustSelectResolver = acceptedResolvers.length > 0
const confirmDisabled = mustSelectResolver && !selectedResolver
return ( return (
<ListingDetail <>
pk={pk as Address} <ListingDetail
walletAddress={account ?? null} pk={pk as Address}
onUpdate={() => router.push(`/listing/create?edit=${pk}`)} walletAddress={account ?? null}
onClose={handleClose} onUpdate={() => router.push(`/listing/create?edit=${pk}`)}
onPlaceOrder={orderLoading ? undefined : handlePlaceOrder} onClose={handleClose}
/> onPlaceOrder={orderLoading ? undefined : openPlaceOrder}
/>
{showResolverPicker && (
<div
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}
onClick={(e) => { if (e.target === e.currentTarget) setShowResolverPicker(false) }}
>
<div style={{ background: 'var(--bg)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '28px 28px 24px', width: 440, maxWidth: '90vw', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 12 }}>PLACE ORDER</div>
<h2 style={{ margin: '0 0 8px', fontSize: 19, fontWeight: 700 }}>Select Resolver</h2>
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--mut)', lineHeight: 1.55 }}>
{mustSelectResolver
? 'This listing requires one of the following resolvers for dispute resolution.'
: 'Choose a resolver for dispute resolution, or proceed without one.'}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 22 }}>
{!mustSelectResolver && (
<ResolverOption
selected={selectedResolver === null}
onClick={() => setSelectedResolver(null)}
label="No resolver"
sublabel="Disputes cannot be resolved on-chain"
/>
)}
{visibleResolvers.map((r) => (
<ResolverOption
key={r.pda}
selected={selectedResolver === r.pda}
onClick={() => setSelectedResolver(r.pda as Address)}
label={`${r.pda.slice(0, 8)}${r.pda.slice(-6)}`}
sublabel={ResolverType[r.data.resolverType as number] as string}
mono
/>
))}
{visibleResolvers.length === 0 && mustSelectResolver && (
<div style={{ fontSize: 13, color: 'var(--mut)', padding: '12px 0' }}>
No registered resolvers found for this listing.
</div>
)}
</div>
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
<Button variant="ghost" onClick={() => setShowResolverPicker(false)}>Cancel</Button>
<Button onClick={handleConfirmOrder} disabled={confirmDisabled}>Place Order</Button>
</div>
</div>
</div>
)}
</>
)
}
function ResolverOption({ selected, onClick, label, sublabel, mono }: {
selected: boolean
onClick: () => void
label: string
sublabel?: string
mono?: boolean
}) {
return (
<button
onClick={onClick}
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
padding: '11px 14px', borderRadius: 9, cursor: 'pointer', textAlign: 'left',
background: selected ? 'var(--accSoft)' : 'var(--bg3)',
border: `1px solid ${selected ? 'var(--accBd)' : 'var(--bd)'}`,
color: 'var(--tx)',
transition: 'background .12s, border-color .12s',
}}
>
<span style={{ fontFamily: mono ? 'var(--font-mono)' : undefined, fontSize: mono ? 12.5 : 13, fontWeight: selected ? 600 : mono ? 400 : 600 }}>
{label}
</span>
{sublabel && (
<span style={{ fontSize: 11.5, color: 'var(--mut)', flexShrink: 0 }}>
{sublabel}
</span>
)}
</button>
) )
} }

View File

@@ -4,7 +4,7 @@ use crate::state::{Currency, ListingAccount, OrderAccount};
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
#[derive(Accounts)] #[derive(Accounts)]
#[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: Currency)] #[instruction(order_id: u64, escrow_id: u64, resolver: Option<Pubkey>, payment_currency: Currency)]
pub struct CreateOrder<'info> { pub struct CreateOrder<'info> {
#[account(mut)] #[account(mut)]
pub buyer: Signer<'info>, pub buyer: Signer<'info>,
@@ -67,7 +67,7 @@ pub fn handler(
ctx: Context<CreateOrder>, ctx: Context<CreateOrder>,
order_id: u64, order_id: u64,
escrow_id: u64, escrow_id: u64,
resolver: Pubkey, resolver: Option<Pubkey>,
payment_currency: Currency, payment_currency: Currency,
expected_amount: u64, expected_amount: u64,
max_slippage_bps: u16, max_slippage_bps: u16,
@@ -80,10 +80,13 @@ pub fn handler(
let listing = &ctx.accounts.listing_account; let listing = &ctx.accounts.listing_account;
if !listing.accepted_resolvers.is_empty() { if !listing.accepted_resolvers.is_empty() {
require!( match resolver {
listing.accepted_resolvers.contains(&resolver), Some(r) => require!(
SolistingError::ResolverNotAccepted listing.accepted_resolvers.contains(&r),
); SolistingError::ResolverNotAccepted
),
None => return err!(SolistingError::ResolverNotAccepted),
}
} }
let amount = if payment_currency == listing.canonical_currency { let amount = if payment_currency == listing.canonical_currency {
@@ -143,7 +146,7 @@ pub fn handler(
}, },
), ),
amount, amount,
Some(resolver), resolver,
escrow_id, escrow_id,
)?; )?;

View File

@@ -70,7 +70,7 @@ pub mod solisting {
ctx: Context<CreateOrder>, ctx: Context<CreateOrder>,
order_id: u64, order_id: u64,
escrow_id: u64, escrow_id: u64,
resolver: Pubkey, resolver: Option<Pubkey>,
payment_currency: state::Currency, payment_currency: state::Currency,
expected_amount: u64, expected_amount: u64,
max_slippage_bps: u16, max_slippage_bps: u16,

View File

@@ -69,7 +69,7 @@ pub struct OrderAccount {
pub listing: Pubkey, pub listing: Pubkey,
pub buyer: Pubkey, pub buyer: Pubkey,
pub seller: Pubkey, pub seller: Pubkey,
pub resolver: Pubkey, pub resolver: Option<Pubkey>,
/// The currency the buyer chose to pay in. /// The currency the buyer chose to pay in.
pub payment_currency: Currency, pub payment_currency: Currency,
/// Actual amount paid (may differ from price when oracle-converted). /// Actual amount paid (may differ from price when oracle-converted).

View File

@@ -195,7 +195,6 @@ pub fn ix_create_order(
canonical_oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>, target_oracle: Option<Pubkey>,
) -> Instruction { ) -> Instruction {
let resolver = Pubkey::new_unique();
ix_create_order_with_resolver( ix_create_order_with_resolver(
buyer, buyer,
seller, seller,
@@ -205,7 +204,7 @@ pub fn ix_create_order(
max_slippage_bps, max_slippage_bps,
canonical_oracle, canonical_oracle,
target_oracle, target_oracle,
resolver, Some(Pubkey::new_unique()),
) )
} }
@@ -219,7 +218,7 @@ pub fn ix_create_order_with_resolver(
max_slippage_bps: u16, max_slippage_bps: u16,
canonical_oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>, target_oracle: Option<Pubkey>,
resolver: Pubkey, resolver: Option<Pubkey>,
) -> Instruction { ) -> Instruction {
let listing = listing_pda(seller, listing_id); let listing = listing_pda(seller, listing_id);
let order = order_pda(listing, *buyer, order_id); let order = order_pda(listing, *buyer, order_id);
@@ -264,7 +263,7 @@ pub fn ix_accept_order(
buyer: &Pubkey, buyer: &Pubkey,
order_id: u64, order_id: u64,
escrow_id: u64, escrow_id: u64,
resolver: Pubkey, resolver: Option<Pubkey>,
) -> Instruction { ) -> Instruction {
let listing_account = listing_pda(seller, listing_id); let listing_account = listing_pda(seller, listing_id);
let order_account = order_pda(listing_account, *buyer, order_id); let order_account = order_pda(listing_account, *buyer, order_id);
@@ -276,7 +275,7 @@ pub fn ix_accept_order(
&solisting::instruction::AcceptOrder {}.data(), &solisting::instruction::AcceptOrder {}.data(),
solisting::accounts::AcceptOrder { solisting::accounts::AcceptOrder {
seller: *seller, seller: *seller,
resolver, resolver: resolver.unwrap_or(system_program::ID),
listing_account, listing_account,
order_account, order_account,
escrow_account, escrow_account,

View File

@@ -118,7 +118,7 @@ fn create_order_fails_if_resolver_not_accepted() {
&mut svm, &mut svm,
&[ix_create_order_with_resolver( &[ix_create_order_with_resolver(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1, &buyer.pubkey(), &seller.pubkey(), listing_id, 1,
Currency::Sol, 0, None, None, Pubkey::new_unique(), Currency::Sol, 0, None, None, Some(Pubkey::new_unique()),
)], )],
&[&buyer], &[&buyer],
); );
@@ -152,7 +152,7 @@ fn seller_accept_creates_active_descro_escrow() {
); );
let order_key = order_pda(listing_key, buyer.pubkey(), order_id); let order_key = order_pda(listing_key, buyer.pubkey(), order_id);
assert!(svm.get_account(&order_key).is_none()); assert!(svm.get_account(&order_key).is_some());
let listing = read_listing(&svm, &seller.pubkey(), listing_id); let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.quantity_reserved, 0); assert_eq!(listing.quantity_reserved, 0);

View File

@@ -21,6 +21,8 @@ import {
getBytesEncoder, getBytesEncoder,
getI64Decoder, getI64Decoder,
getI64Encoder, getI64Encoder,
getOptionDecoder,
getOptionEncoder,
getStructDecoder, getStructDecoder,
getStructEncoder, getStructEncoder,
getU64Decoder, getU64Decoder,
@@ -38,6 +40,8 @@ import {
type FetchAccountsConfig, type FetchAccountsConfig,
type MaybeAccount, type MaybeAccount,
type MaybeEncodedAccount, type MaybeEncodedAccount,
type Option,
type OptionOrNullable,
type ReadonlyUint8Array, type ReadonlyUint8Array,
} from "@solana/kit"; } from "@solana/kit";
import { import {
@@ -61,7 +65,7 @@ export type SolistingStateOrderAccount = {
listing: Address; listing: Address;
buyer: Address; buyer: Address;
seller: Address; seller: Address;
resolver: Address; resolver: Option<Address>;
/** The currency the buyer chose to pay in. */ /** The currency the buyer chose to pay in. */
paymentCurrency: SolistingStateCurrency; paymentCurrency: SolistingStateCurrency;
/** Actual amount paid (may differ from price when oracle-converted). */ /** Actual amount paid (may differ from price when oracle-converted). */
@@ -79,7 +83,7 @@ export type SolistingStateOrderAccountArgs = {
listing: Address; listing: Address;
buyer: Address; buyer: Address;
seller: Address; seller: Address;
resolver: Address; resolver: OptionOrNullable<Address>;
/** The currency the buyer chose to pay in. */ /** The currency the buyer chose to pay in. */
paymentCurrency: SolistingStateCurrencyArgs; paymentCurrency: SolistingStateCurrencyArgs;
/** Actual amount paid (may differ from price when oracle-converted). */ /** Actual amount paid (may differ from price when oracle-converted). */
@@ -101,7 +105,7 @@ export function getSolistingStateOrderAccountEncoder(): Encoder<SolistingStateOr
["listing", getAddressEncoder()], ["listing", getAddressEncoder()],
["buyer", getAddressEncoder()], ["buyer", getAddressEncoder()],
["seller", getAddressEncoder()], ["seller", getAddressEncoder()],
["resolver", getAddressEncoder()], ["resolver", getOptionEncoder(getAddressEncoder())],
["paymentCurrency", getSolistingStateCurrencyEncoder()], ["paymentCurrency", getSolistingStateCurrencyEncoder()],
["amount", getU64Encoder()], ["amount", getU64Encoder()],
["escrowAccount", getAddressEncoder()], ["escrowAccount", getAddressEncoder()],
@@ -124,7 +128,7 @@ export function getSolistingStateOrderAccountDecoder(): Decoder<SolistingStateOr
["listing", getAddressDecoder()], ["listing", getAddressDecoder()],
["buyer", getAddressDecoder()], ["buyer", getAddressDecoder()],
["seller", getAddressDecoder()], ["seller", getAddressDecoder()],
["resolver", getAddressDecoder()], ["resolver", getOptionDecoder(getAddressDecoder())],
["paymentCurrency", getSolistingStateCurrencyDecoder()], ["paymentCurrency", getSolistingStateCurrencyDecoder()],
["amount", getU64Decoder()], ["amount", getU64Decoder()],
["escrowAccount", getAddressDecoder()], ["escrowAccount", getAddressDecoder()],

View File

@@ -14,6 +14,8 @@ import {
getAddressEncoder, getAddressEncoder,
getBytesDecoder, getBytesDecoder,
getBytesEncoder, getBytesEncoder,
getOptionDecoder,
getOptionEncoder,
getProgramDerivedAddress, getProgramDerivedAddress,
getStructDecoder, getStructDecoder,
getStructEncoder, getStructEncoder,
@@ -33,6 +35,8 @@ import {
type Instruction, type Instruction,
type InstructionWithAccounts, type InstructionWithAccounts,
type InstructionWithData, type InstructionWithData,
type Option,
type OptionOrNullable,
type ReadonlyAccount, type ReadonlyAccount,
type ReadonlyUint8Array, type ReadonlyUint8Array,
type TransactionSigner, type TransactionSigner,
@@ -121,7 +125,7 @@ export type CreateOrderInstructionData = {
discriminator: ReadonlyUint8Array; discriminator: ReadonlyUint8Array;
orderId: bigint; orderId: bigint;
escrowId: bigint; escrowId: bigint;
resolver: Address; resolver: Option<Address>;
paymentCurrency: SolistingStateCurrency; paymentCurrency: SolistingStateCurrency;
expectedAmount: bigint; expectedAmount: bigint;
maxSlippageBps: number; maxSlippageBps: number;
@@ -130,7 +134,7 @@ export type CreateOrderInstructionData = {
export type CreateOrderInstructionDataArgs = { export type CreateOrderInstructionDataArgs = {
orderId: number | bigint; orderId: number | bigint;
escrowId: number | bigint; escrowId: number | bigint;
resolver: Address; resolver: OptionOrNullable<Address>;
paymentCurrency: SolistingStateCurrencyArgs; paymentCurrency: SolistingStateCurrencyArgs;
expectedAmount: number | bigint; expectedAmount: number | bigint;
maxSlippageBps: number; maxSlippageBps: number;
@@ -142,7 +146,7 @@ export function getCreateOrderInstructionDataEncoder(): Encoder<CreateOrderInstr
["discriminator", fixEncoderSize(getBytesEncoder(), 8)], ["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
["orderId", getU64Encoder()], ["orderId", getU64Encoder()],
["escrowId", getU64Encoder()], ["escrowId", getU64Encoder()],
["resolver", getAddressEncoder()], ["resolver", getOptionEncoder(getAddressEncoder())],
["paymentCurrency", getSolistingStateCurrencyEncoder()], ["paymentCurrency", getSolistingStateCurrencyEncoder()],
["expectedAmount", getU64Encoder()], ["expectedAmount", getU64Encoder()],
["maxSlippageBps", getU16Encoder()], ["maxSlippageBps", getU16Encoder()],
@@ -156,7 +160,7 @@ export function getCreateOrderInstructionDataDecoder(): Decoder<CreateOrderInstr
["discriminator", fixDecoderSize(getBytesDecoder(), 8)], ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
["orderId", getU64Decoder()], ["orderId", getU64Decoder()],
["escrowId", getU64Decoder()], ["escrowId", getU64Decoder()],
["resolver", getAddressDecoder()], ["resolver", getOptionDecoder(getAddressDecoder())],
["paymentCurrency", getSolistingStateCurrencyDecoder()], ["paymentCurrency", getSolistingStateCurrencyDecoder()],
["expectedAmount", getU64Decoder()], ["expectedAmount", getU64Decoder()],
["maxSlippageBps", getU16Decoder()], ["maxSlippageBps", getU16Decoder()],

View File

@@ -21,15 +21,15 @@ type GetRpc = Rpc<GetAccountInfoApi>
// 8: listing (32) // 8: listing (32)
// 40: buyer (32) // 40: buyer (32)
// 72: seller (32) // 72: seller (32)
// 104: resolver (32) // 104: resolver Option<Pubkey> = 1 byte tag + 32 bytes = 33 bytes
// 136: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total // 137: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total
// 137: amount (8) // 138: amount (8)
// 145: escrowAccount (32) // 146: escrowAccount (32)
const LISTING_OFFSET = 8n const LISTING_OFFSET = 8n
const BUYER_OFFSET = 40n const BUYER_OFFSET = 40n
// escrowAccount offset is only valid for Sol-currency orders. // escrowAccount offset is only valid for Sol-currency orders.
// SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 178. // SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 179.
const ESCROW_ACCOUNT_OFFSET = 145n const ESCROW_ACCOUNT_OFFSET = 146n
type RawGpaResult = Array<{ type RawGpaResult = Array<{
pubkey: Address pubkey: Address

View File

@@ -835,7 +835,9 @@
}, },
{ {
"name": "resolver", "name": "resolver",
"type": "pubkey" "type": {
"option": "pubkey"
}
}, },
{ {
"name": "payment_currency", "name": "payment_currency",
@@ -1461,7 +1463,9 @@
}, },
{ {
"name": "resolver", "name": "resolver",
"type": "pubkey" "type": {
"option": "pubkey"
}
}, },
{ {
"name": "payment_currency", "name": "payment_currency",