From 0c86751d519be1a0421ef120c45c41ffd230af1c Mon Sep 17 00:00:00 2001
From: thesn10 <38666407+thesn10@users.noreply.github.com>
Date: Fri, 26 Jun 2026 20:01:36 +0200
Subject: [PATCH] fix escrow handling and fetching
---
app/src/app/escrow/[pk]/page.tsx | 8 ++
app/src/components/EscrowDetail.tsx | 110 ++++++++++++++++++++++++++++
app/src/components/EscrowsTable.tsx | 2 +-
app/src/hooks/useEscrow.ts | 21 ++++++
app/src/hooks/useOrderByEscrow.ts | 18 +++++
sdk/src/index.ts | 2 +-
sdk/src/order.ts | 77 +++++++++++++------
7 files changed, 212 insertions(+), 26 deletions(-)
create mode 100644 app/src/app/escrow/[pk]/page.tsx
create mode 100644 app/src/components/EscrowDetail.tsx
create mode 100644 app/src/hooks/useEscrow.ts
create mode 100644 app/src/hooks/useOrderByEscrow.ts
diff --git a/app/src/app/escrow/[pk]/page.tsx b/app/src/app/escrow/[pk]/page.tsx
new file mode 100644
index 0000000..49c302c
--- /dev/null
+++ b/app/src/app/escrow/[pk]/page.tsx
@@ -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
+}
diff --git a/app/src/components/EscrowDetail.tsx b/app/src/components/EscrowDetail.tsx
new file mode 100644
index 0000000..211d741
--- /dev/null
+++ b/app/src/components/EscrowDetail.tsx
@@ -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
Loading…
+ if (error || !escrow) return Escrow account not found.
+
+ 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 (
+
+
+
+
+
+
+ ESCROW ACCOUNT · #{String(ed.escrowId)}
+
+
{abbrev(pda)}
+
+
{stateLabel}
+
+
+
+
+
+
+ ESCROW ACCOUNT · descro
+
+
+
+ navigator.clipboard.writeText(pda)} />
+
+
+ {stateLabel}
+
+
+ #{String(ed.escrowId)}
+
+
+ navigator.clipboard.writeText(ed.seller)} />
+
+
+ navigator.clipboard.writeText(ed.buyer)} />
+
+
+ {fmtSol(ed.amount)}
+
+ {resolverPk && (
+
+ router.push(`/resolver/${resolverPk}`)}
+ onCopy={() => navigator.clipboard.writeText(resolverPk)}
+ />
+
+ )}
+
+ {order
+ ? (
+ router.push(`/order/${order.address}`)}
+ onCopy={() => navigator.clipboard.writeText(order.address)}
+ />
+ )
+ : —
+ }
+
+
+
+ )
+}
diff --git a/app/src/components/EscrowsTable.tsx b/app/src/components/EscrowsTable.tsx
index 04264f7..fe1fbb6 100644
--- a/app/src/components/EscrowsTable.tsx
+++ b/app/src/components/EscrowsTable.tsx
@@ -137,7 +137,7 @@ export function EscrowsTable() {
return (
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',
diff --git a/app/src/hooks/useEscrow.ts b/app/src/hooks/useEscrow.ts
new file mode 100644
index 0000000..1fe42df
--- /dev/null
+++ b/app/src/hooks/useEscrow.ts
@@ -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[0],
+ pda,
+ ).catch(() => null)
+ },
+ enabled: !!cluster && !!pda,
+ refetchInterval: 15_000,
+ })
+}
diff --git a/app/src/hooks/useOrderByEscrow.ts b/app/src/hooks/useOrderByEscrow.ts
new file mode 100644
index 0000000..3cb2739
--- /dev/null
+++ b/app/src/hooks/useOrderByEscrow.ts
@@ -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,
+ })
+}
diff --git a/sdk/src/index.ts b/sdk/src/index.ts
index 67b32d8..c501c5d 100644
--- a/sdk/src/index.ts
+++ b/sdk/src/index.ts
@@ -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'
diff --git a/sdk/src/order.ts b/sdk/src/order.ts
index 03d0c14..4cf4fb9 100644
--- a/sdk/src/order.ts
+++ b/sdk/src/order.ts
@@ -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
type GpaRpc = Rpc
type GetRpc = Rpc
-async function fetchAllOrders(rpc: GpaRpc): Promise {
- 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 (32)
+// 136: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total
+// 137: amount (8)
+// 145: escrowAccount (32)
+const LISTING_OFFSET = 8n
+const BUYER_OFFSET = 40n
+// escrowAccount offset is only valid for Sol-currency orders.
+// SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 178.
+const ESCROW_ACCOUNT_OFFSET = 145n
+
+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 {
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 {
})
}
-export async function fetchOrdersForListing(
- rpc: GpaRpc,
- listingPk: Address,
-): Promise {
- const all = await fetchAllOrders(rpc)
- return all.filter((o) => o.data.listing === listingPk)
+export function fetchOrdersForListing(rpc: GpaRpc, listingPk: Address): Promise {
+ return gpa(rpc, { offset: LISTING_OFFSET, addr: listingPk })
}
-export async function fetchOrdersByBuyer(
- rpc: GpaRpc,
- buyer: Address,
-): Promise {
- const all = await fetchAllOrders(rpc)
- return all.filter((o) => o.data.buyer === buyer)
+export function fetchOrdersByBuyer(rpc: GpaRpc, buyer: Address): Promise {
+ return gpa(rpc, { offset: BUYER_OFFSET, addr: buyer })
}
-export async function fetchOrder(
- rpc: GetRpc,
- addr: Address,
-): Promise {
+export async function fetchOrderByEscrow(rpc: GpaRpc, escrowPda: Address): Promise {
+ const results = await gpa(rpc, { offset: ESCROW_ACCOUNT_OFFSET, addr: escrowPda })
+ return results[0] ?? null
+}
+
+export async function fetchOrder(rpc: GetRpc, addr: Address): Promise {
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
}