Compare commits
10 Commits
b18bdefcc0
...
aafbc17e94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aafbc17e94 | ||
|
|
aac025e2a8 | ||
|
|
e799c0be9d | ||
|
|
0c86751d51 | ||
|
|
02c26e186f | ||
|
|
12f1344138 | ||
|
|
94d2d67e83 | ||
|
|
ec1c7d9f8c | ||
|
|
555598535d | ||
|
|
ca1c0b1cd8 |
@@ -18,3 +18,11 @@ wallet = "~/.config/solana/id.json"
|
||||
test = "cargo test"
|
||||
|
||||
[hooks]
|
||||
|
||||
[[test.genesis]]
|
||||
address = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3"
|
||||
program = "../descro/target/deploy/descro.so"
|
||||
|
||||
[[test.genesis]]
|
||||
address = "GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp"
|
||||
program = "../descro/target/deploy/descro_ext_resolvers.so"
|
||||
|
||||
8
app/src/app/escrow/[pk]/page.tsx
Normal file
8
app/src/app/escrow/[pk]/page.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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<Address | null>(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,
|
||||
@@ -60,20 +72,108 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
|
||||
[['listing', pk], ['orders', pk]],
|
||||
'create_order',
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('create_order failed:', err)
|
||||
} catch {
|
||||
// error already shown via toast in useTx
|
||||
} finally {
|
||||
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 (
|
||||
<>
|
||||
<ListingDetail
|
||||
pk={pk as Address}
|
||||
walletAddress={account ?? null}
|
||||
onUpdate={() => router.push(`/listing/create?edit=${pk}`)}
|
||||
onClose={handleClose}
|
||||
onPlaceOrder={orderLoading ? undefined : handlePlaceOrder}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ import {
|
||||
DESCRO_PROGRAM_ADDRESS,
|
||||
findResolverEntryPda,
|
||||
} from '@descro/sdk'
|
||||
import { isSome } from '@solana/kit'
|
||||
import type { Address } from '@solisting/sdk'
|
||||
|
||||
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address
|
||||
|
||||
export default function OrderPage({ params }: { params: Promise<{ pk: string }> }) {
|
||||
const { pk } = use(params)
|
||||
const { account } = useWallet()
|
||||
@@ -31,10 +34,14 @@ export default function OrderPage({ params }: { params: Promise<{ pk: string }>
|
||||
async function handleAccept() {
|
||||
if (!signer || !data) return
|
||||
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({
|
||||
seller: signer,
|
||||
resolver: od.resolver,
|
||||
resolver: resolverKey,
|
||||
listingAccount: od.listing,
|
||||
orderAccount: pk as Address,
|
||||
escrowAccount: od.escrowAccount,
|
||||
|
||||
@@ -26,6 +26,8 @@ interface FormState {
|
||||
price: string
|
||||
quantity: string
|
||||
oracle: string
|
||||
name: string
|
||||
description: string
|
||||
metadataUri: string
|
||||
alts: AltEntry[]
|
||||
resolvers: string[]
|
||||
@@ -39,6 +41,8 @@ function blank(): FormState {
|
||||
price: '',
|
||||
quantity: '',
|
||||
oracle: '',
|
||||
name: '',
|
||||
description: '',
|
||||
metadataUri: '',
|
||||
alts: [],
|
||||
resolvers: [],
|
||||
@@ -87,6 +91,9 @@ function CurrencyToggle({
|
||||
}) {
|
||||
const active = (on: boolean): React.CSSProperties => ({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 11,
|
||||
borderRadius: 10,
|
||||
fontSize: 13,
|
||||
@@ -94,7 +101,7 @@ function CurrencyToggle({
|
||||
cursor: 'pointer',
|
||||
color: on ? '#0b0613' : 'var(--mut)',
|
||||
background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)',
|
||||
border: `1px solid ${on ? 'transparent' : 'var(--bd)'}`,
|
||||
border: `1px solid var(--bd)`,
|
||||
})
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
@@ -145,6 +152,8 @@ export function CreateListingForm({ editPk }: Props) {
|
||||
: (Number(d.price) / 1e9).toString(),
|
||||
quantity: String(d.quantity),
|
||||
oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '',
|
||||
name: d.name ?? '',
|
||||
description: d.description ?? '',
|
||||
metadataUri: d.metadataUri ?? '',
|
||||
alts: d.altCurrencies.map((a) => ({
|
||||
currency:
|
||||
@@ -262,6 +271,8 @@ export function CreateListingForm({ editPk }: Props) {
|
||||
altCurrencies,
|
||||
acceptedResolvers,
|
||||
quantity: quantityN,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
metadataUri: form.metadataUri,
|
||||
})
|
||||
await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing')
|
||||
@@ -275,6 +286,8 @@ export function CreateListingForm({ editPk }: Props) {
|
||||
altCurrencies,
|
||||
acceptedResolvers,
|
||||
quantity: quantityN,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
metadataUri: form.metadataUri,
|
||||
})
|
||||
await sendTx([ix as never], [['listings']], 'create_listing')
|
||||
@@ -334,6 +347,16 @@ export function CreateListingForm({ editPk }: Props) {
|
||||
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 */}
|
||||
<div>
|
||||
<label style={LABEL}>CANONICAL CURRENCY</label>
|
||||
|
||||
@@ -168,7 +168,7 @@ export function Dashboard() {
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 15 }}>
|
||||
{l.data.metadataUri || abbrev(l.address)}
|
||||
{l.data.name || `Listing #${String(l.data.listingId)}`}
|
||||
</span>
|
||||
<Badge color={sc.color} bg={sc.bg}>
|
||||
{l.data.isActive ? 'Active' : 'Inactive'}
|
||||
@@ -182,7 +182,7 @@ export function Dashboard() {
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
110
app/src/components/EscrowDetail.tsx
Normal file
110
app/src/components/EscrowDetail.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -82,8 +82,8 @@ export function EscrowsTable() {
|
||||
borderRadius: 7,
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
color: filter === f.key ? 'var(--tx)' : 'var(--mut)',
|
||||
background: filter === f.key ? 'var(--bg2)' : 'transparent',
|
||||
color: filter === f.key ? '#0b0613' : 'var(--mut)',
|
||||
background: filter === f.key ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -137,7 +137,7 @@ export function EscrowsTable() {
|
||||
return (
|
||||
<div
|
||||
key={e.pda}
|
||||
onClick={() => router.push(`/search?q=${e.pda}`)}
|
||||
onClick={() => router.push(`/escrow/${e.pda}`)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr',
|
||||
|
||||
@@ -54,30 +54,40 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
|
||||
LISTING ACCOUNT · #{String(d.listingId)}
|
||||
</div>
|
||||
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>
|
||||
{d.metadataUri || abbrev(pk)}
|
||||
{d.name || abbrev(pk)}
|
||||
</h1>
|
||||
</div>
|
||||
<Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge>
|
||||
</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 }}>
|
||||
{/* Left: account fields */}
|
||||
<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>
|
||||
<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="Price"><span style={{ fontSize: 13.5, fontWeight: 600 }}>{fmtListingPrice()}</span></FieldRow>
|
||||
<FieldRow label="Currency">
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5 }}>
|
||||
{d.canonicalCurrency.__kind === 'Sol' ? 'SOL (native)' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`}
|
||||
<FieldRow label="Listing ID"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13 }}>#{String(d.listingId)}</span></FieldRow>
|
||||
<FieldRow label="Canonical Currency">
|
||||
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
||||
{d.canonicalCurrency.__kind === 'Sol' ? 'SOL' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`}
|
||||
</span>
|
||||
</FieldRow>
|
||||
{canonicalOracleAddr && (
|
||||
<FieldRow label="Oracle">
|
||||
<MonoChip value={abbrev(canonicalOracleAddr)} onCopy={() => navigator.clipboard.writeText(canonicalOracleAddr)} />
|
||||
<FieldRow label="Price"><span style={{ fontWeight: 700, fontSize: 13.5 }}>{fmtListingPrice()}</span></FieldRow>
|
||||
<FieldRow label="Canonical Oracle">
|
||||
{canonicalOracleAddr
|
||||
? <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="Bump"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--mut)' }}>{d.bump}</span></FieldRow>
|
||||
|
||||
{d.metadataUri && (
|
||||
<>
|
||||
@@ -137,7 +147,7 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
|
||||
<Button variant="danger" onClick={onClose}>Close Listing</Button>
|
||||
</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 && !isSeller && !canPlace && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>No actions available — listing is inactive or out of stock.</div>}
|
||||
</div>
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}>
|
||||
{FILTERS.map((f) => (
|
||||
{FILTERS.map((f) => {
|
||||
const active = filter === f.key
|
||||
return (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setFilter(f.key)}
|
||||
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' }}
|
||||
style={{
|
||||
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>
|
||||
@@ -96,7 +108,7 @@ export function ListingsTable() {
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<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 style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}>
|
||||
#{String(l.data.listingId)} · {abbrev(l.address)}
|
||||
|
||||
@@ -147,6 +147,9 @@ export function Nav() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Spacer — pushes right-side controls to the edge */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Network picker */}
|
||||
<div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}>
|
||||
<button
|
||||
|
||||
@@ -44,7 +44,7 @@ export function OrderDetail({ pk, walletAddress, onAccept, onReject, onCancel, o
|
||||
const od = order.data
|
||||
const ed = escrow?.data
|
||||
|
||||
const stateEnum = ed?.state ?? EscrowState.Cancelled
|
||||
const stateEnum = ed?.state ?? EscrowState.Complete
|
||||
const stateLabel = escrowStateLabel(stateEnum)
|
||||
const sc = STATUS_COLORS[escrowStatusKey(stateEnum)]
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ export function Button({ variant = 'primary', children, style, disabled, ...rest
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '11px 18px',
|
||||
borderRadius: 'var(--radius)',
|
||||
fontWeight: 700,
|
||||
|
||||
@@ -2,24 +2,26 @@
|
||||
|
||||
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() {
|
||||
return useContext(ToastCtx)
|
||||
}
|
||||
|
||||
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) => {
|
||||
setMsg(m)
|
||||
setTimeout(() => setMsg(''), 2800)
|
||||
const show = useCallback<ToastFn>((m, type = 'success') => {
|
||||
setState({ msg: m, type })
|
||||
setTimeout(() => setState(null), type === 'error' ? 4500 : 2800)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ToastCtx.Provider value={show}>
|
||||
{children}
|
||||
{msg && (
|
||||
{state && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
@@ -32,7 +34,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
gap: 10,
|
||||
padding: '12px 18px',
|
||||
background: 'var(--bg2)',
|
||||
border: '1px solid var(--accBd)',
|
||||
border: `1px solid ${state.type === 'error' ? 'var(--errBd, #f87171)' : 'var(--accBd)'}`,
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 16px 40px rgba(0,0,0,.55)',
|
||||
fontSize: 13,
|
||||
@@ -46,12 +48,12 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--acc2)',
|
||||
boxShadow: '0 0 8px var(--acc2)',
|
||||
background: state.type === 'error' ? '#f87171' : 'var(--acc2)',
|
||||
boxShadow: state.type === 'error' ? '0 0 8px #f87171' : '0 0 8px var(--acc2)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{msg}
|
||||
{state.msg}
|
||||
</div>
|
||||
)}
|
||||
</ToastCtx.Provider>
|
||||
|
||||
21
app/src/hooks/useEscrow.ts
Normal file
21
app/src/hooks/useEscrow.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
18
app/src/hooks/useOrderByEscrow.ts
Normal file
18
app/src/hooks/useOrderByEscrow.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -39,10 +39,15 @@ export function useTx() {
|
||||
|
||||
const signed = await signTransactionMessageWithSigners(txMsg)
|
||||
assertIsTransactionWithBlockhashLifetime(signed)
|
||||
try {
|
||||
await sendAndConfirmTransactionFactory({ rpc: rpc as never, rpcSubscriptions: rpcSubscriptions as never })(
|
||||
signed as never,
|
||||
{ commitment: 'confirmed' },
|
||||
)
|
||||
} catch (err) {
|
||||
toast(friendlyTxError(err), 'error')
|
||||
throw err
|
||||
}
|
||||
|
||||
toast(`Tx confirmed — ${label}`)
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
bump = order_account.bump,
|
||||
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
|
||||
close = seller,
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ pub fn handler(
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
@@ -40,6 +42,8 @@ pub fn handler(
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.quantity_reserved = 0;
|
||||
listing.name = name;
|
||||
listing.description = description;
|
||||
listing.metadata_uri = metadata_uri;
|
||||
listing.listing_id = listing_id;
|
||||
listing.is_active = true;
|
||||
|
||||
@@ -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<Pubkey>, payment_currency: Currency)]
|
||||
pub struct CreateOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub buyer: Signer<'info>,
|
||||
@@ -67,7 +67,7 @@ pub fn handler(
|
||||
ctx: Context<CreateOrder>,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
resolver: Option<Pubkey>,
|
||||
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),
|
||||
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,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ pub fn handler(
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
@@ -34,6 +36,8 @@ pub fn handler(
|
||||
listing.alt_currencies = alt_currencies;
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.name = name;
|
||||
listing.description = description;
|
||||
listing.metadata_uri = metadata_uri;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,11 +31,14 @@ pub mod solisting {
|
||||
alt_currencies: Vec<state::AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
create_listing::handler(
|
||||
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>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
update_listing::handler(
|
||||
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>,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
resolver: Option<Pubkey>,
|
||||
payment_currency: state::Currency,
|
||||
expected_amount: u64,
|
||||
max_slippage_bps: u16,
|
||||
|
||||
@@ -52,6 +52,10 @@ pub struct ListingAccount {
|
||||
pub quantity: u32,
|
||||
/// Units held by pending orders. available = quantity - quantity_reserved.
|
||||
pub quantity_reserved: u32,
|
||||
#[max_len(64)]
|
||||
pub name: String,
|
||||
#[max_len(256)]
|
||||
pub description: String,
|
||||
#[max_len(256)]
|
||||
pub metadata_uri: String,
|
||||
pub listing_id: u64,
|
||||
@@ -65,7 +69,7 @@ pub struct OrderAccount {
|
||||
pub listing: Pubkey,
|
||||
pub buyer: Pubkey,
|
||||
pub seller: Pubkey,
|
||||
pub resolver: Pubkey,
|
||||
pub resolver: Option<Pubkey>,
|
||||
/// The currency the buyer chose to pay in.
|
||||
pub payment_currency: Currency,
|
||||
/// Actual amount paid (may differ from price when oracle-converted).
|
||||
|
||||
@@ -101,6 +101,8 @@ pub fn ix_create_listing(
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
@@ -114,6 +116,8 @@ pub fn ix_create_listing(
|
||||
alt_currencies,
|
||||
accepted_resolvers,
|
||||
quantity,
|
||||
name,
|
||||
description,
|
||||
metadata_uri,
|
||||
}
|
||||
.data(),
|
||||
@@ -136,6 +140,8 @@ pub fn ix_update_listing(
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
metadata_uri: String,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
@@ -148,6 +154,8 @@ pub fn ix_update_listing(
|
||||
alt_currencies,
|
||||
accepted_resolvers,
|
||||
quantity,
|
||||
name,
|
||||
description,
|
||||
metadata_uri,
|
||||
}
|
||||
.data(),
|
||||
@@ -187,7 +195,6 @@ pub fn ix_create_order(
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let resolver = Pubkey::new_unique();
|
||||
ix_create_order_with_resolver(
|
||||
buyer,
|
||||
seller,
|
||||
@@ -197,7 +204,7 @@ pub fn ix_create_order(
|
||||
max_slippage_bps,
|
||||
canonical_oracle,
|
||||
target_oracle,
|
||||
resolver,
|
||||
Some(Pubkey::new_unique()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,7 +218,7 @@ pub fn ix_create_order_with_resolver(
|
||||
max_slippage_bps: u16,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
resolver: Pubkey,
|
||||
resolver: Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let listing = listing_pda(seller, listing_id);
|
||||
let order = order_pda(listing, *buyer, order_id);
|
||||
@@ -256,7 +263,7 @@ pub fn ix_accept_order(
|
||||
buyer: &Pubkey,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
resolver: Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_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::accounts::AcceptOrder {
|
||||
seller: *seller,
|
||||
resolver,
|
||||
resolver: resolver.unwrap_or(system_program::ID),
|
||||
listing_account,
|
||||
order_account,
|
||||
escrow_account,
|
||||
|
||||
@@ -17,7 +17,7 @@ fn seller_can_create_sol_only_listing() {
|
||||
vec![],
|
||||
vec![],
|
||||
10,
|
||||
"ipfs://test".to_string(),
|
||||
"".to_string(), "".to_string(), "ipfs://test".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
@@ -48,7 +48,7 @@ fn seller_can_create_listing_with_usdc_alt_stablecoin() {
|
||||
}],
|
||||
vec![],
|
||||
5,
|
||||
"ipfs://x".to_string(),
|
||||
"".to_string(), "".to_string(), "ipfs://x".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
@@ -75,7 +75,7 @@ fn seller_can_create_listing_with_bonk_alt_oracle() {
|
||||
}],
|
||||
vec![],
|
||||
5,
|
||||
"".to_string(),
|
||||
"".to_string(), "".to_string(), "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
@@ -90,7 +90,7 @@ fn seller_can_update_listing() {
|
||||
let listing_id: u64 = 2;
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
|
||||
@@ -103,7 +103,7 @@ fn seller_can_update_listing() {
|
||||
vec![],
|
||||
vec![],
|
||||
20,
|
||||
"ipfs://new".to_string(),
|
||||
"".to_string(), "".to_string(), "ipfs://new".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix_update], &[&seller]);
|
||||
|
||||
@@ -118,7 +118,7 @@ fn seller_can_close_listing() {
|
||||
let listing_id: u64 = 3;
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ fn buyer_can_create_order_canonical_sol() {
|
||||
let price = 100_000_000u64;
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
|
||||
@@ -42,7 +42,7 @@ fn create_order_fails_if_listing_inactive() {
|
||||
let listing_id = 2u64;
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
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 ix = ix_create_listing(
|
||||
&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(
|
||||
@@ -87,7 +87,7 @@ fn create_order_fails_if_currency_not_accepted() {
|
||||
let listing_id = 4u64;
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
|
||||
@@ -110,7 +110,7 @@ fn create_order_fails_if_resolver_not_accepted() {
|
||||
let allowed_resolver = Pubkey::new_unique();
|
||||
let ix = ix_create_listing(
|
||||
&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]);
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
@@ -132,7 +132,7 @@ fn seller_accept_creates_active_descro_escrow() {
|
||||
let price = 100_000_000u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
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);
|
||||
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);
|
||||
@@ -166,7 +166,7 @@ fn seller_can_reject_order() {
|
||||
let price = 100_000_000u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
send(
|
||||
@@ -197,7 +197,7 @@ fn buyer_can_cancel_order() {
|
||||
let listing_id = 21u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
send(
|
||||
@@ -223,7 +223,7 @@ fn reject_handles_already_cancelled_escrow() {
|
||||
let listing_id = 22u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
send(
|
||||
@@ -250,7 +250,7 @@ fn anyone_can_close_stale_order_after_terminal_escrow() {
|
||||
let listing_id = 30u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
send(
|
||||
@@ -284,7 +284,7 @@ fn close_stale_order_fails_if_escrow_still_active() {
|
||||
let listing_id = 31u64;
|
||||
send(
|
||||
&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],
|
||||
);
|
||||
send(
|
||||
|
||||
@@ -96,6 +96,8 @@ export type SolistingStateListingAccount = {
|
||||
quantity: number;
|
||||
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
||||
quantityReserved: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
listingId: bigint;
|
||||
isActive: boolean;
|
||||
@@ -125,6 +127,8 @@ export type SolistingStateListingAccountArgs = {
|
||||
quantity: number;
|
||||
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
||||
quantityReserved: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
listingId: number | bigint;
|
||||
isActive: boolean;
|
||||
@@ -147,6 +151,8 @@ export function getSolistingStateListingAccountEncoder(): Encoder<SolistingState
|
||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||
["quantity", getU32Encoder()],
|
||||
["quantityReserved", getU32Encoder()],
|
||||
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["listingId", getU64Encoder()],
|
||||
["isActive", getBooleanEncoder()],
|
||||
@@ -174,6 +180,8 @@ export function getSolistingStateListingAccountDecoder(): Decoder<SolistingState
|
||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||
["quantity", getU32Decoder()],
|
||||
["quantityReserved", getU32Decoder()],
|
||||
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["listingId", getU64Decoder()],
|
||||
["isActive", getBooleanDecoder()],
|
||||
|
||||
@@ -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<Address>;
|
||||
/** 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<Address>;
|
||||
/** 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<SolistingStateOr
|
||||
["listing", getAddressEncoder()],
|
||||
["buyer", getAddressEncoder()],
|
||||
["seller", getAddressEncoder()],
|
||||
["resolver", getAddressEncoder()],
|
||||
["resolver", getOptionEncoder(getAddressEncoder())],
|
||||
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
||||
["amount", getU64Encoder()],
|
||||
["escrowAccount", getAddressEncoder()],
|
||||
@@ -124,7 +128,7 @@ export function getSolistingStateOrderAccountDecoder(): Decoder<SolistingStateOr
|
||||
["listing", getAddressDecoder()],
|
||||
["buyer", getAddressDecoder()],
|
||||
["seller", getAddressDecoder()],
|
||||
["resolver", getAddressDecoder()],
|
||||
["resolver", getOptionDecoder(getAddressDecoder())],
|
||||
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
||||
["amount", getU64Decoder()],
|
||||
["escrowAccount", getAddressDecoder()],
|
||||
|
||||
@@ -111,6 +111,8 @@ export type CreateListingInstructionData = {
|
||||
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
||||
acceptedResolvers: Array<Address>;
|
||||
quantity: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
};
|
||||
|
||||
@@ -122,6 +124,8 @@ export type CreateListingInstructionDataArgs = {
|
||||
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
||||
acceptedResolvers: Array<Address>;
|
||||
quantity: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
};
|
||||
|
||||
@@ -139,6 +143,8 @@ export function getCreateListingInstructionDataEncoder(): Encoder<CreateListingI
|
||||
],
|
||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||
["quantity", getU32Encoder()],
|
||||
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
]),
|
||||
(value) => ({ ...value, discriminator: CREATE_LISTING_DISCRIMINATOR }),
|
||||
@@ -158,6 +164,8 @@ export function getCreateListingInstructionDataDecoder(): Decoder<CreateListingI
|
||||
],
|
||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||
["quantity", getU32Decoder()],
|
||||
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
]);
|
||||
}
|
||||
@@ -187,6 +195,8 @@ export type CreateListingAsyncInput<
|
||||
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
||||
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
||||
quantity: CreateListingInstructionDataArgs["quantity"];
|
||||
name: CreateListingInstructionDataArgs["name"];
|
||||
description: CreateListingInstructionDataArgs["description"];
|
||||
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
||||
};
|
||||
|
||||
@@ -279,6 +289,8 @@ export type CreateListingInput<
|
||||
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
||||
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
||||
quantity: CreateListingInstructionDataArgs["quantity"];
|
||||
name: CreateListingInstructionDataArgs["name"];
|
||||
description: CreateListingInstructionDataArgs["description"];
|
||||
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
getAddressEncoder,
|
||||
getBytesDecoder,
|
||||
getBytesEncoder,
|
||||
getOptionDecoder,
|
||||
getOptionEncoder,
|
||||
getProgramDerivedAddress,
|
||||
getStructDecoder,
|
||||
getStructEncoder,
|
||||
@@ -33,6 +35,8 @@ import {
|
||||
type Instruction,
|
||||
type InstructionWithAccounts,
|
||||
type InstructionWithData,
|
||||
type Option,
|
||||
type OptionOrNullable,
|
||||
type ReadonlyAccount,
|
||||
type ReadonlyUint8Array,
|
||||
type TransactionSigner,
|
||||
@@ -121,7 +125,7 @@ export type CreateOrderInstructionData = {
|
||||
discriminator: ReadonlyUint8Array;
|
||||
orderId: bigint;
|
||||
escrowId: bigint;
|
||||
resolver: Address;
|
||||
resolver: Option<Address>;
|
||||
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<Address>;
|
||||
paymentCurrency: SolistingStateCurrencyArgs;
|
||||
expectedAmount: number | bigint;
|
||||
maxSlippageBps: number;
|
||||
@@ -142,7 +146,7 @@ export function getCreateOrderInstructionDataEncoder(): Encoder<CreateOrderInstr
|
||||
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
|
||||
["orderId", getU64Encoder()],
|
||||
["escrowId", getU64Encoder()],
|
||||
["resolver", getAddressEncoder()],
|
||||
["resolver", getOptionEncoder(getAddressEncoder())],
|
||||
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
||||
["expectedAmount", getU64Encoder()],
|
||||
["maxSlippageBps", getU16Encoder()],
|
||||
@@ -156,7 +160,7 @@ export function getCreateOrderInstructionDataDecoder(): Decoder<CreateOrderInstr
|
||||
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
||||
["orderId", getU64Decoder()],
|
||||
["escrowId", getU64Decoder()],
|
||||
["resolver", getAddressDecoder()],
|
||||
["resolver", getOptionDecoder(getAddressDecoder())],
|
||||
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
||||
["expectedAmount", getU64Decoder()],
|
||||
["maxSlippageBps", getU16Decoder()],
|
||||
|
||||
@@ -101,6 +101,8 @@ export type UpdateListingInstructionData = {
|
||||
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
||||
acceptedResolvers: Array<Address>;
|
||||
quantity: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
};
|
||||
|
||||
@@ -111,6 +113,8 @@ export type UpdateListingInstructionDataArgs = {
|
||||
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
||||
acceptedResolvers: Array<Address>;
|
||||
quantity: number;
|
||||
name: string;
|
||||
description: string;
|
||||
metadataUri: string;
|
||||
};
|
||||
|
||||
@@ -127,6 +131,8 @@ export function getUpdateListingInstructionDataEncoder(): Encoder<UpdateListingI
|
||||
],
|
||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||
["quantity", getU32Encoder()],
|
||||
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
]),
|
||||
(value) => ({ ...value, discriminator: UPDATE_LISTING_DISCRIMINATOR }),
|
||||
@@ -145,6 +151,8 @@ export function getUpdateListingInstructionDataDecoder(): Decoder<UpdateListingI
|
||||
],
|
||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||
["quantity", getU32Decoder()],
|
||||
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
]);
|
||||
}
|
||||
@@ -171,6 +179,8 @@ export type UpdateListingInput<
|
||||
altCurrencies: UpdateListingInstructionDataArgs["altCurrencies"];
|
||||
acceptedResolvers: UpdateListingInstructionDataArgs["acceptedResolvers"];
|
||||
quantity: UpdateListingInstructionDataArgs["quantity"];
|
||||
name: UpdateListingInstructionDataArgs["name"];
|
||||
description: UpdateListingInstructionDataArgs["description"];
|
||||
metadataUri: UpdateListingInstructionDataArgs["metadataUri"];
|
||||
};
|
||||
|
||||
|
||||
@@ -115,6 +115,14 @@
|
||||
"name": "quantity",
|
||||
"type": "u32"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
@@ -208,6 +216,14 @@
|
||||
"name": "quantity",
|
||||
"type": "u32"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
@@ -1337,6 +1353,14 @@
|
||||
],
|
||||
"type": "u32"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
|
||||
@@ -7,7 +7,7 @@ export * from './listing'
|
||||
// Order exports are imported but we exclude the re-exported deriveEscrowId to avoid duplication
|
||||
// since it's already exported from pda.js
|
||||
export type { OrderAccountWithPda } from './order'
|
||||
export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrder } from './order'
|
||||
export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrderByEscrow, fetchOrder } from './order'
|
||||
|
||||
// Re-export Address for consumers
|
||||
export type { Address, Account } from '@solana/kit'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { address, type Account, type Address, type Rpc } from '@solana/kit'
|
||||
import { type GetProgramAccountsApi } 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 {
|
||||
decodeSolistingStateOrderAccount,
|
||||
fetchSolistingStateOrderAccount,
|
||||
SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR,
|
||||
getSolistingStateOrderAccountDiscriminatorBytes,
|
||||
type SolistingStateOrderAccount,
|
||||
} from './generated/solisting/src/generated/index'
|
||||
import { deriveEscrowId } from './pda'
|
||||
@@ -16,22 +16,57 @@ export type OrderAccountWithPda = Account<SolistingStateOrderAccount>
|
||||
type GpaRpc = Rpc<GetProgramAccountsApi>
|
||||
type GetRpc = Rpc<GetAccountInfoApi>
|
||||
|
||||
async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
|
||||
const disc = SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR
|
||||
const discBase64 = Buffer.from(disc).toString('base64') as Base64EncodedBytes
|
||||
// OrderAccount byte layout (Sol-currency variant, the only one currently implemented):
|
||||
// 0: discriminator (8)
|
||||
// 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
|
||||
.getProgramAccounts(PROGRAM_ADDRESS, {
|
||||
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()
|
||||
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'))
|
||||
return decodeSolistingStateOrderAccount({
|
||||
address: r.pubkey,
|
||||
data,
|
||||
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,
|
||||
space: r.account.space,
|
||||
exists: true,
|
||||
@@ -39,26 +74,20 @@ async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchOrdersForListing(
|
||||
rpc: GpaRpc,
|
||||
listingPk: Address,
|
||||
): Promise<OrderAccountWithPda[]> {
|
||||
const all = await fetchAllOrders(rpc)
|
||||
return all.filter((o) => o.data.listing === listingPk)
|
||||
export function fetchOrdersForListing(rpc: GpaRpc, listingPk: Address): Promise<OrderAccountWithPda[]> {
|
||||
return gpa(rpc, { offset: LISTING_OFFSET, addr: listingPk })
|
||||
}
|
||||
|
||||
export async function fetchOrdersByBuyer(
|
||||
rpc: GpaRpc,
|
||||
buyer: Address,
|
||||
): Promise<OrderAccountWithPda[]> {
|
||||
const all = await fetchAllOrders(rpc)
|
||||
return all.filter((o) => o.data.buyer === buyer)
|
||||
export function fetchOrdersByBuyer(rpc: GpaRpc, buyer: Address): Promise<OrderAccountWithPda[]> {
|
||||
return gpa(rpc, { offset: BUYER_OFFSET, addr: buyer })
|
||||
}
|
||||
|
||||
export async function fetchOrder(
|
||||
rpc: GetRpc,
|
||||
addr: Address,
|
||||
): Promise<OrderAccountWithPda | null> {
|
||||
export async function fetchOrderByEscrow(rpc: GpaRpc, escrowPda: Address): Promise<OrderAccountWithPda | null> {
|
||||
const results = await gpa(rpc, { offset: ESCROW_ACCOUNT_OFFSET, addr: escrowPda })
|
||||
return results[0] ?? null
|
||||
}
|
||||
|
||||
export async function fetchOrder(rpc: GetRpc, addr: Address): Promise<OrderAccountWithPda | null> {
|
||||
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user