update app to use solana kit

This commit is contained in:
thesn10
2026-05-23 14:45:49 +02:00
parent c92e71fe81
commit 9b7391677c
13 changed files with 320 additions and 256 deletions

1
app/.gitignore vendored
View File

@@ -2,3 +2,4 @@
node_modules/ node_modules/
public/tamagui.generated.css public/tamagui.generated.css
.env*.local .env*.local
tsconfig.tsbuildinfo

View File

@@ -7,10 +7,9 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@coral-xyz/anchor": "^0.32.1",
"@descro/sdk": "workspace:*", "@descro/sdk": "workspace:*",
"@solana/connector": "^0.2.4", "@solana/connector": "^0.2.4",
"@solana/web3.js": "^1.98.4", "@solana/kit": "^6.0.0",
"@tamagui/config": "^2.0.0-rc.42", "@tamagui/config": "^2.0.0-rc.42",
"@tamagui/next-theme": "^2.0.0-rc.42", "@tamagui/next-theme": "^2.0.0-rc.42",
"next": "^16.2.6", "next": "^16.2.6",

View File

@@ -124,9 +124,9 @@ export default function PlaygroundPage() {
<YStack gap="$2"> <YStack gap="$2">
{escrows.map((item) => ( {escrows.map((item) => (
<EscrowCard <EscrowCard
key={item.pda.toBase58()} key={item.pda}
item={item} item={item}
selected={selected?.pda.equals(item.pda) ?? false} selected={selected?.pda === item.pda}
onSelect={() => setSelected(item)} onSelect={() => setSelected(item)}
/> />
))} ))}

View File

@@ -1,21 +1,32 @@
'use client' 'use client'
import { useState, useMemo } from 'react' import { useState } from 'react'
import { YStack, XStack, Text, Button, Input, Spinner } from 'tamagui' import { YStack, XStack, Text, Button, Input, Spinner } from 'tamagui'
import { PublicKey, Transaction, Connection } from '@solana/web3.js' import {
import { AnchorProvider } from '@coral-xyz/anchor' createSolanaRpc,
import { useTransactionSigner, useDisconnectWallet, useCluster } from '@solana/connector/react' createSolanaRpcSubscriptions,
import { useWalletAdapterCompat } from '@solana/connector/compat' pipe,
import BN from 'bn.js' createTransactionMessage,
import { EscrowClient } from '@descro/sdk' setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
assertIsTransactionWithBlockhashLifetime,
getSignatureFromTransaction,
address as kitAddress,
} from '@solana/kit'
import { useKitTransactionSigner, useWallet, useCluster } from '@solana/connector/react'
import { getCreateEscrowInstructionAsync } from '@descro/sdk'
import type { Address } from '@descro/sdk'
function getNextEscrowId(walletPubkey: string): BN { function getNextEscrowId(walletAddress: string): bigint {
const key = `descro_escrow_id_${walletPubkey}` const key = `descro_escrow_id_${walletAddress}`
const stored = localStorage.getItem(key) const stored = localStorage.getItem(key)
const current = stored ? parseInt(stored, 10) : 0 const current = stored ? parseInt(stored, 10) : 0
const next = current + 1 const next = current + 1
localStorage.setItem(key, next.toString()) localStorage.setItem(key, next.toString())
return new BN(next) return BigInt(next)
} }
interface CreateEscrowFormProps { interface CreateEscrowFormProps {
@@ -23,17 +34,10 @@ interface CreateEscrowFormProps {
} }
export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) { export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
const { signer } = useTransactionSigner() const { signer } = useKitTransactionSigner()
const { disconnect } = useDisconnectWallet() const { account: address } = useWallet()
const { cluster } = useCluster() const { cluster } = useCluster()
const walletAdapter = useWalletAdapterCompat(signer, disconnect) const rpcUrl = cluster?.url ?? null
const connection = useMemo(
() => (cluster?.url ? new Connection(cluster.url, 'confirmed') : null),
[cluster?.url],
)
const publicKey = walletAdapter.publicKey ? new PublicKey(walletAdapter.publicKey.toString()) : null
const [buyer, setBuyer] = useState('') const [buyer, setBuyer] = useState('')
const [amountSol, setAmountSol] = useState('') const [amountSol, setAmountSol] = useState('')
@@ -47,29 +51,30 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
setError(null) setError(null)
setSuccess(null) setSuccess(null)
if (!publicKey || !connection) { if (!signer || !address || !rpcUrl) {
setError('Connect your wallet first.') setError('Connect your wallet first.')
return return
} }
let buyerPk: PublicKey let buyerAddress: Address
try { try {
buyerPk = new PublicKey(buyer) buyerAddress = kitAddress(buyer)
} catch { } catch {
setError('Invalid buyer public key.') setError('Invalid buyer public key.')
return return
} }
const amountLamports = parseFloat(amountSol) const amountSolNum = parseFloat(amountSol)
if (isNaN(amountLamports) || amountLamports <= 0) { if (isNaN(amountSolNum) || amountSolNum <= 0) {
setError('Invalid SOL amount.') setError('Invalid SOL amount.')
return return
} }
const amountLamports = BigInt(Math.round(amountSolNum * 1e9))
let resolverPk: PublicKey | null = null let resolverAddress: Address | null = null
if (resolver.trim()) { if (resolver.trim()) {
try { try {
resolverPk = new PublicKey(resolver.trim()) resolverAddress = kitAddress(resolver.trim())
} catch { } catch {
setError('Invalid resolver public key.') setError('Invalid resolver public key.')
return return
@@ -78,29 +83,33 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
setLoading(true) setLoading(true)
try { try {
const provider = new AnchorProvider( const rpc = createSolanaRpc(rpcUrl)
connection, const rpcSubscriptions = createSolanaRpcSubscriptions(rpcUrl.replace('http', 'ws'))
{ publicKey, signTransaction: walletAdapter.signTransaction!.bind(walletAdapter) } as never, const escrowId = getNextEscrowId(address)
{},
)
const client = new EscrowClient(provider)
const escrowId = getNextEscrowId(publicKey.toBase58())
const amount = new BN(Math.round(amountLamports * 1e9))
const ix = await client.buildCreateEscrow({ const ix = await getCreateEscrowInstructionAsync({
seller: publicKey, seller: signer,
buyer: buyerPk, buyer: buyerAddress,
amount, amount: amountLamports,
disputeResolver: resolverPk, disputeResolver: resolverAddress,
escrowId, escrowId,
}) })
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
const tx = new Transaction({ blockhash, lastValidBlockHeight, feePayer: publicKey }).add(ix)
const sig = await walletAdapter.sendTransaction(tx, connection)
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed')
setSuccess(`Escrow #${escrowId.toString()} created! Tx: ${sig.slice(0, 16)}`) const txMsg = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([ix as never], tx),
)
const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed)
setSuccess(`Escrow #${escrowId} created! Tx: ${sig.slice(0, 16)}`)
setBuyer('') setBuyer('')
setAmountSol('') setAmountSol('')
setResolver('') setResolver('')
@@ -164,7 +173,7 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
<Button <Button
theme="active" theme="active"
onPress={handleSubmit as unknown as () => void} onPress={handleSubmit as unknown as () => void}
disabled={loading || !publicKey} disabled={loading || !address}
icon={loading ? <Spinner /> : undefined} icon={loading ? <Spinner /> : undefined}
> >
{loading ? 'Creating…' : 'Create Escrow'} {loading ? 'Creating…' : 'Create Escrow'}

View File

@@ -4,11 +4,8 @@ import React from 'react'
import { YStack, XStack, Text } from 'tamagui' import { YStack, XStack, Text } from 'tamagui'
import { StatusBadge } from './StatusBadge' import { StatusBadge } from './StatusBadge'
import type { EscrowAccountWithPda } from '@descro/sdk' import type { EscrowAccountWithPda } from '@descro/sdk'
import { PublicKey } from '@solana/web3.js' function truncAddr(addr: string): string {
return `${addr.slice(0, 4)}${addr.slice(-4)}`
function truncPubkey(pk: PublicKey): string {
const s = pk.toBase58()
return `${s.slice(0, 4)}${s.slice(-4)}`
} }
interface EscrowCardProps { interface EscrowCardProps {
@@ -47,16 +44,16 @@ export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) {
<XStack gap="$4"> <XStack gap="$4">
<YStack> <YStack>
<Text fontSize={11} color="$color10">Seller</Text> <Text fontSize={11} color="$color10">Seller</Text>
<Text fontSize={12} fontFamily="monospace">{truncPubkey(account.seller)}</Text> <Text fontSize={12} fontFamily="monospace">{truncAddr(account.seller)}</Text>
</YStack> </YStack>
<YStack> <YStack>
<Text fontSize={11} color="$color10">Buyer</Text> <Text fontSize={11} color="$color10">Buyer</Text>
<Text fontSize={12} fontFamily="monospace">{truncPubkey(account.buyer)}</Text> <Text fontSize={12} fontFamily="monospace">{truncAddr(account.buyer)}</Text>
</YStack> </YStack>
</XStack> </XStack>
<Text fontSize={11} color="$color10" fontFamily="monospace"> <Text fontSize={11} color="$color10" fontFamily="monospace">
PDA: {truncPubkey(pda)} PDA: {truncAddr(pda)}
</Text> </Text>
</YStack> </YStack>
) )

View File

@@ -1,13 +1,36 @@
'use client' 'use client'
import { useState, useMemo } from 'react' import { useState } from 'react'
import { YStack, XStack, Text, Button, Spinner, Separator } from 'tamagui' import { YStack, XStack, Text, Button, Spinner, Separator } from 'tamagui'
import { PublicKey, Transaction, Connection } from '@solana/web3.js' import {
import { AnchorProvider } from '@coral-xyz/anchor' createSolanaRpc,
import { useTransactionSigner, useDisconnectWallet, useCluster } from '@solana/connector/react' createSolanaRpcSubscriptions,
import { useWalletAdapterCompat } from '@solana/connector/compat' pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
assertIsTransactionWithBlockhashLifetime,
getSignatureFromTransaction,
isSome,
} from '@solana/kit'
import { useKitTransactionSigner, useWallet, useCluster } from '@solana/connector/react'
import { StatusBadge } from './StatusBadge' import { StatusBadge } from './StatusBadge'
import { EscrowClient, isAwaitingDeposit, isActive, isDisputed } from '@descro/sdk' import {
isAwaitingDeposit,
isActive,
isDisputed,
getDepositInstructionAsync,
getCancelInstruction,
getCompleteInstructionAsync,
getDisputeInstruction,
getResolveInstructionAsync,
findResolverEntryPda,
DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS,
Winner,
} from '@descro/sdk'
import type { EscrowAccountWithPda } from '@descro/sdk' import type { EscrowAccountWithPda } from '@descro/sdk'
import { useEscrowDetail } from '@/hooks/useEscrowDetail' import { useEscrowDetail } from '@/hooks/useEscrowDetail'
@@ -16,41 +39,28 @@ interface EscrowDetailProps {
onAction?: () => void onAction?: () => void
} }
function fullPubkey(pk: PublicKey): string {
return pk.toBase58()
}
export function EscrowDetail({ item, onAction }: EscrowDetailProps) { export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
const { pda } = item const { pda } = item
const { account, loading: detailLoading } = useEscrowDetail(pda) const { account, loading: detailLoading } = useEscrowDetail(pda)
const escrow = account ?? item.account const escrow = account ?? item.account
const { signer } = useTransactionSigner() const { signer } = useKitTransactionSigner()
const { disconnect } = useDisconnectWallet() const { account: address } = useWallet()
const { cluster } = useCluster() const { cluster } = useCluster()
const walletAdapter = useWalletAdapterCompat(signer, disconnect) const rpcUrl = cluster?.url ?? null
const connection = useMemo(
() => (cluster?.url ? new Connection(cluster.url, 'confirmed') : null),
[cluster?.url],
)
const publicKey = walletAdapter.publicKey ? new PublicKey(walletAdapter.publicKey.toString()) : null
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null) const [success, setSuccess] = useState<string | null>(null)
const solAmount = (Number(escrow.amount.toString()) / 1e9).toFixed(4) const solAmount = (Number(escrow.amount) / 1e9).toFixed(4)
const isSeller = publicKey?.equals(escrow.seller) ?? false const isSeller = address != null && address === escrow.seller
const isBuyer = publicKey?.equals(escrow.buyer) ?? false const isBuyer = address != null && address === escrow.buyer
const isResolver = const resolverAddr = isSome(escrow.disputeResolver) ? escrow.disputeResolver.value : null
escrow.disputeResolver != null && publicKey?.equals(escrow.disputeResolver) === true const isResolver = address != null && resolverAddr != null && address === resolverAddr
async function runTx( async function runTx(buildIx: () => Promise<unknown>) {
buildFn: (client: EscrowClient) => Promise<import('@solana/web3.js').TransactionInstruction>, if (!signer || !address || !rpcUrl) {
) {
if (!publicKey || !connection || !walletAdapter.signTransaction) {
setError('Connect your wallet first.') setError('Connect your wallet first.')
return return
} }
@@ -58,17 +68,20 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
setError(null) setError(null)
setSuccess(null) setSuccess(null)
try { try {
const provider = new AnchorProvider( const rpc = createSolanaRpc(rpcUrl)
connection, const rpcSubscriptions = createSolanaRpcSubscriptions(rpcUrl.replace('http', 'ws'))
{ publicKey, signTransaction: walletAdapter.signTransaction.bind(walletAdapter) } as never, const ix = await buildIx()
{}, const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
const txMsg = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([ix as never], tx),
) )
const client = new EscrowClient(provider) const signed = await signTransactionMessageWithSigners(txMsg)
const ix = await buildFn(client) assertIsTransactionWithBlockhashLifetime(signed)
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
const tx = new Transaction({ blockhash, lastValidBlockHeight, feePayer: publicKey }).add(ix) const sig = getSignatureFromTransaction(signed)
const sig = await walletAdapter.sendTransaction(tx, connection)
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed')
setSuccess(`Done! Tx: ${sig.slice(0, 20)}`) setSuccess(`Done! Tx: ${sig.slice(0, 20)}`)
onAction?.() onAction?.()
} catch (err: unknown) { } catch (err: unknown) {
@@ -79,31 +92,33 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
} }
const handleDeposit = () => const handleDeposit = () =>
runTx((client) => client.buildDeposit({ buyer: publicKey!, escrowPda: pda })) runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda }))
const handleCancel = () => const handleCancel = () =>
runTx((client) => client.buildCancel({ seller: publicKey!, escrowPda: pda })) runTx(async () => getCancelInstruction({ seller: signer!, escrowAccount: pda }))
const handleComplete = () => const handleComplete = () =>
runTx((client) => runTx(() =>
client.buildComplete({ buyer: publicKey!, seller: escrow.seller, escrowPda: pda }), getCompleteInstructionAsync({ buyer: signer!, seller: escrow.seller, escrowAccount: pda }),
) )
const handleDispute = () => const handleDispute = () =>
runTx((client) => client.buildDispute({ initiator: publicKey!, escrowPda: pda })) runTx(async () => getDisputeInstruction({ initiator: signer!, escrowAccount: pda }))
const handleResolve = (winner: 'buyer' | 'seller') => { const handleResolve = (winner: 'buyer' | 'seller') => {
const winnerPubkey = winner === 'buyer' ? escrow.buyer : escrow.seller if (!resolverAddr) return
runTx((client) => runTx(async () => {
client.buildResolve({ const [resolverEntryAddr] = await findResolverEntryPda({ authority: resolverAddr })
resolver: publicKey!, return getResolveInstructionAsync({
winner, resolver: signer!,
winnerPubkey, winner: winner === 'buyer' ? escrow.buyer : escrow.seller,
seller: escrow.seller, seller: escrow.seller,
escrowPda: pda, escrowAccount: pda,
disputeResolver: escrow.disputeResolver, resolverEntry: resolverEntryAddr,
}), registryProgram: DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS,
) winnerArg: winner === 'buyer' ? Winner.Buyer : Winner.Seller,
})
})
} }
void detailLoading void detailLoading
@@ -121,14 +136,14 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
<YStack gap="$2"> <YStack gap="$2">
<Row label="Amount" value={`${solAmount} SOL`} /> <Row label="Amount" value={`${solAmount} SOL`} />
<Row label="Seller" value={fullPubkey(escrow.seller)} mono /> <Row label="Seller" value={escrow.seller} mono />
<Row label="Buyer" value={fullPubkey(escrow.buyer)} mono /> <Row label="Buyer" value={escrow.buyer} mono />
<Row <Row
label="Resolver" label="Resolver"
value={escrow.disputeResolver ? fullPubkey(escrow.disputeResolver) : 'None'} value={resolverAddr ?? 'None'}
mono={!!escrow.disputeResolver} mono={!!resolverAddr}
/> />
<Row label="PDA" value={fullPubkey(pda)} mono /> <Row label="PDA" value={pda} mono />
</YStack> </YStack>
<Separator /> <Separator />

View File

@@ -1,39 +1,47 @@
'use client' 'use client'
import React, { useState, useEffect, useMemo } from 'react' import React, { useState, useEffect } from 'react'
import { YStack, XStack, Text, Button, Input, Spinner, Separator } from 'tamagui' import { YStack, XStack, Text, Button, Input, Spinner, Separator } from 'tamagui'
import { PublicKey, Transaction, Connection } from '@solana/web3.js' import {
import { AnchorProvider } from '@coral-xyz/anchor' createSolanaRpc,
import { useTransactionSigner, useDisconnectWallet, useCluster } from '@solana/connector/react' createSolanaRpcSubscriptions,
import { useWalletAdapterCompat } from '@solana/connector/compat' pipe,
import { RegistryClient, resolverTypeLabel } from '@descro/sdk' createTransactionMessage,
import type { ResolverEntryWithPda, ResolverType } from '@descro/sdk' setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
assertIsTransactionWithBlockhashLifetime,
getSignatureFromTransaction,
address as kitAddress,
} from '@solana/kit'
import { useKitTransactionSigner, useWallet, useCluster } from '@solana/connector/react'
import {
fetchAllResolvers,
getRegisterResolverInstructionAsync,
resolverTypeLabel,
ResolverType,
} from '@descro/sdk'
import type { ResolverEntryWithPda } from '@descro/sdk'
const RESOLVER_TYPE_OPTIONS: { label: string; value: ResolverType }[] = [ const RESOLVER_TYPE_OPTIONS: { label: string; value: ResolverType }[] = [
{ label: 'CentralAuthority', value: { centralAuthority: {} } }, { label: 'CentralAuthority', value: ResolverType.CentralAuthority },
{ label: 'JuryDAO', value: { juryDAO: {} } }, { label: 'JuryDAO', value: ResolverType.JuryDAO },
{ label: 'MAD', value: { mad: {} } }, { label: 'MAD', value: ResolverType.MAD },
{ label: 'Algorithmic', value: { algorithmic: {} } }, { label: 'Algorithmic', value: ResolverType.Algorithmic },
{ label: 'Multisig', value: { multisig: {} } }, { label: 'Multisig', value: ResolverType.Multisig },
] ]
function truncPk(pk: PublicKey): string { function truncAddr(addr: string): string {
const s = pk.toBase58() return `${addr.slice(0, 4)}${addr.slice(-4)}`
return `${s.slice(0, 4)}${s.slice(-4)}`
} }
export function ResolverPanel() { export function ResolverPanel() {
const { signer } = useTransactionSigner() const { signer } = useKitTransactionSigner()
const { disconnect } = useDisconnectWallet() const { account: address } = useWallet()
const { cluster } = useCluster() const { cluster } = useCluster()
const walletAdapter = useWalletAdapterCompat(signer, disconnect) const rpcUrl = cluster?.url ?? null
const connection = useMemo(
() => (cluster?.url ? new Connection(cluster.url, 'confirmed') : null),
[cluster?.url],
)
const publicKey = walletAdapter.publicKey ? new PublicKey(walletAdapter.publicKey.toString()) : null
const [resolvers, setResolvers] = useState<ResolverEntryWithPda[]>([]) const [resolvers, setResolvers] = useState<ResolverEntryWithPda[]>([])
const [resolversLoading, setResolversLoading] = useState(false) const [resolversLoading, setResolversLoading] = useState(false)
@@ -50,12 +58,11 @@ export function ResolverPanel() {
const [success, setSuccess] = useState<string | null>(null) const [success, setSuccess] = useState<string | null>(null)
async function loadResolvers() { async function loadResolvers() {
if (!connection) return if (!rpcUrl) return
setResolversLoading(true) setResolversLoading(true)
try { try {
const provider = new AnchorProvider(connection, {} as never, {}) const rpc = createSolanaRpc(rpcUrl)
const client = new RegistryClient(provider) const all = await fetchAllResolvers(rpc)
const all = await client.fetchAllResolvers()
setResolvers(all) setResolvers(all)
} catch (err) { } catch (err) {
console.error('Failed to load resolvers', err) console.error('Failed to load resolvers', err)
@@ -67,22 +74,22 @@ export function ResolverPanel() {
useEffect(() => { useEffect(() => {
loadResolvers() loadResolvers()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [connection]) }, [rpcUrl])
async function handleRegister(e: React.FormEvent) { async function handleRegister(e: React.FormEvent) {
e.preventDefault() e.preventDefault()
setError(null) setError(null)
setSuccess(null) setSuccess(null)
if (!publicKey || !connection || !walletAdapter.signTransaction) { if (!signer || !address || !rpcUrl) {
setError('Connect your wallet first.') setError('Connect your wallet first.')
return return
} }
let recipientPk: PublicKey let recipientAddr
const recipientStr = feeRecipient.trim() || publicKey.toBase58() const recipientStr = feeRecipient.trim() || address
try { try {
recipientPk = new PublicKey(recipientStr) recipientAddr = kitAddress(recipientStr)
} catch { } catch {
setError('Invalid fee recipient public key.') setError('Invalid fee recipient public key.')
return return
@@ -101,28 +108,31 @@ export function ResolverPanel() {
setFormLoading(true) setFormLoading(true)
try { try {
const provider = new AnchorProvider( const rpc = createSolanaRpc(rpcUrl)
connection, const rpcSubscriptions = createSolanaRpcSubscriptions(rpcUrl.replace('http', 'ws'))
{ publicKey, signTransaction: walletAdapter.signTransaction.bind(walletAdapter) } as never,
{},
)
const client = new RegistryClient(provider)
const resolverType = RESOLVER_TYPE_OPTIONS[resolverTypeIdx].value const resolverType = RESOLVER_TYPE_OPTIONS[resolverTypeIdx].value
const ix = await client.buildRegisterResolver({ const ix = await getRegisterResolverInstructionAsync({
authority: publicKey, authority: signer,
resolverType, resolverType,
name: name.trim(), name: name.trim(),
description: description.trim(), description: description.trim(),
feeBps: fee, feeBps: fee,
feeRecipient: recipientPk, feeRecipient: recipientAddr,
metadataUri: metadataUri.trim(), metadataUri: metadataUri.trim(),
}) })
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
const tx = new Transaction({ blockhash, lastValidBlockHeight, feePayer: publicKey }).add(ix) const txMsg = pipe(
const sig = await walletAdapter.sendTransaction(tx, connection) createTransactionMessage({ version: 0 }),
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed') (tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([ix as never], tx),
)
const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed)
setSuccess(`Registered! Tx: ${sig.slice(0, 20)}`) setSuccess(`Registered! Tx: ${sig.slice(0, 20)}`)
setName('') setName('')
@@ -156,7 +166,7 @@ export function ResolverPanel() {
{resolvers.map(({ pda, account }) => ( {resolvers.map(({ pda, account }) => (
<YStack <YStack
key={pda.toBase58()} key={pda}
padding="$3" padding="$3"
borderRadius="$3" borderRadius="$3"
borderWidth={1} borderWidth={1}
@@ -177,7 +187,7 @@ export function ResolverPanel() {
<Text fontSize={12} color="$orange9">S: {account.ruledForSeller.toString()}</Text> <Text fontSize={12} color="$orange9">S: {account.ruledForSeller.toString()}</Text>
</XStack> </XStack>
<Text fontSize={11} color="$color10" style={{ fontFamily: 'monospace' }}> <Text fontSize={11} color="$color10" style={{ fontFamily: 'monospace' }}>
{truncPk(account.authority)} {truncAddr(account.authority)}
</Text> </Text>
</YStack> </YStack>
))} ))}
@@ -241,7 +251,7 @@ export function ResolverPanel() {
<Button <Button
theme="active" theme="active"
onPress={handleRegister as unknown as () => void} onPress={handleRegister as unknown as () => void}
disabled={formLoading || !publicKey} disabled={formLoading || !address}
icon={formLoading ? <Spinner /> : undefined} icon={formLoading ? <Spinner /> : undefined}
> >
{formLoading ? 'Registering…' : 'Register'} {formLoading ? 'Registering…' : 'Register'}

View File

@@ -1,40 +1,33 @@
'use client' 'use client'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect } from 'react'
import { PublicKey, Connection } from '@solana/web3.js' import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit'
import { useCluster } from '@solana/connector/react' import { useCluster } from '@solana/connector/react'
import { AnchorProvider } from '@coral-xyz/anchor' import { fetchMaybeEscrowAccount, subscribeEscrow } from '@descro/sdk'
import { EscrowClient, subscribeEscrow } from '@descro/sdk' import type { Address, EscrowAccount } from '@descro/sdk'
import type { EscrowAccount } from '@descro/sdk'
export function useEscrowDetail(pda: PublicKey | null) { export function useEscrowDetail(pda: Address | null) {
const { cluster } = useCluster() const { cluster } = useCluster()
const connection = useMemo( const rpcUrl = cluster?.url ?? null
() => (cluster?.url ? new Connection(cluster.url) : null),
[cluster?.url],
)
const [account, setAccount] = useState<EscrowAccount | null>(null) const [account, setAccount] = useState<EscrowAccount | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const unsubRef = useRef<(() => void) | null>(null)
useEffect(() => { useEffect(() => {
if (!pda || !connection) { if (!pda || !rpcUrl) {
setAccount(null) setAccount(null)
return return
} }
let cancelled = false let cancelled = false
setLoading(true) setLoading(true)
const provider = new AnchorProvider(connection, {} as never, {}) const rpc = createSolanaRpc(rpcUrl)
const client = new EscrowClient(provider)
client fetchMaybeEscrowAccount(rpc, pda)
.fetchEscrow(pda) .then((maybeAcc) => {
.then((acc) => {
if (!cancelled) { if (!cancelled) {
setAccount(acc) setAccount(maybeAcc.exists ? maybeAcc.data : null)
setError(null) setError(null)
} }
}) })
@@ -45,17 +38,17 @@ export function useEscrowDetail(pda: PublicKey | null) {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false)
}) })
const unsub = subscribeEscrow(connection, pda, (updated) => { const wsUrl = rpcUrl.replace('http', 'ws')
const rpcSubscriptions = createSolanaRpcSubscriptions(wsUrl)
const unsub = subscribeEscrow(rpcSubscriptions, pda, (updated) => {
if (!cancelled) setAccount(updated) if (!cancelled) setAccount(updated)
}) })
unsubRef.current = unsub
return () => { return () => {
cancelled = true cancelled = true
unsub() unsub()
unsubRef.current = null
} }
}, [pda?.toBase58(), connection]) }, [pda, rpcUrl])
return { account, loading, error } return { account, loading, error }
} }

View File

@@ -1,23 +1,17 @@
'use client' 'use client'
import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { Connection, PublicKey } from '@solana/web3.js' import { createSolanaRpc } from '@solana/kit'
import { useTransactionSigner, useCluster } from '@solana/connector/react' import { useWallet, useCluster } from '@solana/connector/react'
import { AnchorProvider } from '@coral-xyz/anchor' import { fetchEscrowsForWallet } from '@descro/sdk'
import { EscrowClient } from '@descro/sdk'
import type { EscrowAccountWithPda } from '@descro/sdk' import type { EscrowAccountWithPda } from '@descro/sdk'
const POLL_INTERVAL_MS = 5000 const POLL_INTERVAL_MS = 5000
export function useEscrows() { export function useEscrows() {
const { address } = useTransactionSigner() const { account: address } = useWallet()
const { cluster } = useCluster() const { cluster } = useCluster()
const rpcUrl = cluster?.url ?? null
const publicKey = useMemo(() => (address ? new PublicKey(address) : null), [address])
const connection = useMemo(
() => (cluster?.url ? new Connection(cluster.url) : null),
[cluster?.url],
)
const [escrows, setEscrows] = useState<EscrowAccountWithPda[]>([]) const [escrows, setEscrows] = useState<EscrowAccountWithPda[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -25,23 +19,20 @@ export function useEscrows() {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null) const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const fetchEscrows = useCallback(async () => { const fetchEscrows = useCallback(async () => {
if (!publicKey || !connection) { if (!address || !rpcUrl) {
setEscrows([]) setEscrows([])
return return
} }
try { try {
const provider = new AnchorProvider(connection, {} as never, {}) const rpc = createSolanaRpc(rpcUrl)
const client = new EscrowClient(provider) const results = await fetchEscrowsForWallet(rpc, address)
const results = await client.fetchEscrowsForWallet(publicKey) results.sort((a, b) => Number(b.account.escrowId - a.account.escrowId))
results.sort(
(a, b) => Number(b.account.escrowId.toString()) - Number(a.account.escrowId.toString()),
)
setEscrows(results) setEscrows(results)
setError(null) setError(null)
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : String(err)) setError(err instanceof Error ? err.message : String(err))
} }
}, [publicKey, connection]) }, [address, rpcUrl])
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2020",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",

View File

@@ -2,4 +2,45 @@ export * from "./types";
export * from "./pda"; export * from "./pda";
export * from "./escrow"; export * from "./escrow";
export * from "./registry"; export * from "./registry";
export * from "./listener"; export * from "./listener";
export {
getCreateEscrowInstructionAsync,
getCreateEscrowInstruction,
type CreateEscrowAsyncInput,
type CreateEscrowInput,
} from "./generated/descro/src/generated/instructions/createEscrow";
export {
getDepositInstructionAsync,
getDepositInstruction,
type DepositAsyncInput,
type DepositInput,
} from "./generated/descro/src/generated/instructions/deposit";
export {
getCompleteInstructionAsync,
getCompleteInstruction,
type CompleteAsyncInput,
type CompleteInput,
} from "./generated/descro/src/generated/instructions/complete";
export {
getCancelInstruction,
type CancelInput,
} from "./generated/descro/src/generated/instructions/cancel";
export {
getDisputeInstruction,
type DisputeInput,
} from "./generated/descro/src/generated/instructions/dispute";
export {
getResolveInstructionAsync,
getResolveInstruction,
type ResolveAsyncInput,
type ResolveInput,
} from "./generated/descro/src/generated/instructions/resolve";
export {
getRegisterResolverInstructionAsync,
getRegisterResolverInstruction,
type RegisterResolverAsyncInput,
type RegisterResolverInput,
} from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver";
export { Winner } from "./generated/descro/src/generated/types/winner";
export { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType";

View File

@@ -1 +1,49 @@
export * from "./generated/descro_ext_resolvers/src/generated/accounts"; import type { GetProgramAccountsApi, Lamports, Rpc } from "@solana/kit";
import {
fetchMaybeResolverEntry,
fetchResolverEntry,
decodeResolverEntry,
getResolverEntryDiscriminatorBytes,
} from "./generated/descro_ext_resolvers/src/generated/accounts/resolverEntry";
import { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "./generated/descro_ext_resolvers/src/generated/programs/descroExtResolvers";
import type { ResolverEntryWithPda } from "./types";
export { fetchResolverEntry, fetchMaybeResolverEntry, decodeResolverEntry };
export async function fetchAllResolvers(
rpc: Rpc<GetProgramAccountsApi>,
): Promise<ResolverEntryWithPda[]> {
const discriminatorBase64 = btoa(
String.fromCharCode(...getResolverEntryDiscriminatorBytes()),
) as never;
const accounts = await rpc
.getProgramAccounts(DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS, {
filters: [
{ memcmp: { offset: 0n, bytes: discriminatorBase64, encoding: "base64" } },
],
encoding: "base64",
})
.send();
const results: ResolverEntryWithPda[] = [];
for (const item of accounts) {
const pda = item.pubkey;
const [base64Data] = item.account.data;
const rawBytes = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0));
const decoded = decodeResolverEntry({
address: pda,
data: rawBytes,
executable: item.account.executable,
lamports: item.account.lamports as Lamports,
programAddress: item.account.owner,
space: item.account.space,
exists: true,
});
if (decoded.exists) results.push({ pda, account: decoded.data });
}
return results;
}

View File

@@ -394,46 +394,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@coral-xyz/anchor-errors@npm:^0.31.1":
version: 0.31.1
resolution: "@coral-xyz/anchor-errors@npm:0.31.1"
checksum: 10c0/d359d6244a89bcfb606e6d31d545c3be8e32dccd47b78dac6ec0091a51b2eb6c78aad9601f39db2659d8e66db8fffc36477247d58d570bc9d866e7ec3e40b5b2
languageName: node
linkType: hard
"@coral-xyz/anchor@npm:^0.32.1":
version: 0.32.1
resolution: "@coral-xyz/anchor@npm:0.32.1"
dependencies:
"@coral-xyz/anchor-errors": "npm:^0.31.1"
"@coral-xyz/borsh": "npm:^0.31.1"
"@noble/hashes": "npm:^1.3.1"
"@solana/web3.js": "npm:^1.69.0"
bn.js: "npm:^5.1.2"
bs58: "npm:^4.0.1"
buffer-layout: "npm:^1.2.2"
camelcase: "npm:^6.3.0"
cross-fetch: "npm:^3.1.5"
eventemitter3: "npm:^4.0.7"
pako: "npm:^2.0.3"
superstruct: "npm:^0.15.4"
toml: "npm:^3.0.0"
checksum: 10c0/669b6f54efb0a5a96dad9e055d30b3b803f691b1b058379efefb87c11f97a9d369d513b2f200082c0b55b4cb31540dc5404e244c6f23f0a14b6d2ad2e18f08b7
languageName: node
linkType: hard
"@coral-xyz/borsh@npm:^0.31.1":
version: 0.31.1
resolution: "@coral-xyz/borsh@npm:0.31.1"
dependencies:
bn.js: "npm:^5.1.2"
buffer-layout: "npm:^1.2.0"
peerDependencies:
"@solana/web3.js": ^1.69.0
checksum: 10c0/3d6d40a1476df5eb0635d8687a640de89fd7e7ffd135f733907f13ff2569a2a07f65f64768d632fb45ed55006d9c9ee94e40fd775813a9cf1211fefa567f98dd
languageName: node
linkType: hard
"@descro/sdk@workspace:*, @descro/sdk@workspace:sdk": "@descro/sdk@workspace:*, @descro/sdk@workspace:sdk":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@descro/sdk@workspace:sdk" resolution: "@descro/sdk@workspace:sdk"
@@ -441,6 +401,7 @@ __metadata:
"@codama/nodes-from-anchor": "npm:^1.4.1" "@codama/nodes-from-anchor": "npm:^1.4.1"
"@codama/renderers-js": "npm:^2.2.0" "@codama/renderers-js": "npm:^2.2.0"
"@solana/kit": "npm:^6.0.0" "@solana/kit": "npm:^6.0.0"
"@solana/program-client-core": "npm:^6.4.0"
codama: "npm:^1.6.0" codama: "npm:^1.6.0"
typescript: "npm:^6.0.3" typescript: "npm:^6.0.3"
languageName: unknown languageName: unknown
@@ -2136,7 +2097,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@solana/program-client-core@npm:6.9.0": "@solana/program-client-core@npm:6.9.0, @solana/program-client-core@npm:^6.4.0":
version: 6.9.0 version: 6.9.0
resolution: "@solana/program-client-core@npm:6.9.0" resolution: "@solana/program-client-core@npm:6.9.0"
dependencies: dependencies:
@@ -2927,7 +2888,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@solana/web3.js@npm:^1.69.0, @solana/web3.js@npm:^1.98.4": "@solana/web3.js@npm:^1.69.0":
version: 1.98.4 version: 1.98.4
resolution: "@solana/web3.js@npm:1.98.4" resolution: "@solana/web3.js@npm:1.98.4"
dependencies: dependencies:
@@ -5844,10 +5805,9 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "descro-app@workspace:app" resolution: "descro-app@workspace:app"
dependencies: dependencies:
"@coral-xyz/anchor": "npm:^0.32.1"
"@descro/sdk": "workspace:*" "@descro/sdk": "workspace:*"
"@solana/connector": "npm:^0.2.4" "@solana/connector": "npm:^0.2.4"
"@solana/web3.js": "npm:^1.98.4" "@solana/kit": "npm:^6.0.0"
"@tamagui/cli": "npm:^2.0.0-rc.42" "@tamagui/cli": "npm:^2.0.0-rc.42"
"@tamagui/config": "npm:^2.0.0-rc.42" "@tamagui/config": "npm:^2.0.0-rc.42"
"@tamagui/next-theme": "npm:^2.0.0-rc.42" "@tamagui/next-theme": "npm:^2.0.0-rc.42"