descro playgound & sdk
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,5 +6,7 @@ node_modules
|
||||
test-ledger
|
||||
.yarn
|
||||
.surfpool
|
||||
.vscode
|
||||
|
||||
spec.md
|
||||
.vscode
|
||||
app_old
|
||||
2126
.pnp.loader.mjs
generated
2126
.pnp.loader.mjs
generated
File diff suppressed because it is too large
Load Diff
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
4
app/.gitignore
vendored
Normal file
4
app/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.next/
|
||||
node_modules/
|
||||
public/tamagui.generated.css
|
||||
.env*.local
|
||||
1
app/.yarnrc.yml
Normal file
1
app/.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
6
app/next-env.d.ts
vendored
Normal file
6
app/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
27
app/next.config.ts
Normal file
27
app/next.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: [
|
||||
'@tamagui/lucide-icons-2',
|
||||
'@descro/sdk',
|
||||
'@solana/wallet-adapter-react-ui',
|
||||
],
|
||||
turbopack: {
|
||||
resolveAlias: {
|
||||
'react-native': 'react-native-web',
|
||||
'react-native-svg': '@tamagui/react-native-svg',
|
||||
},
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
os: false,
|
||||
path: false,
|
||||
crypto: false,
|
||||
}
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
32
app/package.json
Normal file
32
app/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "descro-app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "tamagui build --target web ./src -- next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.32.1",
|
||||
"@descro/sdk": "workspace:*",
|
||||
"@solana/wallet-adapter-base": "^0.9.27",
|
||||
"@solana/wallet-adapter-phantom": "^0.9.29",
|
||||
"@solana/wallet-adapter-react": "^0.15.39",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.39",
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"@tamagui/config": "^2.0.0-rc.42",
|
||||
"@tamagui/next-theme": "^2.0.0-rc.42",
|
||||
"next": "^16.2.6",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-native-web": "^0.19.13",
|
||||
"tamagui": "^2.0.0-rc.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tamagui/cli": "^2.0.0-rc.42",
|
||||
"@types/node": "^25.9.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
23
app/src/app/NextTamaguiProvider.tsx
Normal file
23
app/src/app/NextTamaguiProvider.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
|
||||
import { TamaguiProvider } from 'tamagui'
|
||||
import tamaguiConfig from '../../tamagui.config'
|
||||
|
||||
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [theme, setTheme] = useRootTheme()
|
||||
|
||||
return (
|
||||
<NextThemeProvider
|
||||
skipNextHead
|
||||
onChangeTheme={(next) => {
|
||||
setTheme(next as any)
|
||||
}}
|
||||
>
|
||||
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass defaultTheme={theme}>
|
||||
{children}
|
||||
</TamaguiProvider>
|
||||
</NextThemeProvider>
|
||||
)
|
||||
}
|
||||
46
app/src/app/WalletProviders.tsx
Normal file
46
app/src/app/WalletProviders.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'
|
||||
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'
|
||||
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'
|
||||
|
||||
export type Network = 'devnet' | 'localnet'
|
||||
|
||||
interface NetworkContextValue {
|
||||
network: Network
|
||||
setNetwork: (n: Network) => void
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export const NetworkContext = React.createContext<NetworkContextValue>({
|
||||
network: 'devnet',
|
||||
setNetwork: () => undefined,
|
||||
endpoint: 'https://api.devnet.solana.com',
|
||||
})
|
||||
|
||||
export function getEndpoint(network: Network): string {
|
||||
if (network === 'localnet') return 'http://localhost:8899'
|
||||
return 'https://api.devnet.solana.com'
|
||||
}
|
||||
|
||||
export function WalletProviders({ children }: { children: React.ReactNode }) {
|
||||
const [network, setNetwork] = useState<Network>('devnet')
|
||||
const endpoint = getEndpoint(network)
|
||||
|
||||
const wallets = useMemo(
|
||||
() => [new PhantomWalletAdapter()],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[network]
|
||||
)
|
||||
|
||||
return (
|
||||
<NetworkContext.Provider value={{ network, setNetwork, endpoint }}>
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<WalletProvider wallets={wallets} autoConnect>
|
||||
<WalletModalProvider>{children}</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
</NetworkContext.Provider>
|
||||
)
|
||||
}
|
||||
23
app/src/app/layout.tsx
Normal file
23
app/src/app/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import '../../public/tamagui.generated.css'
|
||||
import '@solana/wallet-adapter-react-ui/styles.css'
|
||||
import { Metadata } from 'next'
|
||||
import { NextTamaguiProvider } from './NextTamaguiProvider'
|
||||
import { WalletProviders } from './WalletProviders'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Descro Playground',
|
||||
description: 'Descro Escrow Protocol Playground',
|
||||
icons: '/favicon.ico',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{ margin: 0, padding: 0, fontFamily: 'system-ui, sans-serif' }}>
|
||||
<NextTamaguiProvider>
|
||||
<WalletProviders>{children}</WalletProviders>
|
||||
</NextTamaguiProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
184
app/src/app/page.tsx
Normal file
184
app/src/app/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client'
|
||||
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { YStack, XStack, Text, Button, Spinner, Separator } from 'tamagui'
|
||||
import { WalletButton } from '@/components/WalletButton'
|
||||
import { CreateEscrowForm } from '@/components/CreateEscrowForm'
|
||||
import { EscrowCard } from '@/components/EscrowCard'
|
||||
import { EscrowDetail } from '@/components/EscrowDetail'
|
||||
import { ResolverPanel } from '@/components/ResolverPanel'
|
||||
import { useEscrows } from '@/hooks/useEscrows'
|
||||
import { NetworkContext } from './WalletProviders'
|
||||
import type { Network } from './WalletProviders'
|
||||
import type { EscrowAccountWithPda } from '@descro/sdk'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
|
||||
export default function PlaygroundPage() {
|
||||
const { network, setNetwork } = useContext(NetworkContext)
|
||||
const { publicKey } = useWallet()
|
||||
const { escrows, loading, error, refresh } = useEscrows()
|
||||
const [selected, setSelected] = useState<EscrowAccountWithPda | null>(null)
|
||||
const [showResolvers, setShowResolvers] = useState(false)
|
||||
|
||||
return (
|
||||
<YStack flex={1} minHeight="100vh" backgroundColor="$background">
|
||||
{/* Header */}
|
||||
<XStack
|
||||
padding="$4"
|
||||
borderBottomWidth={1}
|
||||
borderColor="$borderColor"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
flexWrap="wrap"
|
||||
gap="$3"
|
||||
backgroundColor="$backgroundStrong"
|
||||
>
|
||||
<Text fontSize={22} fontWeight="800" color="$blue9">
|
||||
Descro Playground
|
||||
</Text>
|
||||
|
||||
<XStack gap="$3" alignItems="center" flexWrap="wrap">
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Text fontSize={13} color="$color10">
|
||||
Network:
|
||||
</Text>
|
||||
<select
|
||||
value={network}
|
||||
onChange={(e) => setNetwork(e.target.value as Network)}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #ccc',
|
||||
fontSize: 13,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="devnet">Devnet</option>
|
||||
<option value="localnet">Localnet</option>
|
||||
</select>
|
||||
</XStack>
|
||||
|
||||
{network === 'devnet' && (
|
||||
<a
|
||||
href="https://faucet.solana.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontSize: 13, color: '#512da8', textDecoration: 'none' }}
|
||||
>
|
||||
Devnet Faucet ↗
|
||||
</a>
|
||||
)}
|
||||
|
||||
<WalletButton />
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
{/* Main content */}
|
||||
<XStack flex={1} padding="$4" gap="$4" flexWrap="wrap" alignItems="flex-start">
|
||||
{/* Left panel */}
|
||||
<YStack
|
||||
flex={1}
|
||||
minWidth={300}
|
||||
maxWidth={420}
|
||||
gap="$4"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
borderRadius="$4"
|
||||
padding="$4"
|
||||
>
|
||||
<CreateEscrowForm onCreated={refresh} />
|
||||
|
||||
<Separator />
|
||||
|
||||
<YStack gap="$3">
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize={15} fontWeight="600">
|
||||
Your Escrows
|
||||
</Text>
|
||||
<Button size="$2" onPress={refresh} disabled={loading}>
|
||||
{loading ? <Spinner size="small" /> : 'Refresh'}
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{!publicKey && (
|
||||
<Text color="$color10" fontSize={13}>
|
||||
Connect your wallet to see escrows.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<XStack backgroundColor="$red3" padding="$2" borderRadius="$2">
|
||||
<Text color="$red9" fontSize={13}>
|
||||
{error}
|
||||
</Text>
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
{publicKey && !loading && escrows.length === 0 && (
|
||||
<Text color="$color10" fontSize={13}>
|
||||
No escrows found for your wallet.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<YStack gap="$2">
|
||||
{escrows.map((item) => (
|
||||
<EscrowCard
|
||||
key={item.pda.toBase58()}
|
||||
item={item}
|
||||
selected={selected?.pda.equals(item.pda) ?? false}
|
||||
onSelect={() => setSelected(item)}
|
||||
/>
|
||||
))}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
{/* Right panel */}
|
||||
<YStack flex={2} minWidth={300} gap="$4">
|
||||
{selected ? (
|
||||
<EscrowDetail item={selected} onAction={refresh} />
|
||||
) : (
|
||||
<YStack
|
||||
padding="$4"
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
minHeight={200}
|
||||
>
|
||||
<Text color="$color10" fontSize={14}>
|
||||
Select an escrow to view details and take actions.
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
<YStack borderWidth={1} borderColor="$borderColor" borderRadius="$4" overflow="hidden">
|
||||
<XStack
|
||||
padding="$3"
|
||||
backgroundColor="$backgroundStrong"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
onPress={() => setShowResolvers((v) => !v)}
|
||||
cursor="pointer"
|
||||
pressStyle={{ opacity: 0.7 }}
|
||||
>
|
||||
<Text fontSize={15} fontWeight="600">
|
||||
Resolver Registry
|
||||
</Text>
|
||||
<Text fontSize={18} color="$color10">
|
||||
{showResolvers ? '▲' : '▼'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
{showResolvers && (
|
||||
<YStack padding="$4">
|
||||
<ResolverPanel />
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
164
app/src/components/CreateEscrowForm.tsx
Normal file
164
app/src/components/CreateEscrowForm.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { YStack, XStack, Text, Button, Input, Spinner } from 'tamagui'
|
||||
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
|
||||
import { PublicKey, Transaction } from '@solana/web3.js'
|
||||
import { AnchorProvider } from '@coral-xyz/anchor'
|
||||
import BN from 'bn.js'
|
||||
import { EscrowClient } from '@descro/sdk'
|
||||
|
||||
function getNextEscrowId(walletPubkey: string): BN {
|
||||
const key = `descro_escrow_id_${walletPubkey}`
|
||||
const stored = localStorage.getItem(key)
|
||||
const current = stored ? parseInt(stored, 10) : 0
|
||||
const next = current + 1
|
||||
localStorage.setItem(key, next.toString())
|
||||
return new BN(next)
|
||||
}
|
||||
|
||||
interface CreateEscrowFormProps {
|
||||
onCreated?: () => void
|
||||
}
|
||||
|
||||
export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
const { publicKey, sendTransaction, signTransaction } = useWallet()
|
||||
const { connection } = useConnection()
|
||||
|
||||
const [buyer, setBuyer] = useState('')
|
||||
const [amountSol, setAmountSol] = useState('')
|
||||
const [resolver, setResolver] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
|
||||
if (!publicKey) {
|
||||
setError('Connect your wallet first.')
|
||||
return
|
||||
}
|
||||
|
||||
let buyerPk: PublicKey
|
||||
try {
|
||||
buyerPk = new PublicKey(buyer)
|
||||
} catch {
|
||||
setError('Invalid buyer public key.')
|
||||
return
|
||||
}
|
||||
|
||||
const amountLamports = parseFloat(amountSol)
|
||||
if (isNaN(amountLamports) || amountLamports <= 0) {
|
||||
setError('Invalid SOL amount.')
|
||||
return
|
||||
}
|
||||
|
||||
let resolverPk: PublicKey | null = null
|
||||
if (resolver.trim()) {
|
||||
try {
|
||||
resolverPk = new PublicKey(resolver.trim())
|
||||
} catch {
|
||||
setError('Invalid resolver public key.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const provider = new AnchorProvider(
|
||||
connection,
|
||||
{ publicKey, signTransaction: signTransaction! } as any,
|
||||
{}
|
||||
)
|
||||
const client = new EscrowClient(provider)
|
||||
const escrowId = getNextEscrowId(publicKey.toBase58())
|
||||
const amount = new BN(Math.round(amountLamports * 1e9))
|
||||
|
||||
const ix = await client.buildCreateEscrow({
|
||||
seller: publicKey,
|
||||
buyer: buyerPk,
|
||||
amount,
|
||||
disputeResolver: resolverPk,
|
||||
escrowId,
|
||||
})
|
||||
|
||||
const tx = new Transaction().add(ix)
|
||||
const sig = await sendTransaction(tx, connection)
|
||||
await connection.confirmTransaction(sig, 'confirmed')
|
||||
|
||||
setSuccess(`Escrow #${escrowId.toString()} created! Tx: ${sig.slice(0, 16)}…`)
|
||||
setBuyer('')
|
||||
setAmountSol('')
|
||||
setResolver('')
|
||||
onCreated?.()
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack gap="$3">
|
||||
<Text fontSize={16} fontWeight="700">Create Escrow</Text>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Buyer Public Key *</Text>
|
||||
<Input
|
||||
value={buyer}
|
||||
onChangeText={setBuyer}
|
||||
placeholder="Pubkey..."
|
||||
fontFamily="monospace"
|
||||
fontSize={13}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Amount (SOL) *</Text>
|
||||
<Input
|
||||
value={amountSol}
|
||||
onChangeText={setAmountSol}
|
||||
placeholder="0.1"
|
||||
keyboardType="decimal-pad"
|
||||
fontSize={13}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Dispute Resolver (optional)</Text>
|
||||
<Input
|
||||
value={resolver}
|
||||
onChangeText={setResolver}
|
||||
placeholder="Pubkey..."
|
||||
fontFamily="monospace"
|
||||
fontSize={13}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
{error && (
|
||||
<XStack backgroundColor="$red3" padding="$2" borderRadius="$2">
|
||||
<Text color="$red9" fontSize={13}>{error}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
{success && (
|
||||
<XStack backgroundColor="$green3" padding="$2" borderRadius="$2">
|
||||
<Text color="$green9" fontSize={13}>{success}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
<Button
|
||||
theme="active"
|
||||
onPress={handleSubmit as unknown as () => void}
|
||||
disabled={loading || !publicKey}
|
||||
icon={loading ? <Spinner /> : undefined}
|
||||
>
|
||||
{loading ? 'Creating…' : 'Create Escrow'}
|
||||
</Button>
|
||||
</form>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
63
app/src/components/EscrowCard.tsx
Normal file
63
app/src/components/EscrowCard.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { YStack, XStack, Text } from 'tamagui'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
import type { EscrowAccountWithPda } from '@descro/sdk'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
|
||||
function truncPubkey(pk: PublicKey): string {
|
||||
const s = pk.toBase58()
|
||||
return `${s.slice(0, 4)}…${s.slice(-4)}`
|
||||
}
|
||||
|
||||
interface EscrowCardProps {
|
||||
item: EscrowAccountWithPda
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) {
|
||||
const { pda, account } = item
|
||||
const solAmount = (Number(account.amount.toString()) / 1e9).toFixed(4)
|
||||
|
||||
return (
|
||||
<YStack
|
||||
padding="$4"
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor={selected ? '$blue9' : '$borderColor'}
|
||||
backgroundColor={selected ? '$blue2' : '$background'}
|
||||
pressStyle={{ opacity: 0.8 }}
|
||||
onPress={onSelect}
|
||||
cursor="pointer"
|
||||
gap="$2"
|
||||
>
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize={12} color="$color11">
|
||||
#{account.escrowId.toString()}
|
||||
</Text>
|
||||
<StatusBadge state={account.state} />
|
||||
</XStack>
|
||||
|
||||
<Text fontSize={16} fontWeight="700">
|
||||
{solAmount} SOL
|
||||
</Text>
|
||||
|
||||
<XStack gap="$4">
|
||||
<YStack>
|
||||
<Text fontSize={11} color="$color10">Seller</Text>
|
||||
<Text fontSize={12} fontFamily="monospace">{truncPubkey(account.seller)}</Text>
|
||||
</YStack>
|
||||
<YStack>
|
||||
<Text fontSize={11} color="$color10">Buyer</Text>
|
||||
<Text fontSize={12} fontFamily="monospace">{truncPubkey(account.buyer)}</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<Text fontSize={11} color="$color10" fontFamily="monospace">
|
||||
PDA: {truncPubkey(pda)}
|
||||
</Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
206
app/src/components/EscrowDetail.tsx
Normal file
206
app/src/components/EscrowDetail.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { YStack, XStack, Text, Button, Spinner, Separator } from 'tamagui'
|
||||
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
|
||||
import { PublicKey, Transaction } from '@solana/web3.js'
|
||||
import { AnchorProvider } from '@coral-xyz/anchor'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
import {
|
||||
EscrowClient,
|
||||
isAwaitingDeposit,
|
||||
isActive,
|
||||
isDisputed,
|
||||
} from '@descro/sdk'
|
||||
import type { EscrowAccountWithPda } from '@descro/sdk'
|
||||
import { useEscrowDetail } from '@/hooks/useEscrowDetail'
|
||||
|
||||
interface EscrowDetailProps {
|
||||
item: EscrowAccountWithPda
|
||||
onAction?: () => void
|
||||
}
|
||||
|
||||
function fullPubkey(pk: PublicKey): string {
|
||||
return pk.toBase58()
|
||||
}
|
||||
|
||||
export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
|
||||
const { pda } = item
|
||||
const { account, loading: detailLoading } = useEscrowDetail(pda)
|
||||
const escrow = account ?? item.account
|
||||
|
||||
const { publicKey, sendTransaction, signTransaction } = useWallet()
|
||||
const { connection } = useConnection()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
|
||||
const solAmount = (Number(escrow.amount.toString()) / 1e9).toFixed(4)
|
||||
const isSeller = publicKey?.equals(escrow.seller) ?? false
|
||||
const isBuyer = publicKey?.equals(escrow.buyer) ?? false
|
||||
const isResolver =
|
||||
escrow.disputeResolver != null &&
|
||||
publicKey?.equals(escrow.disputeResolver) === true
|
||||
|
||||
async function runTx(
|
||||
buildFn: (client: EscrowClient) => Promise<import('@solana/web3.js').TransactionInstruction>
|
||||
) {
|
||||
if (!publicKey || !signTransaction) {
|
||||
setError('Connect your wallet first.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
try {
|
||||
const provider = new AnchorProvider(
|
||||
connection,
|
||||
{ publicKey, signTransaction } as any,
|
||||
{}
|
||||
)
|
||||
const client = new EscrowClient(provider)
|
||||
const ix = await buildFn(client)
|
||||
const tx = new Transaction().add(ix)
|
||||
const sig = await sendTransaction(tx, connection)
|
||||
await connection.confirmTransaction(sig, 'confirmed')
|
||||
setSuccess(`Done! Tx: ${sig.slice(0, 20)}…`)
|
||||
onAction?.()
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeposit() {
|
||||
runTx((client) => client.buildDeposit({ buyer: publicKey!, escrowPda: pda }))
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
runTx((client) => client.buildCancel({ seller: publicKey!, escrowPda: pda }))
|
||||
}
|
||||
|
||||
function handleComplete() {
|
||||
runTx((client) =>
|
||||
client.buildComplete({ buyer: publicKey!, seller: escrow.seller, escrowPda: pda })
|
||||
)
|
||||
}
|
||||
|
||||
function handleDispute() {
|
||||
runTx((client) => client.buildDispute({ initiator: publicKey!, escrowPda: pda }))
|
||||
}
|
||||
|
||||
function handleResolve(winner: 'buyer' | 'seller') {
|
||||
const winnerPubkey = winner === 'buyer' ? escrow.buyer : escrow.seller
|
||||
runTx((client) =>
|
||||
client.buildResolve({
|
||||
resolver: publicKey!,
|
||||
winner,
|
||||
winnerPubkey,
|
||||
seller: escrow.seller,
|
||||
escrowPda: pda,
|
||||
disputeResolver: escrow.disputeResolver,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack
|
||||
padding="$4"
|
||||
borderRadius="$4"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
gap="$3"
|
||||
>
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize={18} fontWeight="700">
|
||||
Escrow #{escrow.escrowId.toString()}
|
||||
</Text>
|
||||
<StatusBadge state={escrow.state} />
|
||||
</XStack>
|
||||
|
||||
<Separator />
|
||||
|
||||
<YStack gap="$2">
|
||||
<Row label="Amount" value={`${solAmount} SOL`} />
|
||||
<Row label="Seller" value={fullPubkey(escrow.seller)} mono />
|
||||
<Row label="Buyer" value={fullPubkey(escrow.buyer)} mono />
|
||||
<Row
|
||||
label="Resolver"
|
||||
value={escrow.disputeResolver ? fullPubkey(escrow.disputeResolver) : 'None'}
|
||||
mono={!!escrow.disputeResolver}
|
||||
/>
|
||||
<Row label="PDA" value={fullPubkey(pda)} mono />
|
||||
</YStack>
|
||||
|
||||
<Separator />
|
||||
|
||||
<YStack gap="$2">
|
||||
{isAwaitingDeposit(escrow.state) && isBuyer && (
|
||||
<Button theme="active" onPress={handleDeposit} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Deposit
|
||||
</Button>
|
||||
)}
|
||||
{isAwaitingDeposit(escrow.state) && isSeller && (
|
||||
<Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{isActive(escrow.state) && isBuyer && (
|
||||
<>
|
||||
<Button theme="green" onPress={handleComplete} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Complete
|
||||
</Button>
|
||||
<Button theme="orange" onPress={handleDispute} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Dispute
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isActive(escrow.state) && isSeller && !isBuyer && (
|
||||
<Button theme="orange" onPress={handleDispute} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Dispute
|
||||
</Button>
|
||||
)}
|
||||
{isDisputed(escrow.state) && isResolver && (
|
||||
<>
|
||||
<Button theme="green" onPress={() => handleResolve('seller')} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Resolve: Seller Wins
|
||||
</Button>
|
||||
<Button theme="active" onPress={() => handleResolve('buyer')} disabled={loading} icon={loading ? <Spinner /> : undefined}>
|
||||
Resolve: Buyer Wins
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
{error && (
|
||||
<XStack backgroundColor="$red3" padding="$2" borderRadius="$2">
|
||||
<Text color="$red9" fontSize={13}>{error}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
{success && (
|
||||
<XStack backgroundColor="$green3" padding="$2" borderRadius="$2">
|
||||
<Text color="$green9" fontSize={13}>{success}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<XStack gap="$2" flexWrap="wrap">
|
||||
<Text fontSize={13} color="$color10" width={80} flexShrink={0}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={13}
|
||||
fontFamily={mono ? 'monospace' : undefined}
|
||||
flex={1}
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
241
app/src/components/ResolverPanel.tsx
Normal file
241
app/src/components/ResolverPanel.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { YStack, XStack, Text, Button, Input, Spinner, Separator } from 'tamagui'
|
||||
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
|
||||
import { PublicKey, Transaction } from '@solana/web3.js'
|
||||
import { AnchorProvider } from '@coral-xyz/anchor'
|
||||
import { RegistryClient, resolverTypeLabel } from '@descro/sdk'
|
||||
import type { ResolverEntryWithPda, ResolverType } from '@descro/sdk'
|
||||
|
||||
const RESOLVER_TYPE_OPTIONS: { label: string; value: ResolverType }[] = [
|
||||
{ label: 'CentralAuthority', value: { centralAuthority: {} } },
|
||||
{ label: 'JuryDAO', value: { juryDAO: {} } },
|
||||
{ label: 'MAD', value: { mad: {} } },
|
||||
{ label: 'Algorithmic', value: { algorithmic: {} } },
|
||||
{ label: 'Multisig', value: { multisig: {} } },
|
||||
]
|
||||
|
||||
function truncPk(pk: PublicKey): string {
|
||||
const s = pk.toBase58()
|
||||
return `${s.slice(0, 4)}…${s.slice(-4)}`
|
||||
}
|
||||
|
||||
export function ResolverPanel() {
|
||||
const { publicKey, sendTransaction, signTransaction } = useWallet()
|
||||
const { connection } = useConnection()
|
||||
|
||||
const [resolvers, setResolvers] = useState<ResolverEntryWithPda[]>([])
|
||||
const [resolversLoading, setResolversLoading] = useState(false)
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [feeBps, setFeeBps] = useState('0')
|
||||
const [feeRecipient, setFeeRecipient] = useState('')
|
||||
const [metadataUri, setMetadataUri] = useState('')
|
||||
const [resolverTypeIdx, setResolverTypeIdx] = useState(0)
|
||||
|
||||
const [formLoading, setFormLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
|
||||
async function loadResolvers() {
|
||||
setResolversLoading(true)
|
||||
try {
|
||||
const provider = new AnchorProvider(connection, {} as any, {})
|
||||
const client = new RegistryClient(provider)
|
||||
const all = await client.fetchAllResolvers()
|
||||
setResolvers(all)
|
||||
} catch (err) {
|
||||
console.error('Failed to load resolvers', err)
|
||||
} finally {
|
||||
setResolversLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadResolvers()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connection])
|
||||
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
|
||||
if (!publicKey || !signTransaction) {
|
||||
setError('Connect your wallet first.')
|
||||
return
|
||||
}
|
||||
|
||||
let recipientPk: PublicKey
|
||||
const recipientStr = feeRecipient.trim() || publicKey.toBase58()
|
||||
try {
|
||||
recipientPk = new PublicKey(recipientStr)
|
||||
} catch {
|
||||
setError('Invalid fee recipient public key.')
|
||||
return
|
||||
}
|
||||
|
||||
const fee = parseInt(feeBps, 10)
|
||||
if (isNaN(fee) || fee < 0 || fee > 10000) {
|
||||
setError('Fee BPS must be between 0 and 10000.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!name.trim()) {
|
||||
setError('Name is required.')
|
||||
return
|
||||
}
|
||||
|
||||
setFormLoading(true)
|
||||
try {
|
||||
const provider = new AnchorProvider(
|
||||
connection,
|
||||
{ publicKey, signTransaction } as any,
|
||||
{}
|
||||
)
|
||||
const client = new RegistryClient(provider)
|
||||
const resolverType = RESOLVER_TYPE_OPTIONS[resolverTypeIdx].value
|
||||
|
||||
const ix = await client.buildRegisterResolver({
|
||||
authority: publicKey,
|
||||
resolverType,
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
feeBps: fee,
|
||||
feeRecipient: recipientPk,
|
||||
metadataUri: metadataUri.trim(),
|
||||
})
|
||||
|
||||
const tx = new Transaction().add(ix)
|
||||
const sig = await sendTransaction(tx, connection)
|
||||
await connection.confirmTransaction(sig, 'confirmed')
|
||||
|
||||
setSuccess(`Registered! Tx: ${sig.slice(0, 20)}…`)
|
||||
setName('')
|
||||
setDescription('')
|
||||
setFeeBps('0')
|
||||
setFeeRecipient('')
|
||||
setMetadataUri('')
|
||||
loadResolvers()
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setFormLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack gap="$4">
|
||||
<Text fontSize={18} fontWeight="700">Resolver Registry</Text>
|
||||
|
||||
<YStack gap="$2">
|
||||
<XStack justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize={15} fontWeight="600">Registered Resolvers</Text>
|
||||
<Button size="$2" onPress={loadResolvers} disabled={resolversLoading}>
|
||||
{resolversLoading ? <Spinner size="small" /> : 'Refresh'}
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
{resolvers.length === 0 && !resolversLoading && (
|
||||
<Text color="$color10" fontSize={13}>No resolvers registered yet.</Text>
|
||||
)}
|
||||
|
||||
{resolvers.map(({ pda, account }) => (
|
||||
<YStack
|
||||
key={pda.toBase58()}
|
||||
padding="$3"
|
||||
borderRadius="$3"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
gap="$1"
|
||||
>
|
||||
<XStack justifyContent="space-between">
|
||||
<Text fontWeight="600">{account.name}</Text>
|
||||
<Text fontSize={12} color="$color10">{resolverTypeLabel(account.resolverType)}</Text>
|
||||
</XStack>
|
||||
{account.description && (
|
||||
<Text fontSize={12} color="$color10">{account.description}</Text>
|
||||
)}
|
||||
<XStack gap="$4">
|
||||
<Text fontSize={12}>Fee: {account.feeBps} bps</Text>
|
||||
<Text fontSize={12}>Resolved: {account.totalResolved.toString()}</Text>
|
||||
<Text fontSize={12} color="$green9">B: {account.ruledForBuyer.toString()}</Text>
|
||||
<Text fontSize={12} color="$orange9">S: {account.ruledForSeller.toString()}</Text>
|
||||
</XStack>
|
||||
<Text fontSize={11} fontFamily="monospace" color="$color10">
|
||||
{truncPk(account.authority)}
|
||||
</Text>
|
||||
</YStack>
|
||||
))}
|
||||
</YStack>
|
||||
|
||||
<Separator />
|
||||
|
||||
<YStack gap="$3">
|
||||
<Text fontSize={15} fontWeight="600">Register as Resolver</Text>
|
||||
|
||||
<form onSubmit={handleRegister} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Name *</Text>
|
||||
<Input value={name} onChangeText={setName} placeholder="My Resolver" fontSize={13} />
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Description</Text>
|
||||
<Input value={description} onChangeText={setDescription} placeholder="What I do..." fontSize={13} />
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Resolver Type</Text>
|
||||
<select
|
||||
value={resolverTypeIdx}
|
||||
onChange={(e) => setResolverTypeIdx(parseInt(e.target.value, 10))}
|
||||
style={{ padding: '8px', borderRadius: 6, border: '1px solid #ccc', fontSize: 13 }}
|
||||
>
|
||||
{RESOLVER_TYPE_OPTIONS.map((opt, i) => (
|
||||
<option key={i} value={i}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Fee (BPS)</Text>
|
||||
<Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} />
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Fee Recipient (default: your wallet)</Text>
|
||||
<Input value={feeRecipient} onChangeText={setFeeRecipient} placeholder="Pubkey or leave blank" fontFamily="monospace" fontSize={13} />
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Metadata URI</Text>
|
||||
<Input value={metadataUri} onChangeText={setMetadataUri} placeholder="https://..." fontSize={13} />
|
||||
</YStack>
|
||||
|
||||
{error && (
|
||||
<XStack backgroundColor="$red3" padding="$2" borderRadius="$2">
|
||||
<Text color="$red9" fontSize={13}>{error}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
{success && (
|
||||
<XStack backgroundColor="$green3" padding="$2" borderRadius="$2">
|
||||
<Text color="$green9" fontSize={13}>{success}</Text>
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
<Button
|
||||
theme="active"
|
||||
onPress={handleRegister as unknown as () => void}
|
||||
disabled={formLoading || !publicKey}
|
||||
icon={formLoading ? <Spinner /> : undefined}
|
||||
>
|
||||
{formLoading ? 'Registering…' : 'Register'}
|
||||
</Button>
|
||||
</form>
|
||||
</YStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
37
app/src/components/StatusBadge.tsx
Normal file
37
app/src/components/StatusBadge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { XStack, Text } from 'tamagui'
|
||||
import { escrowStateLabel } from '@descro/sdk'
|
||||
import type { EscrowState } from '@descro/sdk'
|
||||
|
||||
const STATE_COLORS: Record<string, string> = {
|
||||
AwaitingDeposit: '$blue9',
|
||||
Active: '$green9',
|
||||
Disputed: '$orange9',
|
||||
Complete: '$gray9',
|
||||
Cancelled: '$red9',
|
||||
}
|
||||
|
||||
interface StatusBadgeProps {
|
||||
state: EscrowState
|
||||
}
|
||||
|
||||
export function StatusBadge({ state }: StatusBadgeProps) {
|
||||
const label = escrowStateLabel(state)
|
||||
const color = STATE_COLORS[label] ?? '$gray9'
|
||||
|
||||
return (
|
||||
<XStack
|
||||
backgroundColor={color}
|
||||
paddingHorizontal="$2"
|
||||
paddingVertical="$1"
|
||||
borderRadius="$2"
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<Text color="white" fontSize={12} fontWeight="600">
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
18
app/src/components/WalletButton.tsx
Normal file
18
app/src/components/WalletButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
|
||||
|
||||
export function WalletButton() {
|
||||
return (
|
||||
<WalletMultiButton
|
||||
style={{
|
||||
backgroundColor: '#512da8',
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
padding: '8px 16px',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
56
app/src/hooks/useEscrowDetail.ts
Normal file
56
app/src/hooks/useEscrowDetail.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { useConnection } from '@solana/wallet-adapter-react'
|
||||
import { AnchorProvider } from '@coral-xyz/anchor'
|
||||
import { EscrowClient, subscribeEscrow } from '@descro/sdk'
|
||||
import type { EscrowAccount } from '@descro/sdk'
|
||||
|
||||
export function useEscrowDetail(pda: PublicKey | null) {
|
||||
const { connection } = useConnection()
|
||||
const [account, setAccount] = useState<EscrowAccount | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const unsubRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!pda) {
|
||||
setAccount(null)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
setLoading(true)
|
||||
const provider = new AnchorProvider(connection, {} as any, {})
|
||||
const client = new EscrowClient(provider)
|
||||
client
|
||||
.fetchEscrow(pda)
|
||||
.then((acc) => {
|
||||
if (!cancelled) {
|
||||
setAccount(acc)
|
||||
setError(null)
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
|
||||
const unsub = subscribeEscrow(connection, pda, (updated) => {
|
||||
if (!cancelled) setAccount(updated)
|
||||
})
|
||||
unsubRef.current = unsub
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
unsub()
|
||||
unsubRef.current = null
|
||||
}
|
||||
}, [pda?.toBase58(), connection])
|
||||
|
||||
return { account, loading, error }
|
||||
}
|
||||
50
app/src/hooks/useEscrows.ts
Normal file
50
app/src/hooks/useEscrows.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
|
||||
import { AnchorProvider } from '@coral-xyz/anchor'
|
||||
import { EscrowClient } from '@descro/sdk'
|
||||
import type { EscrowAccountWithPda } from '@descro/sdk'
|
||||
|
||||
const POLL_INTERVAL_MS = 5000
|
||||
|
||||
export function useEscrows() {
|
||||
const { publicKey } = useWallet()
|
||||
const { connection } = useConnection()
|
||||
const [escrows, setEscrows] = useState<EscrowAccountWithPda[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const fetchEscrows = useCallback(async () => {
|
||||
if (!publicKey) {
|
||||
setEscrows([])
|
||||
return
|
||||
}
|
||||
try {
|
||||
const provider = new AnchorProvider(connection, {} as any, {})
|
||||
const client = new EscrowClient(provider)
|
||||
const results = await client.fetchEscrowsForWallet(publicKey)
|
||||
results.sort((a, b) =>
|
||||
Number(b.account.escrowId.toString()) - Number(a.account.escrowId.toString())
|
||||
)
|
||||
setEscrows(results)
|
||||
setError(null)
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
}, [publicKey, connection])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchEscrows().finally(() => setLoading(false))
|
||||
|
||||
intervalRef.current = setInterval(fetchEscrows, POLL_INTERVAL_MS)
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current !== null) clearInterval(intervalRef.current)
|
||||
}
|
||||
}, [fetchEscrows])
|
||||
|
||||
return { escrows, loading, error, refresh: fetchEscrows }
|
||||
}
|
||||
7
app/tamagui.build.ts
Normal file
7
app/tamagui.build.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { TamaguiBuildOptions } from '@tamagui/core'
|
||||
|
||||
export default {
|
||||
components: ['@tamagui/core'],
|
||||
config: './tamagui.config.ts',
|
||||
outputCSS: './public/tamagui.generated.css',
|
||||
} satisfies TamaguiBuildOptions
|
||||
12
app/tamagui.config.ts
Normal file
12
app/tamagui.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defaultConfig } from '@tamagui/config/v5'
|
||||
import { createTamagui } from 'tamagui'
|
||||
|
||||
const appConfig = createTamagui(defaultConfig)
|
||||
|
||||
export type AppConfig = typeof appConfig
|
||||
|
||||
declare module 'tamagui' {
|
||||
interface TamaguiCustomConfig extends AppConfig {}
|
||||
}
|
||||
|
||||
export default appConfig
|
||||
41
app/tsconfig.json
Normal file
41
app/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"name": "descro",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"sdk",
|
||||
"app"
|
||||
],
|
||||
"scripts": {
|
||||
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
|
||||
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
|
||||
|
||||
@@ -9,7 +9,7 @@ pub use instructions::*;
|
||||
pub use state::*;
|
||||
|
||||
// Replace with actual program ID after first deployment
|
||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||
declare_id!("GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp");
|
||||
|
||||
/// Escrow Program ID — only its PDA may call update_stats.
|
||||
pub const ESCROW_PROGRAM_ID: Pubkey =
|
||||
|
||||
16
sdk/package.json
Normal file
16
sdk/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@descro/sdk",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.32.1",
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"bn.js": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
198
sdk/src/escrow.ts
Normal file
198
sdk/src/escrow.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
|
||||
import type { Idl } from "@coral-xyz/anchor";
|
||||
import descroIdl from "./idl/descro.json";
|
||||
import {
|
||||
ESCROW_PROGRAM_ID,
|
||||
REGISTRY_PROGRAM_ID,
|
||||
deriveEscrowPda,
|
||||
deriveVaultPda,
|
||||
deriveEscrowAuthorityPda,
|
||||
deriveResolverEntryPda,
|
||||
} from "./pda";
|
||||
import type { EscrowAccount, EscrowAccountWithPda, Winner } from "./types";
|
||||
|
||||
export class EscrowClient {
|
||||
readonly program: Program;
|
||||
readonly provider: AnchorProvider;
|
||||
|
||||
constructor(provider: AnchorProvider) {
|
||||
this.provider = provider;
|
||||
this.program = new Program(
|
||||
descroIdl as Idl,
|
||||
new PublicKey(ESCROW_PROGRAM_ID),
|
||||
provider
|
||||
);
|
||||
}
|
||||
|
||||
async buildCreateEscrow(params: {
|
||||
seller: PublicKey;
|
||||
buyer: PublicKey;
|
||||
amount: BN;
|
||||
disputeResolver: PublicKey | null;
|
||||
escrowId: BN;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { seller, buyer, amount, disputeResolver, escrowId } = params;
|
||||
const [escrowPda] = deriveEscrowPda(seller, escrowId);
|
||||
const [vaultPda] = deriveVaultPda(escrowPda);
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.createEscrow(amount, disputeResolver, escrowId)
|
||||
.accounts({
|
||||
seller,
|
||||
buyer,
|
||||
escrowAccount: escrowPda,
|
||||
vault: vaultPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildDeposit(params: {
|
||||
buyer: PublicKey;
|
||||
escrowPda: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { buyer, escrowPda } = params;
|
||||
const [vaultPda] = deriveVaultPda(escrowPda);
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.deposit()
|
||||
.accounts({
|
||||
buyer,
|
||||
escrowAccount: escrowPda,
|
||||
vault: vaultPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildComplete(params: {
|
||||
buyer: PublicKey;
|
||||
seller: PublicKey;
|
||||
escrowPda: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { buyer, seller, escrowPda } = params;
|
||||
const [vaultPda] = deriveVaultPda(escrowPda);
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.complete()
|
||||
.accounts({
|
||||
buyer,
|
||||
seller,
|
||||
escrowAccount: escrowPda,
|
||||
vault: vaultPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildDispute(params: {
|
||||
initiator: PublicKey;
|
||||
escrowPda: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { initiator, escrowPda } = params;
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.dispute()
|
||||
.accounts({
|
||||
initiator,
|
||||
escrowAccount: escrowPda,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildCancel(params: {
|
||||
seller: PublicKey;
|
||||
escrowPda: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { seller, escrowPda } = params;
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.cancel()
|
||||
.accounts({
|
||||
seller,
|
||||
escrowAccount: escrowPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildResolve(params: {
|
||||
resolver: PublicKey;
|
||||
winner: "buyer" | "seller";
|
||||
winnerPubkey: PublicKey;
|
||||
seller: PublicKey;
|
||||
escrowPda: PublicKey;
|
||||
disputeResolver: PublicKey | null;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { resolver, winner, winnerPubkey, seller, escrowPda, disputeResolver } = params;
|
||||
const [vaultPda] = deriveVaultPda(escrowPda);
|
||||
const [escrowAuthority] = deriveEscrowAuthorityPda();
|
||||
|
||||
const registryProgram = disputeResolver ? REGISTRY_PROGRAM_ID : SystemProgram.programId;
|
||||
const resolverEntryAuthority = disputeResolver ?? resolver;
|
||||
const [resolverEntry] = deriveResolverEntryPda(resolverEntryAuthority);
|
||||
|
||||
const winnerArg: Winner =
|
||||
winner === "buyer" ? { buyer: {} } : { seller: {} };
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.resolve(winnerArg)
|
||||
.accounts({
|
||||
resolver,
|
||||
winner: winnerPubkey,
|
||||
seller,
|
||||
escrowAccount: escrowPda,
|
||||
vault: vaultPda,
|
||||
resolverEntry,
|
||||
escrowAuthority,
|
||||
registryProgram,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async fetchEscrow(pda: PublicKey): Promise<EscrowAccount | null> {
|
||||
try {
|
||||
const raw = await (this.program.account as any).escrowAccount.fetch(pda);
|
||||
return raw as EscrowAccount;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAllEscrows(): Promise<EscrowAccountWithPda[]> {
|
||||
const accounts = await (this.program.account as any).escrowAccount.all();
|
||||
return accounts.map((a: { publicKey: PublicKey; account: EscrowAccount }) => ({
|
||||
pda: a.publicKey,
|
||||
account: a.account as EscrowAccount,
|
||||
}));
|
||||
}
|
||||
|
||||
async fetchEscrowsForWallet(wallet: PublicKey): Promise<EscrowAccountWithPda[]> {
|
||||
const asSeller = await (this.program.account as any).escrowAccount.all([
|
||||
{ memcmp: { offset: 8, bytes: wallet.toBase58() } },
|
||||
]);
|
||||
|
||||
const asBuyer = await (this.program.account as any).escrowAccount.all([
|
||||
{ memcmp: { offset: 8 + 32, bytes: wallet.toBase58() } },
|
||||
]);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const results: EscrowAccountWithPda[] = [];
|
||||
|
||||
for (const a of [...asSeller, ...asBuyer]) {
|
||||
const key = a.publicKey.toBase58();
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
results.push({ pda: a.publicKey, account: a.account as EscrowAccount });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
674
sdk/src/idl/descro.json
Normal file
674
sdk/src/idl/descro.json
Normal file
@@ -0,0 +1,674 @@
|
||||
{
|
||||
"address": "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3",
|
||||
"metadata": {
|
||||
"name": "descro",
|
||||
"version": "0.1.0",
|
||||
"spec": "0.1.0",
|
||||
"description": "Created with Anchor"
|
||||
},
|
||||
"instructions": [
|
||||
{
|
||||
"name": "cancel",
|
||||
"discriminator": [
|
||||
232,
|
||||
219,
|
||||
223,
|
||||
41,
|
||||
219,
|
||||
236,
|
||||
220,
|
||||
190
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "seller",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.seller",
|
||||
"account": "EscrowAccount"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.escrow_id",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "complete",
|
||||
"discriminator": [
|
||||
0,
|
||||
77,
|
||||
224,
|
||||
147,
|
||||
136,
|
||||
25,
|
||||
88,
|
||||
76
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "buyer",
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "seller",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.seller",
|
||||
"account": "EscrowAccount"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.escrow_id",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "create_escrow",
|
||||
"discriminator": [
|
||||
253,
|
||||
215,
|
||||
165,
|
||||
116,
|
||||
36,
|
||||
108,
|
||||
68,
|
||||
80
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "seller",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "buyer"
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "seller"
|
||||
},
|
||||
{
|
||||
"kind": "arg",
|
||||
"path": "escrow_id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "dispute_resolver",
|
||||
"type": {
|
||||
"option": "pubkey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "escrow_id",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "deposit",
|
||||
"discriminator": [
|
||||
242,
|
||||
35,
|
||||
198,
|
||||
137,
|
||||
82,
|
||||
225,
|
||||
242,
|
||||
182
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "buyer",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.seller",
|
||||
"account": "EscrowAccount"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.escrow_id",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "dispute",
|
||||
"discriminator": [
|
||||
216,
|
||||
92,
|
||||
128,
|
||||
146,
|
||||
202,
|
||||
85,
|
||||
135,
|
||||
73
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "initiator",
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.seller",
|
||||
"account": "EscrowAccount"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.escrow_id",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "resolve",
|
||||
"discriminator": [
|
||||
246,
|
||||
150,
|
||||
236,
|
||||
206,
|
||||
108,
|
||||
63,
|
||||
58,
|
||||
10
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "resolver",
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "winner",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "seller",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.seller",
|
||||
"account": "EscrowAccount"
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.escrow_id",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
118,
|
||||
97,
|
||||
117,
|
||||
108,
|
||||
116
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "resolver_entry",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
114,
|
||||
101,
|
||||
115,
|
||||
111,
|
||||
108,
|
||||
118,
|
||||
101,
|
||||
114
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "escrow_account.dispute_resolver",
|
||||
"account": "EscrowAccount"
|
||||
}
|
||||
],
|
||||
"program": {
|
||||
"kind": "const",
|
||||
"value": [
|
||||
4,
|
||||
21,
|
||||
77,
|
||||
207,
|
||||
27,
|
||||
130,
|
||||
71,
|
||||
134,
|
||||
111,
|
||||
76,
|
||||
186,
|
||||
244,
|
||||
136,
|
||||
42,
|
||||
168,
|
||||
238,
|
||||
151,
|
||||
132,
|
||||
148,
|
||||
41,
|
||||
241,
|
||||
127,
|
||||
56,
|
||||
240,
|
||||
140,
|
||||
66,
|
||||
190,
|
||||
194,
|
||||
253,
|
||||
243,
|
||||
165,
|
||||
91
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "escrow_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "registry_program"
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "winner",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "Winner"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "EscrowAccount",
|
||||
"discriminator": [
|
||||
36,
|
||||
69,
|
||||
48,
|
||||
18,
|
||||
128,
|
||||
225,
|
||||
125,
|
||||
135
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": 6000,
|
||||
"name": "InvalidState",
|
||||
"msg": "Invalid state for this instruction"
|
||||
},
|
||||
{
|
||||
"code": 6001,
|
||||
"name": "Unauthorized",
|
||||
"msg": "Signer is not authorized"
|
||||
},
|
||||
{
|
||||
"code": 6002,
|
||||
"name": "NoResolverConfigured",
|
||||
"msg": "No resolver configured for this escrow"
|
||||
},
|
||||
{
|
||||
"code": 6003,
|
||||
"name": "UnauthorizedResolver",
|
||||
"msg": "Signer is not the configured resolver"
|
||||
},
|
||||
{
|
||||
"code": 6004,
|
||||
"name": "Expired",
|
||||
"msg": "Escrow has expired"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "EscrowAccount",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "seller",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "buyer",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "dispute_resolver",
|
||||
"type": {
|
||||
"option": "pubkey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "EscrowState"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bump",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "vault_bump",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "escrow_id",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EscrowState",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "AwaitingDeposit"
|
||||
},
|
||||
{
|
||||
"name": "Active"
|
||||
},
|
||||
{
|
||||
"name": "Disputed"
|
||||
},
|
||||
{
|
||||
"name": "Complete"
|
||||
},
|
||||
{
|
||||
"name": "Cancelled"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Winner",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Buyer"
|
||||
},
|
||||
{
|
||||
"name": "Seller"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
385
sdk/src/idl/descro_ext_resolvers.json
Normal file
385
sdk/src/idl/descro_ext_resolvers.json
Normal file
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"address": "GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp",
|
||||
"metadata": {
|
||||
"name": "descro_ext_resolvers",
|
||||
"version": "0.1.0",
|
||||
"spec": "0.1.0",
|
||||
"description": "Resolver Registry for the descro escrow protocol"
|
||||
},
|
||||
"instructions": [
|
||||
{
|
||||
"name": "register_resolver",
|
||||
"discriminator": [
|
||||
76,
|
||||
101,
|
||||
253,
|
||||
229,
|
||||
153,
|
||||
242,
|
||||
212,
|
||||
230
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "resolver_entry",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
114,
|
||||
101,
|
||||
115,
|
||||
111,
|
||||
108,
|
||||
118,
|
||||
101,
|
||||
114
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "authority"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "resolver_type",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "ResolverType"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "fee_bps",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "fee_recipient",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_resolver",
|
||||
"discriminator": [
|
||||
108,
|
||||
227,
|
||||
28,
|
||||
163,
|
||||
123,
|
||||
230,
|
||||
190,
|
||||
84
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "resolver_entry",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
114,
|
||||
101,
|
||||
115,
|
||||
111,
|
||||
108,
|
||||
118,
|
||||
101,
|
||||
114
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "authority"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "fee_bps",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_stats",
|
||||
"discriminator": [
|
||||
145,
|
||||
138,
|
||||
9,
|
||||
150,
|
||||
178,
|
||||
31,
|
||||
158,
|
||||
244
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "escrow_authority",
|
||||
"docs": [
|
||||
"Must be the Escrow Program's authority PDA — only it can sign this."
|
||||
],
|
||||
"signer": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
101,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
111,
|
||||
119,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
],
|
||||
"program": {
|
||||
"kind": "const",
|
||||
"value": [
|
||||
189,
|
||||
46,
|
||||
196,
|
||||
9,
|
||||
38,
|
||||
68,
|
||||
13,
|
||||
236,
|
||||
154,
|
||||
101,
|
||||
74,
|
||||
161,
|
||||
29,
|
||||
155,
|
||||
110,
|
||||
167,
|
||||
230,
|
||||
204,
|
||||
79,
|
||||
86,
|
||||
74,
|
||||
46,
|
||||
229,
|
||||
168,
|
||||
214,
|
||||
228,
|
||||
65,
|
||||
126,
|
||||
160,
|
||||
53,
|
||||
53,
|
||||
204
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "resolver_entry",
|
||||
"writable": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "ruling",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "Ruling"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "ResolverEntry",
|
||||
"discriminator": [
|
||||
0,
|
||||
60,
|
||||
55,
|
||||
58,
|
||||
157,
|
||||
135,
|
||||
51,
|
||||
191
|
||||
]
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": 6000,
|
||||
"name": "Unauthorized",
|
||||
"msg": "Signer is not the registered authority"
|
||||
},
|
||||
{
|
||||
"code": 6001,
|
||||
"name": "UnauthorizedCaller",
|
||||
"msg": "Caller is not the authorized escrow program"
|
||||
},
|
||||
{
|
||||
"code": 6002,
|
||||
"name": "InvalidFeeBps",
|
||||
"msg": "Fee basis points must be <= 10000"
|
||||
},
|
||||
{
|
||||
"code": 6003,
|
||||
"name": "EmptyName",
|
||||
"msg": "Name must not be empty"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "ResolverEntry",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "resolver_type",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "ResolverType"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "fee_bps",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "fee_recipient",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "metadata_uri",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "total_resolved",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "ruled_for_buyer",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "ruled_for_seller",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "registered_at",
|
||||
"type": "i64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ResolverType",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "CentralAuthority"
|
||||
},
|
||||
{
|
||||
"name": "JuryDAO"
|
||||
},
|
||||
{
|
||||
"name": "MAD"
|
||||
},
|
||||
{
|
||||
"name": "Algorithmic"
|
||||
},
|
||||
{
|
||||
"name": "Multisig"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ruling",
|
||||
"docs": [
|
||||
"Passed to update_stats; mirrors the Escrow program's Winner enum."
|
||||
],
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Buyer"
|
||||
},
|
||||
{
|
||||
"name": "Seller"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
19
sdk/src/index.ts
Normal file
19
sdk/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export * from "./types";
|
||||
export * from "./pda";
|
||||
export * from "./escrow";
|
||||
export * from "./registry";
|
||||
export * from "./listener";
|
||||
|
||||
import { AnchorProvider } from "@coral-xyz/anchor";
|
||||
import { EscrowClient } from "./escrow";
|
||||
import { RegistryClient } from "./registry";
|
||||
|
||||
export class DescroSdk {
|
||||
readonly escrow: EscrowClient;
|
||||
readonly registry: RegistryClient;
|
||||
|
||||
constructor(provider: AnchorProvider) {
|
||||
this.escrow = new EscrowClient(provider);
|
||||
this.registry = new RegistryClient(provider);
|
||||
}
|
||||
}
|
||||
53
sdk/src/listener.ts
Normal file
53
sdk/src/listener.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Connection, PublicKey, AccountInfo } from "@solana/web3.js";
|
||||
import { AnchorProvider, Program } from "@coral-xyz/anchor";
|
||||
import type { Idl } from "@coral-xyz/anchor";
|
||||
import descroIdl from "./idl/descro.json";
|
||||
import { ESCROW_PROGRAM_ID } from "./pda";
|
||||
import type { EscrowAccount } from "./types";
|
||||
|
||||
function decodeEscrowAccount(
|
||||
program: Program,
|
||||
info: AccountInfo<Buffer>
|
||||
): EscrowAccount | null {
|
||||
try {
|
||||
return (program.coder.accounts as any).decode("EscrowAccount", info.data) as EscrowAccount;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeEscrow(
|
||||
connection: Connection,
|
||||
pda: PublicKey,
|
||||
cb: (account: EscrowAccount | null) => void
|
||||
): () => void {
|
||||
const provider = new AnchorProvider(connection, {} as never, {});
|
||||
const program = new Program(descroIdl as Idl, new PublicKey(ESCROW_PROGRAM_ID), provider);
|
||||
|
||||
const subId = connection.onAccountChange(pda, (info) => {
|
||||
cb(decodeEscrowAccount(program, info as AccountInfo<Buffer>));
|
||||
});
|
||||
|
||||
return () => {
|
||||
connection.removeAccountChangeListener(subId).catch(() => undefined);
|
||||
};
|
||||
}
|
||||
|
||||
export function subscribeAll(
|
||||
connection: Connection,
|
||||
programId: PublicKey = ESCROW_PROGRAM_ID,
|
||||
cb: (pda: PublicKey, account: EscrowAccount | null) => void
|
||||
): () => void {
|
||||
const provider = new AnchorProvider(connection, {} as never, {});
|
||||
const program = new Program(descroIdl as Idl, new PublicKey(programId), provider);
|
||||
|
||||
const subId = connection.onProgramAccountChange(programId, (keyedAccountInfo) => {
|
||||
const pda = keyedAccountInfo.accountId;
|
||||
const info = keyedAccountInfo.accountInfo;
|
||||
cb(pda, decodeEscrowAccount(program, info as AccountInfo<Buffer>));
|
||||
});
|
||||
|
||||
return () => {
|
||||
connection.removeProgramAccountChangeListener(subId).catch(() => undefined);
|
||||
};
|
||||
}
|
||||
52
sdk/src/pda.ts
Normal file
52
sdk/src/pda.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
|
||||
export const ESCROW_PROGRAM_ID = new PublicKey(
|
||||
"DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3"
|
||||
);
|
||||
export const REGISTRY_PROGRAM_ID = new PublicKey(
|
||||
"GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp"
|
||||
);
|
||||
|
||||
export function deriveEscrowPda(
|
||||
seller: PublicKey,
|
||||
escrowId: BN | bigint | number,
|
||||
programId: PublicKey = ESCROW_PROGRAM_ID
|
||||
): [PublicKey, number] {
|
||||
const id = new BN(escrowId.toString());
|
||||
const idBuf = Buffer.alloc(8);
|
||||
idBuf.writeBigUInt64LE(BigInt(id.toString()));
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("escrow"), seller.toBuffer(), idBuf],
|
||||
programId
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveVaultPda(
|
||||
escrowPda: PublicKey,
|
||||
programId: PublicKey = ESCROW_PROGRAM_ID
|
||||
): [PublicKey, number] {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("vault"), escrowPda.toBuffer()],
|
||||
programId
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveEscrowAuthorityPda(
|
||||
programId: PublicKey = ESCROW_PROGRAM_ID
|
||||
): [PublicKey, number] {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("escrow_authority")],
|
||||
programId
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveResolverEntryPda(
|
||||
authority: PublicKey,
|
||||
registryProgramId: PublicKey = REGISTRY_PROGRAM_ID
|
||||
): [PublicKey, number] {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("resolver"), authority.toBuffer()],
|
||||
registryProgramId
|
||||
);
|
||||
}
|
||||
88
sdk/src/registry.ts
Normal file
88
sdk/src/registry.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { PublicKey, TransactionInstruction, SystemProgram } from "@solana/web3.js";
|
||||
import { AnchorProvider, Program } from "@coral-xyz/anchor";
|
||||
import type { Idl } from "@coral-xyz/anchor";
|
||||
import registryIdl from "./idl/descro_ext_resolvers.json";
|
||||
import { REGISTRY_PROGRAM_ID, deriveResolverEntryPda } from "./pda";
|
||||
import type { ResolverEntry, ResolverEntryWithPda, ResolverType } from "./types";
|
||||
|
||||
export class RegistryClient {
|
||||
readonly program: Program;
|
||||
readonly provider: AnchorProvider;
|
||||
|
||||
constructor(provider: AnchorProvider) {
|
||||
this.provider = provider;
|
||||
this.program = new Program(
|
||||
registryIdl as Idl,
|
||||
new PublicKey(REGISTRY_PROGRAM_ID),
|
||||
provider
|
||||
);
|
||||
}
|
||||
|
||||
async buildRegisterResolver(params: {
|
||||
authority: PublicKey;
|
||||
resolverType: ResolverType;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
feeRecipient: PublicKey;
|
||||
metadataUri: string;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { authority, resolverType, name, description, feeBps, feeRecipient, metadataUri } = params;
|
||||
const [resolverEntry] = deriveResolverEntryPda(authority);
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.registerResolver(resolverType, name, description, feeBps, feeRecipient, metadataUri)
|
||||
.accounts({
|
||||
authority,
|
||||
resolverEntry,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async buildUpdateResolver(params: {
|
||||
authority: PublicKey;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
metadataUri: string;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { authority, name, description, feeBps, metadataUri } = params;
|
||||
const [resolverEntry] = deriveResolverEntryPda(authority);
|
||||
|
||||
return await (this.program.methods as any)
|
||||
.updateResolver(name, description, feeBps, metadataUri)
|
||||
.accounts({
|
||||
authority,
|
||||
resolverEntry,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async fetchResolver(authority: PublicKey): Promise<ResolverEntry | null> {
|
||||
try {
|
||||
const [pda] = deriveResolverEntryPda(authority);
|
||||
const raw = await (this.program.account as any).resolverEntry.fetch(pda);
|
||||
return raw as ResolverEntry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchResolverByPda(pda: PublicKey): Promise<ResolverEntry | null> {
|
||||
try {
|
||||
const raw = await (this.program.account as any).resolverEntry.fetch(pda);
|
||||
return raw as ResolverEntry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAllResolvers(): Promise<ResolverEntryWithPda[]> {
|
||||
const accounts = await (this.program.account as any).resolverEntry.all();
|
||||
return accounts.map((a: { publicKey: PublicKey; account: ResolverEntry }) => ({
|
||||
pda: a.publicKey,
|
||||
account: a.account as ResolverEntry,
|
||||
}));
|
||||
}
|
||||
}
|
||||
87
sdk/src/types.ts
Normal file
87
sdk/src/types.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
|
||||
export type EscrowState =
|
||||
| { awaitingDeposit: Record<string, never> }
|
||||
| { active: Record<string, never> }
|
||||
| { disputed: Record<string, never> }
|
||||
| { complete: Record<string, never> }
|
||||
| { cancelled: Record<string, never> };
|
||||
|
||||
export type Winner = { buyer: Record<string, never> } | { seller: Record<string, never> };
|
||||
|
||||
export type ResolverType =
|
||||
| { centralAuthority: Record<string, never> }
|
||||
| { juryDAO: Record<string, never> }
|
||||
| { mad: Record<string, never> }
|
||||
| { algorithmic: Record<string, never> }
|
||||
| { multisig: Record<string, never> };
|
||||
|
||||
export interface EscrowAccount {
|
||||
seller: PublicKey;
|
||||
buyer: PublicKey;
|
||||
amount: BN;
|
||||
disputeResolver: PublicKey | null;
|
||||
state: EscrowState;
|
||||
bump: number;
|
||||
vaultBump: number;
|
||||
escrowId: BN;
|
||||
}
|
||||
|
||||
export interface ResolverEntry {
|
||||
authority: PublicKey;
|
||||
resolverType: ResolverType;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
feeRecipient: PublicKey;
|
||||
metadataUri: string;
|
||||
totalResolved: BN;
|
||||
ruledForBuyer: BN;
|
||||
ruledForSeller: BN;
|
||||
registeredAt: BN;
|
||||
}
|
||||
|
||||
export interface EscrowAccountWithPda {
|
||||
pda: PublicKey;
|
||||
account: EscrowAccount;
|
||||
}
|
||||
|
||||
export interface ResolverEntryWithPda {
|
||||
pda: PublicKey;
|
||||
account: ResolverEntry;
|
||||
}
|
||||
|
||||
export function isAwaitingDeposit(state: EscrowState): boolean {
|
||||
return "awaitingDeposit" in state;
|
||||
}
|
||||
export function isActive(state: EscrowState): boolean {
|
||||
return "active" in state;
|
||||
}
|
||||
export function isDisputed(state: EscrowState): boolean {
|
||||
return "disputed" in state;
|
||||
}
|
||||
export function isComplete(state: EscrowState): boolean {
|
||||
return "complete" in state;
|
||||
}
|
||||
export function isCancelled(state: EscrowState): boolean {
|
||||
return "cancelled" in state;
|
||||
}
|
||||
|
||||
export function escrowStateLabel(state: EscrowState): string {
|
||||
if (isAwaitingDeposit(state)) return "AwaitingDeposit";
|
||||
if (isActive(state)) return "Active";
|
||||
if (isDisputed(state)) return "Disputed";
|
||||
if (isComplete(state)) return "Complete";
|
||||
if (isCancelled(state)) return "Cancelled";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
export function resolverTypeLabel(rt: ResolverType): string {
|
||||
if ("centralAuthority" in rt) return "CentralAuthority";
|
||||
if ("juryDAO" in rt) return "JuryDAO";
|
||||
if ("mad" in rt) return "MAD";
|
||||
if ("algorithmic" in rt) return "Algorithmic";
|
||||
if ("multisig" in rt) return "Multisig";
|
||||
return "Unknown";
|
||||
}
|
||||
16
sdk/tsconfig.json
Normal file
16
sdk/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user