Compare commits

...

10 Commits

Author SHA1 Message Date
thesn10
aafbc17e94 fix order state 2026-06-27 15:13:36 +02:00
thesn10
aac025e2a8 fix empty resolver 2026-06-27 15:00:06 +02:00
thesn10
e799c0be9d resolver picker 2026-06-27 14:52:11 +02:00
thesn10
0c86751d51 fix escrow handling and fetching 2026-06-26 20:01:36 +02:00
thesn10
02c26e186f dont close on accept order 2026-06-26 19:13:30 +02:00
thesn10
12f1344138 better error handling 2026-06-26 19:13:17 +02:00
thesn10
94d2d67e83 load descro 2026-06-26 19:12:50 +02:00
thesn10
ec1c7d9f8c small fixes 2026-06-26 18:36:34 +02:00
thesn10
555598535d feat: add name and description fields to ListingAccount; update form, table, detail, dashboard 2026-06-25 20:21:28 +02:00
thesn10
ca1c0b1cd8 fix: filter highlight, nav alignment, button centering, listing title, dashboard listing name 2026-06-25 19:59:03 +02:00
35 changed files with 1233 additions and 671 deletions

View File

@@ -18,3 +18,11 @@ wallet = "~/.config/solana/id.json"
test = "cargo test" test = "cargo test"
[hooks] [hooks]
[[test.genesis]]
address = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3"
program = "../descro/target/deploy/descro.so"
[[test.genesis]]
address = "GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp"
program = "../descro/target/deploy/descro_ext_resolvers.so"

View File

@@ -0,0 +1,8 @@
import { use } from 'react'
import { EscrowDetail } from '@/components/EscrowDetail'
import type { Address } from '@solana/kit'
export default function EscrowPage({ params }: { params: Promise<{ pk: string }> }) {
const { pk } = use(params)
return <EscrowDetail pda={pk as Address} />
}

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,
@@ -60,20 +72,108 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
[['listing', pk], ['orders', pk]], [['listing', pk], ['orders', pk]],
'create_order', 'create_order',
) )
} catch (err) { } catch {
console.error('create_order failed:', err) // error already shown via toast in useTx
} finally { } finally {
setOrderLoading(false) setOrderLoading(false)
} }
} }
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

