From ef7dca34f9b2d52b1ee7e3d2c52924a3e5d956b0 Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:34:39 +0200 Subject: [PATCH] feat: escrows view --- app/src/app/escrows/page.tsx | 5 + app/src/components/EscrowsTable.tsx | 177 ++++++++++++++++++++++++++++ app/src/hooks/useEscrows.ts | 63 ++++++++++ 3 files changed, 245 insertions(+) create mode 100644 app/src/app/escrows/page.tsx create mode 100644 app/src/components/EscrowsTable.tsx create mode 100644 app/src/hooks/useEscrows.ts diff --git a/app/src/app/escrows/page.tsx b/app/src/app/escrows/page.tsx new file mode 100644 index 0000000..778633a --- /dev/null +++ b/app/src/app/escrows/page.tsx @@ -0,0 +1,5 @@ +import { EscrowsTable } from '@/components/EscrowsTable' + +export default function EscrowsPage() { + return +} diff --git a/app/src/components/EscrowsTable.tsx b/app/src/components/EscrowsTable.tsx new file mode 100644 index 0000000..4807fe3 --- /dev/null +++ b/app/src/components/EscrowsTable.tsx @@ -0,0 +1,177 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { isSome } from '@solana/kit' +import { useEscrows } from '@/hooks/useEscrows' +import { Badge, STATUS_COLORS } from '@/components/ui/Badge' +import { abbrev, fmtSol } from '@/lib/format' +import { escrowStateLabel } from '@descro/sdk' +import { EscrowState } from '@descro/sdk/src/generated/descro/src/generated/types/escrowState' + +type FilterKey = 'all' | 'awaiting' | 'active' | 'disputed' | 'complete' | 'cancelled' + +const FILTERS: { key: FilterKey; label: string; state?: EscrowState }[] = [ + { key: 'all', label: 'All' }, + { key: 'awaiting', label: 'Awaiting', state: EscrowState.AwaitingSellerConfirm }, + { key: 'active', label: 'Active', state: EscrowState.Active }, + { key: 'disputed', label: 'Disputed', state: EscrowState.Disputed }, + { key: 'complete', label: 'Complete', state: EscrowState.Complete }, + { key: 'cancelled', label: 'Cancelled', state: EscrowState.Cancelled }, +] + +function stateToStatusKey(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' + case EscrowState.AwaitingDeposit: + case EscrowState.Cancelled: + default: + return 'cancelled' + } +} + +export function EscrowsTable() { + const router = useRouter() + const { data: escrows = [], isLoading, error } = useEscrows() + const [filter, setFilter] = useState('all') + + if (isLoading) { + return
Loading escrows…
+ } + if (error) { + return
{String(error)}
+ } + + const rows = escrows.filter((e) => { + if (filter === 'all') return true + const f = FILTERS.find((f) => f.key === filter) + if (!f || f.state === undefined) return true + return e.account.state === f.state + }) + + return ( +
+ {/* Header */} +
+
+

Escrows

+

+ {escrows.length} EscrowAccount PDAs · descro program +

+
+ + {/* Filter tabs */} +
+ {FILTERS.map((f) => { + const count = f.key === 'all' + ? escrows.length + : escrows.filter((e) => e.account.state === f.state).length + return ( + + ) + })} +
+
+ + {/* Table */} +
+
+
ESCROW ID
+
SELLER
+
BUYER
+
AMOUNT
+
RESOLVER
+
STATE
+
+ + {rows.length === 0 && ( +
+ No escrows match these filters. +
+ )} + + {rows.map((e) => { + const statusKey = stateToStatusKey(e.account.state) + const sc = STATUS_COLORS[statusKey] + const resolverDisplay = isSome(e.account.disputeResolver) + ? abbrev(e.account.disputeResolver.value) + : '—' + return ( +
router.push(`/search?q=${e.pda}`)} + style={{ + display: 'grid', + gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr', + gap: 14, + padding: '15px 20px', + borderBottom: '1px solid var(--bdSoft)', + cursor: 'pointer', + alignItems: 'center', + }} + > +
+ #{String(e.account.escrowId)} +
+
+ {abbrev(e.account.seller)} +
+
+ {abbrev(e.account.buyer)} +
+
+ {fmtSol(e.account.amount)} +
+
+ {resolverDisplay} +
+
+ + {escrowStateLabel(e.account.state)} + +
+
+ ) + })} +
+
+ ) +} diff --git a/app/src/hooks/useEscrows.ts b/app/src/hooks/useEscrows.ts new file mode 100644 index 0000000..f19aa4d --- /dev/null +++ b/app/src/hooks/useEscrows.ts @@ -0,0 +1,63 @@ +import { useQuery } from '@tanstack/react-query' +import { useCluster } from '@solana/connector/react' +import { createSolanaRpc } from '@solana/kit' +import type { Address, Lamports } from '@solana/kit' +import { + decodeEscrowAccount, + getEscrowAccountDiscriminatorBytes, +} from '@descro/sdk/src/generated/descro/src/generated/accounts/escrowAccount' +import { DESCRO_PROGRAM_ADDRESS } from '@descro/sdk/src/generated/descro/src/generated/programs/descro' +import type { EscrowAccountWithPda } from '@descro/sdk' + +async function fetchAllEscrows( + rpc: ReturnType, +): Promise { + const discriminatorBase64 = btoa( + String.fromCharCode(...getEscrowAccountDiscriminatorBytes()), + ) as never + + const results = await rpc + .getProgramAccounts(DESCRO_PROGRAM_ADDRESS, { + encoding: 'base64', + filters: [{ memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: 'base64' } }], + }) + .send() + + return ( + results as Array<{ + pubkey: Address + account: { + executable: boolean + lamports: bigint + owner: Address + space: bigint + data: [string, 'base64'] + } + }> + ).flatMap((r) => { + const rawBytes = Uint8Array.from(atob(r.account.data[0]), (c) => c.charCodeAt(0)) + const decoded = decodeEscrowAccount({ + address: r.pubkey, + data: rawBytes, + executable: r.account.executable, + lamports: r.account.lamports as Lamports, + programAddress: r.account.owner, + space: r.account.space, + exists: true, + }) + return decoded.exists ? [{ pda: r.pubkey, account: decoded.data }] : [] + }) +} + +export function useEscrows() { + const { cluster } = useCluster() + return useQuery({ + queryKey: ['escrows', cluster?.id], + queryFn: () => { + const rpc = createSolanaRpc(cluster!.url) + return fetchAllEscrows(rpc) + }, + enabled: !!cluster, + refetchInterval: 30_000, + }) +}