diff --git a/app/src/app/listing/[pk]/page.tsx b/app/src/app/listing/[pk]/page.tsx
index 114a496..a42b229 100644
--- a/app/src/app/listing/[pk]/page.tsx
+++ b/app/src/app/listing/[pk]/page.tsx
@@ -6,6 +6,7 @@ import { useWallet, useKitTransactionSigner } from '@solana/connector/react'
import { ListingDetail } from '@/components/ListingDetail'
import { useTx } from '@/hooks/useTx'
import { useListing } from '@/hooks/useListing'
+import { useResolvers } from '@/hooks/useResolvers'
import {
getCloseListingInstruction,
getCreateOrderInstructionAsync,
@@ -13,6 +14,8 @@ import {
deriveEscrowId,
} from '@solisting/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'
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>
@@ -24,7 +27,10 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
const { signer } = useKitTransactionSigner()
const sendTx = useTx()
const { data: listing } = useListing(pk as Address)
+ const { data: resolvers = [] } = useResolvers()
const [orderLoading, setOrderLoading] = useState(false)
+ const [showResolverPicker, setShowResolverPicker] = useState(false)
+ const [selectedResolver, setSelectedResolver] = useState
(null)
async function handleClose() {
if (!signer) return
@@ -33,8 +39,14 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
router.push('/listings')
}
- async function handlePlaceOrder() {
+ function openPlaceOrder() {
+ setSelectedResolver(null)
+ setShowResolverPicker(true)
+ }
+
+ async function handleConfirmOrder() {
if (!signer || !account || !listing) return
+ setShowResolverPicker(false)
setOrderLoading(true)
try {
const orderId = BigInt(Date.now())
@@ -50,7 +62,7 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
descroProgram: DESCRO_PROGRAM_ADDRESS,
orderId,
escrowId,
- resolver: account as Address,
+ resolver: selectedResolver,
paymentCurrency: { __kind: 'Sol' },
expectedAmount: listing.data.price,
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 (
- router.push(`/listing/create?edit=${pk}`)}
- onClose={handleClose}
- onPlaceOrder={orderLoading ? undefined : handlePlaceOrder}
- />
+ <>
+ router.push(`/listing/create?edit=${pk}`)}
+ onClose={handleClose}
+ onPlaceOrder={orderLoading ? undefined : openPlaceOrder}
+ />
+
+ {showResolverPicker && (
+ { if (e.target === e.currentTarget) setShowResolverPicker(false) }}
+ >
+
+
PLACE ORDER
+
Select Resolver
+
+ {mustSelectResolver
+ ? 'This listing requires one of the following resolvers for dispute resolution.'
+ : 'Choose a resolver for dispute resolution, or proceed without one.'}
+
+
+
+ {!mustSelectResolver && (
+
setSelectedResolver(null)}
+ label="No resolver"
+ sublabel="Disputes cannot be resolved on-chain"
+ />
+ )}
+ {visibleResolvers.map((r) => (
+ 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 && (
+
+ No registered resolvers found for this listing.
+
+ )}
+
+
+
+ setShowResolverPicker(false)}>Cancel
+ Place Order
+
+
+
+ )}
+ >
+ )
+}
+
+function ResolverOption({ selected, onClick, label, sublabel, mono }: {
+ selected: boolean
+ onClick: () => void
+ label: string
+ sublabel?: string
+ mono?: boolean
+}) {
+ return (
+
+
+ {label}
+
+ {sublabel && (
+
+ {sublabel}
+
+ )}
+
)
}
diff --git a/programs/solisting/src/instructions/create_order.rs b/programs/solisting/src/instructions/create_order.rs
index 61acaad..16e9a2b 100644
--- a/programs/solisting/src/instructions/create_order.rs
+++ b/programs/solisting/src/instructions/create_order.rs
@@ -4,7 +4,7 @@ use crate::state::{Currency, ListingAccount, OrderAccount};
use anchor_lang::prelude::*;
#[derive(Accounts)]
-#[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: Currency)]
+#[instruction(order_id: u64, escrow_id: u64, resolver: Option, payment_currency: Currency)]
pub struct CreateOrder<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
@@ -67,7 +67,7 @@ pub fn handler(
ctx: Context,
order_id: u64,
escrow_id: u64,
- resolver: Pubkey,
+ resolver: Option,
payment_currency: Currency,
expected_amount: u64,
max_slippage_bps: u16,
@@ -80,10 +80,13 @@ pub fn handler(
let listing = &ctx.accounts.listing_account;
if !listing.accepted_resolvers.is_empty() {
- require!(
- listing.accepted_resolvers.contains(&resolver),
- SolistingError::ResolverNotAccepted
- );
+ match resolver {
+ Some(r) => require!(
+ listing.accepted_resolvers.contains(&r),
+ SolistingError::ResolverNotAccepted
+ ),
+ None => return err!(SolistingError::ResolverNotAccepted),
+ }
}
let amount = if payment_currency == listing.canonical_currency {
@@ -143,7 +146,7 @@ pub fn handler(
},
),
amount,
- Some(resolver),
+ resolver,
escrow_id,
)?;
diff --git a/programs/solisting/src/lib.rs b/programs/solisting/src/lib.rs
index 17de565..45863e3 100644
--- a/programs/solisting/src/lib.rs
+++ b/programs/solisting/src/lib.rs
@@ -70,7 +70,7 @@ pub mod solisting {
ctx: Context,
order_id: u64,
escrow_id: u64,
- resolver: Pubkey,
+ resolver: Option,
payment_currency: state::Currency,
expected_amount: u64,
max_slippage_bps: u16,
diff --git a/programs/solisting/src/state.rs b/programs/solisting/src/state.rs
index 47b90af..d7f111e 100644
--- a/programs/solisting/src/state.rs
+++ b/programs/solisting/src/state.rs
@@ -69,7 +69,7 @@ pub struct OrderAccount {
pub listing: Pubkey,
pub buyer: Pubkey,
pub seller: Pubkey,
- pub resolver: Pubkey,
+ pub resolver: Option,
/// The currency the buyer chose to pay in.
pub payment_currency: Currency,
/// Actual amount paid (may differ from price when oracle-converted).
diff --git a/programs/solisting/tests/common/mod.rs b/programs/solisting/tests/common/mod.rs
index 4c4872b..ed8ffd5 100644
--- a/programs/solisting/tests/common/mod.rs
+++ b/programs/solisting/tests/common/mod.rs
@@ -195,7 +195,6 @@ pub fn ix_create_order(
canonical_oracle: Option,
target_oracle: Option,
) -> Instruction {
- let resolver = Pubkey::new_unique();
ix_create_order_with_resolver(
buyer,
seller,
@@ -205,7 +204,7 @@ pub fn ix_create_order(
max_slippage_bps,
canonical_oracle,
target_oracle,
- resolver,
+ Some(Pubkey::new_unique()),
)
}
@@ -219,7 +218,7 @@ pub fn ix_create_order_with_resolver(
max_slippage_bps: u16,
canonical_oracle: Option,
target_oracle: Option,
- resolver: Pubkey,
+ resolver: Option,
) -> Instruction {
let listing = listing_pda(seller, listing_id);
let order = order_pda(listing, *buyer, order_id);
@@ -264,7 +263,7 @@ pub fn ix_accept_order(
buyer: &Pubkey,
order_id: u64,
escrow_id: u64,
- resolver: Pubkey,
+ resolver: Option,
) -> Instruction {
let listing_account = listing_pda(seller, listing_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::accounts::AcceptOrder {
seller: *seller,
- resolver,
+ resolver: resolver.unwrap_or(system_program::ID),
listing_account,
order_account,
escrow_account,
diff --git a/programs/solisting/tests/test_orders.rs b/programs/solisting/tests/test_orders.rs
index d2c6624..d9cf652 100644
--- a/programs/solisting/tests/test_orders.rs
+++ b/programs/solisting/tests/test_orders.rs
@@ -118,7 +118,7 @@ fn create_order_fails_if_resolver_not_accepted() {
&mut svm,
&[ix_create_order_with_resolver(
&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],
);
@@ -152,7 +152,7 @@ fn seller_accept_creates_active_descro_escrow() {
);
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);
assert_eq!(listing.quantity_reserved, 0);
diff --git a/sdk/src/generated/solisting/src/generated/accounts/solistingStateOrderAccount.ts b/sdk/src/generated/solisting/src/generated/accounts/solistingStateOrderAccount.ts
index 33bb7fc..ad24cb2 100644
--- a/sdk/src/generated/solisting/src/generated/accounts/solistingStateOrderAccount.ts
+++ b/sdk/src/generated/solisting/src/generated/accounts/solistingStateOrderAccount.ts
@@ -21,6 +21,8 @@ import {
getBytesEncoder,
getI64Decoder,
getI64Encoder,
+ getOptionDecoder,
+ getOptionEncoder,
getStructDecoder,
getStructEncoder,
getU64Decoder,
@@ -38,6 +40,8 @@ import {
type FetchAccountsConfig,
type MaybeAccount,
type MaybeEncodedAccount,
+ type Option,
+ type OptionOrNullable,
type ReadonlyUint8Array,
} from "@solana/kit";
import {
@@ -61,7 +65,7 @@ export type SolistingStateOrderAccount = {
listing: Address;
buyer: Address;
seller: Address;
- resolver: Address;
+ resolver: Option;
/** The currency the buyer chose to pay in. */
paymentCurrency: SolistingStateCurrency;
/** Actual amount paid (may differ from price when oracle-converted). */
@@ -79,7 +83,7 @@ export type SolistingStateOrderAccountArgs = {
listing: Address;
buyer: Address;
seller: Address;
- resolver: Address;
+ resolver: OptionOrNullable;
/** The currency the buyer chose to pay in. */
paymentCurrency: SolistingStateCurrencyArgs;
/** Actual amount paid (may differ from price when oracle-converted). */
@@ -101,7 +105,7 @@ export function getSolistingStateOrderAccountEncoder(): Encoder;
paymentCurrency: SolistingStateCurrency;
expectedAmount: bigint;
maxSlippageBps: number;
@@ -130,7 +134,7 @@ export type CreateOrderInstructionData = {
export type CreateOrderInstructionDataArgs = {
orderId: number | bigint;
escrowId: number | bigint;
- resolver: Address;
+ resolver: OptionOrNullable;
paymentCurrency: SolistingStateCurrencyArgs;
expectedAmount: number | bigint;
maxSlippageBps: number;
@@ -142,7 +146,7 @@ export function getCreateOrderInstructionDataEncoder(): Encoder
// 8: listing (32)
// 40: buyer (32)
// 72: seller (32)
-// 104: resolver (32)
-// 136: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total
-// 137: amount (8)
-// 145: escrowAccount (32)
+// 104: resolver Option = 1 byte tag + 32 bytes = 33 bytes
+// 137: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total
+// 138: amount (8)
+// 146: escrowAccount (32)
const LISTING_OFFSET = 8n
const BUYER_OFFSET = 40n
// escrowAccount offset is only valid for Sol-currency orders.
-// SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 178.
-const ESCROW_ACCOUNT_OFFSET = 145n
+// SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 179.
+const ESCROW_ACCOUNT_OFFSET = 146n
type RawGpaResult = Array<{
pubkey: Address
diff --git a/target/idl/solisting.json b/target/idl/solisting.json
index 4db8d51..1cca1c1 100644
--- a/target/idl/solisting.json
+++ b/target/idl/solisting.json
@@ -835,7 +835,9 @@
},
{
"name": "resolver",
- "type": "pubkey"
+ "type": {
+ "option": "pubkey"
+ }
},
{
"name": "payment_currency",
@@ -1461,7 +1463,9 @@
},
{
"name": "resolver",
- "type": "pubkey"
+ "type": {
+ "option": "pubkey"
+ }
},
{
"name": "payment_currency",