@@ -17,8 +17,11 @@ import {
DESCRO_PROGRAM_ADDRESS, DESCRO_PROGRAM_ADDRESS,
findResolverEntryPda, findResolverEntryPda,
} from '@descro/sdk' } from '@descro/sdk'
import { isSome } from '@solana/kit'
import type { Address } from '@solisting/sdk' import type { Address } from '@solisting/sdk'
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address
export default function OrderPage({ params }: { params: Promise<{ pk: string }> }) { export default function OrderPage({ params }: { params: Promise<{ pk: string }> }) {
const { pk } = use(params) const { pk } = use(params)
const { account } = useWallet() const { account } = useWallet()
@@ -31,10 +34,14 @@ export default function OrderPage({ params }: { params: Promise<{ pk: string }>
async function handleAccept() { async function handleAccept() {
if (!signer || !data) return if (!signer || !data) return
const od = data.order.data const od = data.order.data
const [resolverEntry] = await findResolverEntryPda({ authority: od.resolver }) const resolverAddr = isSome(od.resolver) ? od.resolver.value : null
const resolverKey = resolverAddr ?? SYSTEM_PROGRAM
const [resolverEntry] = resolverAddr
? await findResolverEntryPda({ authority: resolverAddr })
: [SYSTEM_PROGRAM]
const ix = getAcceptOrderInstruction({ const ix = getAcceptOrderInstruction({
seller: signer, seller: signer,
resolver: od.resolver, resolver: resolverKey,
listingAccount: od.listing, listingAccount: od.listing,
orderAccount: pk as Address, orderAccount: pk as Address,
escrowAccount: od.escrowAccount, escrowAccount: od.escrowAccount,

View File

@@ -26,6 +26,8 @@ interface FormState {
price: string price: string
quantity: string quantity: string
oracle: string oracle: string
name: string
description: string
metadataUri: string metadataUri: string
alts: AltEntry[] alts: AltEntry[]
resolvers: string[] resolvers: string[]
@@ -39,6 +41,8 @@ function blank(): FormState {
price: '', price: '',
quantity: '', quantity: '',
oracle: '', oracle: '',
name: '',
description: '',
metadataUri: '', metadataUri: '',
alts: [], alts: [],
resolvers: [], resolvers: [],
@@ -87,6 +91,9 @@ function CurrencyToggle({
}) { }) {
const active = (on: boolean): React.CSSProperties => ({ const active = (on: boolean): React.CSSProperties => ({
flex: 1, flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 11, padding: 11,
borderRadius: 10, borderRadius: 10,
fontSize: 13, fontSize: 13,
@@ -94,7 +101,7 @@ function CurrencyToggle({
cursor: 'pointer', cursor: 'pointer',
color: on ? '#0b0613' : 'var(--mut)', color: on ? '#0b0613' : 'var(--mut)',
background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)', background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)',
border: `1px solid ${on ? 'transparent' : 'var(--bd)'}`, border: `1px solid var(--bd)`,
}) })
return ( return (
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
@@ -145,6 +152,8 @@ export function CreateListingForm({ editPk }: Props) {
: (Number(d.price) / 1e9).toString(), : (Number(d.price) / 1e9).toString(),
quantity: String(d.quantity), quantity: String(d.quantity),
oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '', oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '',
name: d.name ?? '',
description: d.description ?? '',
metadataUri: d.metadataUri ?? '', metadataUri: d.metadataUri ?? '',
alts: d.altCurrencies.map((a) => ({ alts: d.altCurrencies.map((a) => ({
currency: currency:
@@ -262,6 +271,8 @@ export function CreateListingForm({ editPk }: Props) {
altCurrencies, altCurrencies,
acceptedResolvers, acceptedResolvers,
quantity: quantityN, quantity: quantityN,
name: form.name,
description: form.description,
metadataUri: form.metadataUri, metadataUri: form.metadataUri,
}) })
await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing') await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing')
@@ -275,6 +286,8 @@ export function CreateListingForm({ editPk }: Props) {
altCurrencies, altCurrencies,
acceptedResolvers, acceptedResolvers,
quantity: quantityN, quantity: quantityN,
name: form.name,
description: form.description,
metadataUri: form.metadataUri, metadataUri: form.metadataUri,
}) })
await sendTx([ix as never], [['listings']], 'create_listing') await sendTx([ix as never], [['listings']], 'create_listing')
@@ -334,6 +347,16 @@ export function CreateListingForm({ editPk }: Props) {
gap: 22, gap: 22,
}} }}
> >
{/* NAME + DESCRIPTION */}
<div>
<label style={LABEL}>NAME <span style={{ fontWeight: 400, textTransform: 'none' }}>(max 64 chars)</span></label>
<input value={form.name} onChange={set('name')} placeholder="e.g. Mechanical Keyboard Kit" style={INPUT} maxLength={64} />
</div>
<div>
<label style={LABEL}>DESCRIPTION <span style={{ fontWeight: 400, textTransform: 'none' }}>(max 256 chars)</span></label>
<input value={form.description} onChange={set('description')} placeholder="Short description of what you're selling" style={INPUT} maxLength={256} />
</div>
{/* CANONICAL CURRENCY */} {/* CANONICAL CURRENCY */}
<div> <div>
<label style={LABEL}>CANONICAL CURRENCY</label> <label style={LABEL}>CANONICAL CURRENCY</label>

View File

@@ -168,7 +168,7 @@ export function Dashboard() {
> >
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontWeight: 600, fontSize: 15 }}> <span style={{ fontWeight: 600, fontSize: 15 }}>
{l.data.metadataUri || abbrev(l.address)} {l.data.name || `Listing #${String(l.data.listingId)}`}
</span> </span>
<Badge color={sc.color} bg={sc.bg}> <Badge color={sc.color} bg={sc.bg}>
{l.data.isActive ? 'Active' : 'Inactive'} {l.data.isActive ? 'Active' : 'Inactive'}
@@ -182,7 +182,7 @@ export function Dashboard() {
fontFamily: 'var(--font-mono)', fontFamily: 'var(--font-mono)',
}} }}
> >
#{String(l.data.listingId)} · {fmtSol(l.data.price)} · {avail}/ {abbrev(l.address)} · {fmtSol(l.data.price)} · {avail}/
{String(l.data.quantity)} avail · {String(l.data.quantityReserved)} pending {String(l.data.quantity)} avail · {String(l.data.quantityReserved)} pending
</div> </div>
</div> </div>

View File

@@ -0,0 +1,110 @@
'use client'
import { useRouter } from 'next/navigation'
import { isSome } from '@solana/kit'
import { escrowStateLabel } from '@descro/sdk'
import { EscrowState } from '@descro/sdk/src/generated/descro/src/generated/types/escrowState'
import { useEscrow } from '@/hooks/useEscrow'
import { useOrderByEscrow } from '@/hooks/useOrderByEscrow'
import { EscrowStateMachine } from '@/components/EscrowStateMachine'
import { FieldRow, MonoChip } from '@/components/ui/FieldRow'
import { Badge, STATUS_COLORS } from '@/components/ui/Badge'
import { abbrev, fmtSol } from '@/lib/format'
import type { Address } from '@solana/kit'
function escrowStatusKey(state: EscrowState): keyof typeof STATUS_COLORS {
switch (state) {
case EscrowState.AwaitingSellerConfirm: return 'awaitingConfirm'
case EscrowState.Active: return 'active'
case EscrowState.Disputed: return 'disputed'
case EscrowState.Complete: return 'complete'
default: return 'cancelled'
}
}
interface Props {
pda: Address
}
export function EscrowDetail({ pda }: Props) {
const router = useRouter()
const { data: escrow, isLoading, error } = useEscrow(pda)
const { data: order } = useOrderByEscrow(pda)
if (isLoading) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading</div>
if (error || !escrow) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Escrow account not found.</div>
const ed = escrow.data
const stateLabel = escrowStateLabel(ed.state)
const sc = STATUS_COLORS[escrowStatusKey(ed.state)]
const resolverPk = isSome(ed.disputeResolver) ? ed.disputeResolver.value : null
return (
<div>
<button
onClick={() => router.push('/escrows')}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--mut)', fontSize: 13, fontWeight: 500, marginBottom: 18 }}
>
Escrows
</button>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
<div>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 5 }}>
ESCROW ACCOUNT · #{String(ed.escrowId)}
</div>
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>{abbrev(pda)}</h1>
</div>
<Badge color={sc.color} bg={sc.bg}>{stateLabel}</Badge>
</div>
<EscrowStateMachine state={ed.state} />
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>
ESCROW ACCOUNT · descro
</div>
<FieldRow label="Address">
<MonoChip value={abbrev(pda)} onCopy={() => navigator.clipboard.writeText(pda)} />
</FieldRow>
<FieldRow label="State">
<Badge color={sc.color} bg={sc.bg}>{stateLabel}</Badge>
</FieldRow>
<FieldRow label="Escrow ID">
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5 }}>#{String(ed.escrowId)}</span>
</FieldRow>
<FieldRow label="Seller">
<MonoChip value={abbrev(ed.seller)} onCopy={() => navigator.clipboard.writeText(ed.seller)} />
</FieldRow>
<FieldRow label="Buyer">
<MonoChip value={abbrev(ed.buyer)} onCopy={() => navigator.clipboard.writeText(ed.buyer)} />
</FieldRow>
<FieldRow label="Amount">
<span style={{ fontSize: 13.5, fontWeight: 600 }}>{fmtSol(ed.amount)}</span>
</FieldRow>
{resolverPk && (
<FieldRow label="Resolver">
<MonoChip
value={abbrev(resolverPk)}
onClick={() => router.push(`/resolver/${resolverPk}`)}
onCopy={() => navigator.clipboard.writeText(resolverPk)}
/>
</FieldRow>
)}
<FieldRow label="Linked Order" last>
{order
? (
<MonoChip
value={abbrev(order.address)}
onClick={() => router.push(`/order/${order.address}`)}
onCopy={() => navigator.clipboard.writeText(order.address)}
/>
)
: <span style={{ fontSize: 13, color: 'var(--mut)' }}></span>
}
</FieldRow>
</div>
</div>
)
}

View File

@@ -82,8 +82,8 @@ export function EscrowsTable() {
borderRadius: 7, borderRadius: 7,
fontSize: 12.5, fontSize: 12.5,
fontWeight: 600, fontWeight: 600,
color: filter === f.key ? 'var(--tx)' : 'var(--mut)', color: filter === f.key ? '#0b0613' : 'var(--mut)',
background: filter === f.key ? 'var(--bg2)' : 'transparent', background: filter === f.key ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@@ -137,7 +137,7 @@ export function EscrowsTable() {
return ( return (
<div <div
key={e.pda} key={e.pda}
onClick={() => router.push(`/search?q=${e.pda}`)} onClick={() => router.push(`/escrow/${e.pda}`)}
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr', gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr',

View File

@@ -54,30 +54,40 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
LISTING ACCOUNT · #{String(d.listingId)} LISTING ACCOUNT · #{String(d.listingId)}
</div> </div>
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}> <h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>
{d.metadataUri || abbrev(pk)} {d.name || abbrev(pk)}
</h1> </h1>
</div> </div>
<Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge> <Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge>
</div> </div>
{(d.description || d.metadataUri) && (
<p style={{ margin: '0 0 24px', fontSize: 14, color: 'var(--mut)', maxWidth: 620, lineHeight: 1.55 }}>
{d.description || (
<a href={d.metadataUri} target="_blank" rel="noreferrer" style={{ color: 'var(--acc2light)' }}>{d.metadataUri}</a>
)}
</p>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, alignItems: 'start', marginBottom: 24 }}> <div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, alignItems: 'start', marginBottom: 24 }}>
{/* Left: account fields */} {/* Left: account fields */}
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}> <div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>ACCOUNT FIELDS</div> <div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>ACCOUNT FIELDS</div>
<FieldRow label="Listing ID"><span style={{ fontSize: 13.5, fontWeight: 600 }}>#{String(d.listingId)}</span></FieldRow> <FieldRow label="Address"><MonoChip value={abbrev(pk)} onCopy={() => navigator.clipboard.writeText(pk)} /></FieldRow>
<FieldRow label="Seller"><MonoChip value={abbrev(d.seller)} onCopy={() => navigator.clipboard.writeText(d.seller)} onClick={() => router.push(`/search?q=${d.seller}`)} /></FieldRow> <FieldRow label="Seller"><MonoChip value={abbrev(d.seller)} onCopy={() => navigator.clipboard.writeText(d.seller)} onClick={() => router.push(`/search?q=${d.seller}`)} /></FieldRow>
<FieldRow label="Price"><span style={{ fontSize: 13.5, fontWeight: 600 }}>{fmtListingPrice()}</span></FieldRow> <FieldRow label="Listing ID"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13 }}>#{String(d.listingId)}</span></FieldRow>
<FieldRow label="Currency"> <FieldRow label="Canonical Currency">
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5 }}> <span style={{ fontWeight: 600, fontSize: 13 }}>
{d.canonicalCurrency.__kind === 'Sol' ? 'SOL (native)' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`} {d.canonicalCurrency.__kind === 'Sol' ? 'SOL' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`}
</span> </span>
</FieldRow> </FieldRow>
{canonicalOracleAddr && ( <FieldRow label="Price"><span style={{ fontWeight: 700, fontSize: 13.5 }}>{fmtListingPrice()}</span></FieldRow>
<FieldRow label="Oracle"> <FieldRow label="Canonical Oracle">
<MonoChip value={abbrev(canonicalOracleAddr)} onCopy={() => navigator.clipboard.writeText(canonicalOracleAddr)} /> {canonicalOracleAddr
</FieldRow> ? <MonoChip value={abbrev(canonicalOracleAddr)} onCopy={() => navigator.clipboard.writeText(canonicalOracleAddr)} />
)} : <span style={{ fontSize: 13, color: 'var(--mut)' }}>None priced directly</span>
}
</FieldRow>
<FieldRow label="Status"><Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge></FieldRow> <FieldRow label="Status"><Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge></FieldRow>
<FieldRow label="Bump"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--mut)' }}>{d.bump}</span></FieldRow>
{d.metadataUri && ( {d.metadataUri && (
<> <>
@@ -137,7 +147,7 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
<Button variant="danger" onClick={onClose}>Close Listing</Button> <Button variant="danger" onClick={onClose}>Close Listing</Button>
</div> </div>
)} )}
{canPlace && <Button onClick={onPlaceOrder}>Place Order</Button>} {canPlace && <Button style={{ width: '100%', padding: 11 }} onClick={onPlaceOrder}>Place Order</Button>}
{!walletAddress && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>Connect a wallet to place an order or manage this listing.</div>} {!walletAddress && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>Connect a wallet to place an order or manage this listing.</div>}
{walletAddress && !isSeller && !canPlace && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>No actions available listing is inactive or out of stock.</div>} {walletAddress && !isSeller && !canPlace && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>No actions available listing is inactive or out of stock.</div>}
</div> </div>

