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"
|
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"
|
||||||
|
|||||||
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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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,
|
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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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)
|
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.'
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
|||||||
@@ -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"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
|||||||
@@ -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"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user