View File

@@ -59,15 +59,27 @@ export function ListingsTable() {
style={{ height: 38, width: 220, padding: '0 13px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, fontSize: 13, fontFamily: 'var(--font-mono)', outline: 'none' }} style={{ height: 38, width: 220, padding: '0 13px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, fontSize: 13, fontFamily: 'var(--font-mono)', outline: 'none' }}
/> />
<div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}> <div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}>
{FILTERS.map((f) => ( {FILTERS.map((f) => {
<button const active = filter === f.key
key={f.key} return (
onClick={() => setFilter(f.key)} <button
style={{ padding: '6px 13px', borderRadius: 7, fontSize: 12.5, fontWeight: 600, color: filter === f.key ? 'var(--tx)' : 'var(--mut)', background: filter === f.key ? 'var(--bg2)' : 'transparent' }} key={f.key}
> onClick={() => setFilter(f.key)}
{f.label} style={{
</button> padding: '6px 13px',
))} borderRadius: 7,
fontSize: 12.5,
fontWeight: 600,
color: active ? '#0b0613' : 'var(--mut)',
background: active ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
border: 'none',
cursor: 'pointer',
}}
>
{f.label}
</button>
)
})}
</div> </div>
</div> </div>
</div> </div>
@@ -96,7 +108,7 @@ export function ListingsTable() {
> >
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{l.data.metadataUri || abbrev(l.address)} {l.data.name || abbrev(l.address)}
</div> </div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}> <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}>
#{String(l.data.listingId)} · {abbrev(l.address)} #{String(l.data.listingId)} · {abbrev(l.address)}

View File

@@ -147,6 +147,9 @@ export function Nav() {
})} })}
</div> </div>
{/* Spacer — pushes right-side controls to the edge */}
<div style={{ flex: 1 }} />
{/* Network picker */} {/* Network picker */}
<div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}> <div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}>
<button <button

View File

@@ -44,7 +44,7 @@ export function OrderDetail({ pk, walletAddress, onAccept, onReject, onCancel, o
const od = order.data const od = order.data
const ed = escrow?.data const ed = escrow?.data
const stateEnum = ed?.state ?? EscrowState.Cancelled const stateEnum = ed?.state ?? EscrowState.Complete
const stateLabel = escrowStateLabel(stateEnum) const stateLabel = escrowStateLabel(stateEnum)
const sc = STATUS_COLORS[escrowStatusKey(stateEnum)] const sc = STATUS_COLORS[escrowStatusKey(stateEnum)]

View File

@@ -36,6 +36,9 @@ export function Button({ variant = 'primary', children, style, disabled, ...rest
{...rest} {...rest}
disabled={disabled} disabled={disabled}
style={{ style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: '11px 18px', padding: '11px 18px',
borderRadius: 'var(--radius)', borderRadius: 'var(--radius)',
fontWeight: 700, fontWeight: 700,

View File

@@ -2,24 +2,26 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react' import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
const ToastCtx = createContext<(msg: string) => void>(() => {}) type ToastFn = (msg: string, type?: 'success' | 'error') => void
const ToastCtx = createContext<ToastFn>(() => {})
export function useToast() { export function useToast() {
return useContext(ToastCtx) return useContext(ToastCtx)
} }
export function ToastProvider({ children }: { children: ReactNode }) { export function ToastProvider({ children }: { children: ReactNode }) {
const [msg, setMsg] = useState('') const [state, setState] = useState<{ msg: string; type: 'success' | 'error' } | null>(null)
const show = useCallback((m: string) => { const show = useCallback<ToastFn>((m, type = 'success') => {
setMsg(m) setState({ msg: m, type })
setTimeout(() => setMsg(''), 2800) setTimeout(() => setState(null), type === 'error' ? 4500 : 2800)
}, []) }, [])
return ( return (
<ToastCtx.Provider value={show}> <ToastCtx.Provider value={show}>
{children} {children}
{msg && ( {state && (
<div <div
style={{ style={{
position: 'fixed', position: 'fixed',
@@ -32,7 +34,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
gap: 10, gap: 10,
padding: '12px 18px', padding: '12px 18px',
background: 'var(--bg2)', background: 'var(--bg2)',
border: '1px solid var(--accBd)', border: `1px solid ${state.type === 'error' ? 'var(--errBd, #f87171)' : 'var(--accBd)'}`,
borderRadius: 12, borderRadius: 12,
boxShadow: '0 16px 40px rgba(0,0,0,.55)', boxShadow: '0 16px 40px rgba(0,0,0,.55)',
fontSize: 13, fontSize: 13,
@@ -46,12 +48,12 @@ export function ToastProvider({ children }: { children: ReactNode }) {
width: 8, width: 8,
height: 8, height: 8,
borderRadius: '50%', borderRadius: '50%',
background: 'var(--acc2)', background: state.type === 'error' ? '#f87171' : 'var(--acc2)',
boxShadow: '0 0 8px var(--acc2)', boxShadow: state.type === 'error' ? '0 0 8px #f87171' : '0 0 8px var(--acc2)',
flexShrink: 0, flexShrink: 0,
}} }}
/> />
{msg} {state.msg}
</div> </div>
)} )}
</ToastCtx.Provider> </ToastCtx.Provider>

View File

@@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query'
import { useCluster } from '@solana/connector/react'
import { createSolanaRpc } from '@solana/kit'
import { fetchEscrowAccount } from '@descro/sdk'
import type { Address } from '@solana/kit'
export function useEscrow(pda: Address) {
const { cluster } = useCluster()
return useQuery({
queryKey: ['escrow', pda, cluster?.id],
queryFn: () => {
const rpc = createSolanaRpc(cluster!.url)
return fetchEscrowAccount(
rpc as Parameters<typeof fetchEscrowAccount>[0],
pda,
).catch(() => null)
},
enabled: !!cluster && !!pda,
refetchInterval: 15_000,
})
}

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query'
import { useCluster } from '@solana/connector/react'
import { createSolanaRpc } from '@solana/kit'
import { fetchOrderByEscrow } from '@solisting/sdk'
import type { Address } from '@solana/kit'
export function useOrderByEscrow(escrowPda: Address) {
const { cluster } = useCluster()
return useQuery({
queryKey: ['orderByEscrow', escrowPda, cluster?.id],
queryFn: () => {
const rpc = createSolanaRpc(cluster!.url)
return fetchOrderByEscrow(rpc, escrowPda)
},
enabled: !!cluster && !!escrowPda,
refetchInterval: 30_000,
})
}

View File

@@ -39,10 +39,15 @@ export function useTx() {
const signed = await signTransactionMessageWithSigners(txMsg) const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed) assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc: rpc as never, rpcSubscriptions: rpcSubscriptions as never })( try {
signed as never, await sendAndConfirmTransactionFactory({ rpc: rpc as never, rpcSubscriptions: rpcSubscriptions as never })(
{ commitment: 'confirmed' }, signed as never,
) { commitment: 'confirmed' },
)
} catch (err) {
toast(friendlyTxError(err), 'error')
throw err
}
toast(`Tx confirmed — ${label}`) toast(`Tx confirmed — ${label}`)
for (const key of invalidateKeys) { for (const key of invalidateKeys) {
@@ -50,3 +55,20 @@ export function useTx() {
} }
} }
} }
function friendlyTxError(err: unknown): string {
const msg = err instanceof Error ? err.message : String(err)
if (
msg.includes('Attempt to debit an account but found no record of a prior credit') ||
msg.includes('AccountNotFound')
) {
return 'Insufficient SOL — please fund your wallet and try again.'
}
if (msg.includes('insufficient lamports') || msg.includes('insufficient funds')) {
return 'Insufficient SOL — please fund your wallet and try again.'
}
if (msg.includes('User rejected') || msg.includes('Transaction was not confirmed')) {
return 'Transaction cancelled.'
}
return 'Transaction failed — please try again.'
}

View File

@@ -23,7 +23,6 @@ pub struct AcceptOrder<'info> {
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()], seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
bump = order_account.bump, bump = order_account.bump,
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized, constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
close = seller,
)] )]
pub order_account: Account<'info, OrderAccount>, pub order_account: Account<'info, OrderAccount>,

View File

@@ -29,6 +29,8 @@ pub fn handler(
alt_currencies: Vec<AltCurrencyConfig>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
@@ -40,6 +42,8 @@ pub fn handler(
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.quantity_reserved = 0; listing.quantity_reserved = 0;
listing.name = name;
listing.description = description;
listing.metadata_uri = metadata_uri; listing.metadata_uri = metadata_uri;
listing.listing_id = listing_id; listing.listing_id = listing_id;
listing.is_active = true; listing.is_active = true;

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

@@ -25,6 +25,8 @@ pub fn handler(
alt_currencies: Vec<AltCurrencyConfig>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
@@ -34,6 +36,8 @@ pub fn handler(
listing.alt_currencies = alt_currencies; listing.alt_currencies = alt_currencies;
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.name = name;
listing.description = description;
listing.metadata_uri = metadata_uri; listing.metadata_uri = metadata_uri;
Ok(()) Ok(())
} }

View File

@@ -31,11 +31,14 @@ pub mod solisting {
alt_currencies: Vec<state::AltCurrencyConfig>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
create_listing::handler( create_listing::handler(
ctx, listing_id, canonical_currency, price, ctx, listing_id, canonical_currency, price,
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity,
name, description, metadata_uri,
) )
} }
@@ -48,11 +51,14 @@ pub mod solisting {
alt_currencies: Vec<state::AltCurrencyConfig>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
update_listing::handler( update_listing::handler(
ctx, canonical_currency, price, ctx, canonical_currency, price,
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity,
name, description, metadata_uri,
) )
} }
@@ -64,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

@@ -52,6 +52,10 @@ pub struct ListingAccount {
pub quantity: u32, pub quantity: u32,
/// Units held by pending orders. available = quantity - quantity_reserved. /// Units held by pending orders. available = quantity - quantity_reserved.
pub quantity_reserved: u32, pub quantity_reserved: u32,
#[max_len(64)]
pub name: String,
#[max_len(256)]
pub description: String,
#[max_len(256)] #[max_len(256)]
pub metadata_uri: String, pub metadata_uri: String,
pub listing_id: u64, pub listing_id: u64,
@@ -65,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

@@ -101,6 +101,8 @@ pub fn ix_create_listing(
alt_currencies: Vec<AltCurrencyConfig>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Instruction { ) -> Instruction {
let listing_account = listing_pda(seller, listing_id); let listing_account = listing_pda(seller, listing_id);
@@ -114,6 +116,8 @@ pub fn ix_create_listing(
alt_currencies, alt_currencies,
accepted_resolvers, accepted_resolvers,
quantity, quantity,
name,
description,
metadata_uri, metadata_uri,
} }
.data(), .data(),
@@ -136,6 +140,8 @@ pub fn ix_update_listing(
alt_currencies: Vec<AltCurrencyConfig>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
name: String,
description: String,
metadata_uri: String, metadata_uri: String,
) -> Instruction { ) -> Instruction {
let listing_account = listing_pda(seller, listing_id); let listing_account = listing_pda(seller, listing_id);
@@ -148,6 +154,8 @@ pub fn ix_update_listing(
alt_currencies, alt_currencies,
accepted_resolvers, accepted_resolvers,
quantity, quantity,
name,
description,
metadata_uri, metadata_uri,
} }
.data(), .data(),
@@ -187,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,
@@ -197,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()),
) )
} }
@@ -211,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);
@@ -256,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);
@@ -268,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

@@ -17,7 +17,7 @@ fn seller_can_create_sol_only_listing() {
vec![], vec![],
vec![], vec![],
10, 10,
"ipfs://test".to_string(), "".to_string(), "".to_string(), "ipfs://test".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -48,7 +48,7 @@ fn seller_can_create_listing_with_usdc_alt_stablecoin() {
}], }],
vec![], vec![],
5, 5,
"ipfs://x".to_string(), "".to_string(), "".to_string(), "ipfs://x".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -75,7 +75,7 @@ fn seller_can_create_listing_with_bonk_alt_oracle() {
}], }],
vec![], vec![],
5, 5,
"".to_string(), "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -90,7 +90,7 @@ fn seller_can_update_listing() {
let listing_id: u64 = 2; let listing_id: u64 = 2;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, &seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 10, "".to_string(), None, vec![], vec![], 10, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -103,7 +103,7 @@ fn seller_can_update_listing() {
vec![], vec![],
vec![], vec![],
20, 20,
"ipfs://new".to_string(), "".to_string(), "".to_string(), "ipfs://new".to_string(),
); );
send(&mut svm, &[ix_update], &[&seller]); send(&mut svm, &[ix_update], &[&seller]);
@@ -118,7 +118,7 @@ fn seller_can_close_listing() {
let listing_id: u64 = 3; let listing_id: u64 = 3;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, &seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 5, "".to_string(), None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);

View File

@@ -10,7 +10,7 @@ fn buyer_can_create_order_canonical_sol() {
let price = 100_000_000u64; let price = 100_000_000u64;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, price, &seller.pubkey(), listing_id, Currency::Sol, price,
None, vec![], vec![], 5, "".to_string(), None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -42,7 +42,7 @@ fn create_order_fails_if_listing_inactive() {
let listing_id = 2u64; let listing_id = 2u64;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, &seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 1, "".to_string(), None, vec![], vec![], 1, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
let ix_close = ix_close_listing(&seller.pubkey(), listing_id); let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
@@ -62,7 +62,7 @@ fn create_order_fails_if_out_of_stock() {
let listing_id = 3u64; let listing_id = 3u64;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, &seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![], 1, "".to_string(), None, vec![], vec![], 1, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
send( send(
@@ -87,7 +87,7 @@ fn create_order_fails_if_currency_not_accepted() {
let listing_id = 4u64; let listing_id = 4u64;
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, &seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![], 5, "".to_string(), None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -110,7 +110,7 @@ fn create_order_fails_if_resolver_not_accepted() {
let allowed_resolver = Pubkey::new_unique(); let allowed_resolver = Pubkey::new_unique();
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, &seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![allowed_resolver], 5, "".to_string(), None, vec![], vec![allowed_resolver], 5, "".to_string(), "".to_string(), "".to_string(),
); );
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
@@ -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],
); );
@@ -132,7 +132,7 @@ fn seller_accept_creates_active_descro_escrow() {
let price = 100_000_000u64; let price = 100_000_000u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
let order_id = 1u64; let order_id = 1u64;
@@ -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);
@@ -166,7 +166,7 @@ fn seller_can_reject_order() {
let price = 100_000_000u64; let price = 100_000_000u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
send( send(
@@ -197,7 +197,7 @@ fn buyer_can_cancel_order() {
let listing_id = 21u64; let listing_id = 21u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
send( send(
@@ -223,7 +223,7 @@ fn reject_handles_already_cancelled_escrow() {
let listing_id = 22u64; let listing_id = 22u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
send( send(
@@ -250,7 +250,7 @@ fn anyone_can_close_stale_order_after_terminal_escrow() {
let listing_id = 30u64; let listing_id = 30u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
send( send(
@@ -284,7 +284,7 @@ fn close_stale_order_fails_if_escrow_still_active() {
let listing_id = 31u64; let listing_id = 31u64;
send( send(
&mut svm, &mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
&[&seller], &[&seller],
); );
send( send(

View File

@@ -96,6 +96,8 @@ export type SolistingStateListingAccount = {
quantity: number; quantity: number;
/** Units held by pending orders. available = quantity - quantity_reserved. */ /** Units held by pending orders. available = quantity - quantity_reserved. */
quantityReserved: number; quantityReserved: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
listingId: bigint; listingId: bigint;
isActive: boolean; isActive: boolean;
@@ -125,6 +127,8 @@ export type SolistingStateListingAccountArgs = {
quantity: number; quantity: number;
/** Units held by pending orders. available = quantity - quantity_reserved. */ /** Units held by pending orders. available = quantity - quantity_reserved. */
quantityReserved: number; quantityReserved: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
listingId: number | bigint; listingId: number | bigint;
isActive: boolean; isActive: boolean;
@@ -147,6 +151,8 @@ export function getSolistingStateListingAccountEncoder(): Encoder<SolistingState
["acceptedResolvers", getArrayEncoder(getAddressEncoder())], ["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
["quantity", getU32Encoder()], ["quantity", getU32Encoder()],
["quantityReserved", getU32Encoder()], ["quantityReserved", getU32Encoder()],
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["listingId", getU64Encoder()], ["listingId", getU64Encoder()],
["isActive", getBooleanEncoder()], ["isActive", getBooleanEncoder()],
@@ -174,6 +180,8 @@ export function getSolistingStateListingAccountDecoder(): Decoder<SolistingState
["acceptedResolvers", getArrayDecoder(getAddressDecoder())], ["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
["quantity", getU32Decoder()], ["quantity", getU32Decoder()],
["quantityReserved", getU32Decoder()], ["quantityReserved", getU32Decoder()],
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["listingId", getU64Decoder()], ["listingId", getU64Decoder()],
["isActive", getBooleanDecoder()], ["isActive", getBooleanDecoder()],

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

@@ -111,6 +111,8 @@ export type CreateListingInstructionData = {
altCurrencies: Array<SolistingStateAltCurrencyConfig>; altCurrencies: Array<SolistingStateAltCurrencyConfig>;
acceptedResolvers: Array<Address>; acceptedResolvers: Array<Address>;
quantity: number; quantity: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
}; };
@@ -122,6 +124,8 @@ export type CreateListingInstructionDataArgs = {
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>; altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
acceptedResolvers: Array<Address>; acceptedResolvers: Array<Address>;
quantity: number; quantity: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
}; };
@@ -139,6 +143,8 @@ export function getCreateListingInstructionDataEncoder(): Encoder<CreateListingI
], ],
["acceptedResolvers", getArrayEncoder(getAddressEncoder())], ["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
["quantity", getU32Encoder()], ["quantity", getU32Encoder()],
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
]), ]),
(value) => ({ ...value, discriminator: CREATE_LISTING_DISCRIMINATOR }), (value) => ({ ...value, discriminator: CREATE_LISTING_DISCRIMINATOR }),
@@ -158,6 +164,8 @@ export function getCreateListingInstructionDataDecoder(): Decoder<CreateListingI
], ],
["acceptedResolvers", getArrayDecoder(getAddressDecoder())], ["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
["quantity", getU32Decoder()], ["quantity", getU32Decoder()],
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
]); ]);
} }
@@ -187,6 +195,8 @@ export type CreateListingAsyncInput<
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"]; altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"]; acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
quantity: CreateListingInstructionDataArgs["quantity"]; quantity: CreateListingInstructionDataArgs["quantity"];
name: CreateListingInstructionDataArgs["name"];
description: CreateListingInstructionDataArgs["description"];
metadataUri: CreateListingInstructionDataArgs["metadataUri"]; metadataUri: CreateListingInstructionDataArgs["metadataUri"];
}; };
@@ -279,6 +289,8 @@ export type CreateListingInput<
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"]; altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"]; acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
quantity: CreateListingInstructionDataArgs["quantity"]; quantity: CreateListingInstructionDataArgs["quantity"];
name: CreateListingInstructionDataArgs["name"];
description: CreateListingInstructionDataArgs["description"];
metadataUri: CreateListingInstructionDataArgs["metadataUri"]; metadataUri: CreateListingInstructionDataArgs["metadataUri"];
}; };

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

@@ -101,6 +101,8 @@ export type UpdateListingInstructionData = {
altCurrencies: Array<SolistingStateAltCurrencyConfig>; altCurrencies: Array<SolistingStateAltCurrencyConfig>;
acceptedResolvers: Array<Address>; acceptedResolvers: Array<Address>;
quantity: number; quantity: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
}; };
@@ -111,6 +113,8 @@ export type UpdateListingInstructionDataArgs = {
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>; altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
acceptedResolvers: Array<Address>; acceptedResolvers: Array<Address>;
quantity: number; quantity: number;
name: string;
description: string;
metadataUri: string; metadataUri: string;
}; };
@@ -127,6 +131,8 @@ export function getUpdateListingInstructionDataEncoder(): Encoder<UpdateListingI
], ],
["acceptedResolvers", getArrayEncoder(getAddressEncoder())], ["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
["quantity", getU32Encoder()], ["quantity", getU32Encoder()],
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
]), ]),
(value) => ({ ...value, discriminator: UPDATE_LISTING_DISCRIMINATOR }), (value) => ({ ...value, discriminator: UPDATE_LISTING_DISCRIMINATOR }),
@@ -145,6 +151,8 @@ export function getUpdateListingInstructionDataDecoder(): Decoder<UpdateListingI
], ],
["acceptedResolvers", getArrayDecoder(getAddressDecoder())], ["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
["quantity", getU32Decoder()], ["quantity", getU32Decoder()],
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
]); ]);
} }
@@ -171,6 +179,8 @@ export type UpdateListingInput<
altCurrencies: UpdateListingInstructionDataArgs["altCurrencies"]; altCurrencies: UpdateListingInstructionDataArgs["altCurrencies"];
acceptedResolvers: UpdateListingInstructionDataArgs["acceptedResolvers"]; acceptedResolvers: UpdateListingInstructionDataArgs["acceptedResolvers"];
quantity: UpdateListingInstructionDataArgs["quantity"]; quantity: UpdateListingInstructionDataArgs["quantity"];
name: UpdateListingInstructionDataArgs["name"];
description: UpdateListingInstructionDataArgs["description"];
metadataUri: UpdateListingInstructionDataArgs["metadataUri"]; metadataUri: UpdateListingInstructionDataArgs["metadataUri"];
}; };

View File

@@ -115,6 +115,14 @@
"name": "quantity", "name": "quantity",
"type": "u32" "type": "u32"
}, },
{
"name": "name",
"type": "string"
},
{
"name": "description",
"type": "string"
},
{ {
"name": "metadata_uri", "name": "metadata_uri",
"type": "string" "type": "string"
@@ -208,6 +216,14 @@
"name": "quantity", "name": "quantity",
"type": "u32" "type": "u32"
}, },
{
"name": "name",
"type": "string"
},
{
"name": "description",
"type": "string"
},
{ {
"name": "metadata_uri", "name": "metadata_uri",
"type": "string" "type": "string"
@@ -1337,6 +1353,14 @@
], ],
"type": "u32" "type": "u32"
}, },
{
"name": "name",
"type": "string"
},
{
"name": "description",
"type": "string"
},
{ {
"name": "metadata_uri", "name": "metadata_uri",
"type": "string" "type": "string"

View File

@@ -7,7 +7,7 @@ export * from './listing'
// Order exports are imported but we exclude the re-exported deriveEscrowId to avoid duplication // Order exports are imported but we exclude the re-exported deriveEscrowId to avoid duplication
// since it's already exported from pda.js // since it's already exported from pda.js
export type { OrderAccountWithPda } from './order' export type { OrderAccountWithPda } from './order'
export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrder } from './order' export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrderByEscrow, fetchOrder } from './order'
// Re-export Address for consumers // Re-export Address for consumers
export type { Address, Account } from '@solana/kit' export type { Address, Account } from '@solana/kit'

View File

@@ -1,11 +1,11 @@
import { address, type Account, type Address, type Rpc } from '@solana/kit' import { address, type Account, type Address, type Rpc } from '@solana/kit'
import { type GetProgramAccountsApi } from '@solana/rpc-api' import { type GetProgramAccountsApi } from '@solana/rpc-api'
import { type GetAccountInfoApi } from '@solana/rpc-api' import { type GetAccountInfoApi } from '@solana/rpc-api'
import type { Base64EncodedBytes } from '@solana/rpc-types' import type { Base64EncodedBytes, Lamports } from '@solana/rpc-types'
import { import {
decodeSolistingStateOrderAccount, decodeSolistingStateOrderAccount,
fetchSolistingStateOrderAccount, fetchSolistingStateOrderAccount,
SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR, getSolistingStateOrderAccountDiscriminatorBytes,
type SolistingStateOrderAccount, type SolistingStateOrderAccount,
} from './generated/solisting/src/generated/index' } from './generated/solisting/src/generated/index'
import { deriveEscrowId } from './pda' import { deriveEscrowId } from './pda'
@@ -16,22 +16,57 @@ export type OrderAccountWithPda = Account<SolistingStateOrderAccount>
type GpaRpc = Rpc<GetProgramAccountsApi> type GpaRpc = Rpc<GetProgramAccountsApi>
type GetRpc = Rpc<GetAccountInfoApi> type GetRpc = Rpc<GetAccountInfoApi>
async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> { // OrderAccount byte layout (Sol-currency variant, the only one currently implemented):
const disc = SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR // 0: discriminator (8)
const discBase64 = Buffer.from(disc).toString('base64') as Base64EncodedBytes // 8: listing (32)
// 40: buyer (32)
// 72: seller (32)
// 104: resolver Option<Pubkey> = 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 179.
const ESCROW_ACCOUNT_OFFSET = 146n
type RawGpaResult = Array<{
pubkey: Address
account: {
executable: boolean
lamports: bigint
owner: Address
space: bigint
data: [string, 'base64']
}
}>
const DISC_BASE64 = Buffer.from(
getSolistingStateOrderAccountDiscriminatorBytes(),
).toString('base64') as Base64EncodedBytes
async function gpa(
rpc: GpaRpc,
addressFilter: { offset: bigint; addr: Address },
): Promise<OrderAccountWithPda[]> {
const results = await rpc const results = await rpc
.getProgramAccounts(PROGRAM_ADDRESS, { .getProgramAccounts(PROGRAM_ADDRESS, {
encoding: 'base64', encoding: 'base64',
filters: [{ memcmp: { offset: 0n, bytes: discBase64, encoding: 'base64' } }], filters: [
{ memcmp: { offset: 0n, bytes: DISC_BASE64, encoding: 'base64' } },
{ memcmp: { offset: addressFilter.offset, bytes: addressFilter.addr as never, encoding: 'base58' } },
],
}) })
.send() .send()
return (results as Array<{ pubkey: Address; account: { executable: boolean; lamports: bigint; owner: Address; space: bigint; data: [string, 'base64'] } }>).map((r) => {
return (results as RawGpaResult).map((r) => {
const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64')) const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64'))
return decodeSolistingStateOrderAccount({ return decodeSolistingStateOrderAccount({
address: r.pubkey, address: r.pubkey,
data, data,
executable: r.account.executable, executable: r.account.executable,
lamports: r.account.lamports as unknown as import('@solana/rpc-types').Lamports, lamports: r.account.lamports as unknown as Lamports,
programAddress: r.account.owner, programAddress: r.account.owner,
space: r.account.space, space: r.account.space,
exists: true, exists: true,
@@ -39,26 +74,20 @@ async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
}) })
} }
export async function fetchOrdersForListing( export function fetchOrdersForListing(rpc: GpaRpc, listingPk: Address): Promise<OrderAccountWithPda[]> {
rpc: GpaRpc, return gpa(rpc, { offset: LISTING_OFFSET, addr: listingPk })
listingPk: Address,
): Promise<OrderAccountWithPda[]> {
const all = await fetchAllOrders(rpc)
return all.filter((o) => o.data.listing === listingPk)
} }
export async function fetchOrdersByBuyer( export function fetchOrdersByBuyer(rpc: GpaRpc, buyer: Address): Promise<OrderAccountWithPda[]> {
rpc: GpaRpc, return gpa(rpc, { offset: BUYER_OFFSET, addr: buyer })
buyer: Address,
): Promise<OrderAccountWithPda[]> {
const all = await fetchAllOrders(rpc)
return all.filter((o) => o.data.buyer === buyer)
} }
export async function fetchOrder( export async function fetchOrderByEscrow(rpc: GpaRpc, escrowPda: Address): Promise<OrderAccountWithPda | null> {
rpc: GetRpc, const results = await gpa(rpc, { offset: ESCROW_ACCOUNT_OFFSET, addr: escrowPda })
addr: Address, return results[0] ?? null
): Promise<OrderAccountWithPda | null> { }
export async function fetchOrder(rpc: GetRpc, addr: Address): Promise<OrderAccountWithPda | null> {
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null) return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
} }

File diff suppressed because it is too large Load Diff