fix errors

This commit is contained in:
thesn10
2026-05-26 21:45:35 +02:00
parent f97792f190
commit 5da7fb2b97
6 changed files with 156 additions and 26 deletions

View File

@@ -14,11 +14,13 @@ import {
getSignatureFromTransaction, getSignatureFromTransaction,
address as kitAddress, address as kitAddress,
} from '@solana/kit' } from '@solana/kit'
import { useKitTransactionSigner, useWallet, useCluster, useSolanaClient } from '@solana/connector/react' import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react'
import { getCreateEscrowInstructionAsync } from '@descro/sdk' import { getCreateEscrowInstructionAsync, getBuyerCreateEscrowInstructionAsync, findResolverEntryPda } from '@descro/sdk'
import { lamportsFromSol } from '@/util/lamports' import { lamportsFromSol } from '@/util/lamports'
import type { Address } from '@descro/sdk' import type { Address } from '@descro/sdk'
const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address
function getNextEscrowId(walletAddress: string): bigint { function getNextEscrowId(walletAddress: string): bigint {
const key = `descro_escrow_id_${walletAddress}` const key = `descro_escrow_id_${walletAddress}`
const stored = localStorage.getItem(key) const stored = localStorage.getItem(key)
@@ -28,6 +30,15 @@ function getNextEscrowId(walletAddress: string): bigint {
return BigInt(next) return BigInt(next)
} }
function getNextBuyerEscrowId(buyerAddress: string, sellerAddress: string): bigint {
const key = `descro_buyer_escrow_id_${buyerAddress}_${sellerAddress}`
const stored = localStorage.getItem(key)
const current = stored ? parseInt(stored, 10) : 0
const next = current + 1
localStorage.setItem(key, next.toString())
return BigInt(next)
}
interface CreateEscrowFormProps { interface CreateEscrowFormProps {
onCreated?: () => void onCreated?: () => void
} }
@@ -37,7 +48,8 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
const { account: address } = useWallet() const { account: address } = useWallet()
const { client } = useSolanaClient() const { client } = useSolanaClient()
const [buyer, setBuyer] = useState('') const [mode, setMode] = useState<'seller' | 'buyer'>('seller')
const [counterparty, setCounterparty] = useState('')
const [amountSol, setAmountSol] = useState('') const [amountSol, setAmountSol] = useState('')
const [resolver, setResolver] = useState('') const [resolver, setResolver] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -54,11 +66,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
return return
} }
let buyerAddress: Address let counterpartyAddress: Address
try { try {
buyerAddress = kitAddress(buyer) counterpartyAddress = kitAddress(counterparty)
} catch { } catch {
setError('Invalid buyer public key.') setError(`Invalid ${mode === 'seller' ? 'buyer' : 'seller'} public key.`)
return return
} }
@@ -68,7 +80,6 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
return return
} }
const amountLamports = lamportsFromSol(amountSolNum) const amountLamports = lamportsFromSol(amountSolNum)
// Vault is a 0-byte PDA funded entirely by the deposit; must be rent-exempt (~890,880 lamports)
if (amountLamports < 890_880n) { if (amountLamports < 890_880n) {
setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).') setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).')
return return
@@ -87,15 +98,37 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
setLoading(true) setLoading(true)
try { try {
const { rpc, rpcSubscriptions } = client const { rpc, rpcSubscriptions } = client
const escrowId = getNextEscrowId(address)
const ix = await getCreateEscrowInstructionAsync({ let ix: unknown
seller: signer, let escrowIdLabel: bigint
buyer: buyerAddress,
amount: amountLamports, if (mode === 'seller') {
disputeResolver: resolverAddress, const escrowId = getNextEscrowId(address)
escrowId, escrowIdLabel = escrowId
}) const resolverAcct = resolverAddress ?? SYSTEM_PROGRAM_ADDRESS
const [resolverEntryAcct] = resolverAddress
? await findResolverEntryPda({ authority: resolverAddress })
: [SYSTEM_PROGRAM_ADDRESS]
ix = await getCreateEscrowInstructionAsync({
seller: signer,
buyer: counterpartyAddress,
resolver: resolverAcct,
resolverEntry: resolverEntryAcct,
amount: amountLamports,
disputeResolver: resolverAddress,
escrowId,
})
} else {
const escrowId = getNextBuyerEscrowId(address, counterpartyAddress)
escrowIdLabel = escrowId
ix = await getBuyerCreateEscrowInstructionAsync({
buyer: signer,
seller: counterpartyAddress,
amount: amountLamports,
disputeResolver: resolverAddress,
escrowId,
})
}
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send() const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
@@ -108,11 +141,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
const signed = await signTransactionMessageWithSigners(txMsg) const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed) assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' }) await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed) const sig = getSignatureFromTransaction(signed)
setSuccess(`Escrow #${escrowId} created! Tx: ${sig.slice(0, 16)}`) setSuccess(`Escrow #${escrowIdLabel} created! Tx: ${sig.slice(0, 16)}`)
setBuyer('') setCounterparty('')
setAmountSol('') setAmountSol('')
setResolver('') setResolver('')
onCreated?.() onCreated?.()
@@ -123,16 +156,46 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
} }
} }
const counterpartyLabel = mode === 'seller' ? 'Buyer Public Key' : 'Seller Public Key'
const buttonLabel = mode === 'seller' ? 'Create Escrow' : 'Create & Fund Escrow'
return ( return (
<YStack gap="$3"> <YStack gap="$3">
<Text fontSize={16} fontWeight="700">Create Escrow</Text> <Text fontSize={16} fontWeight="700">Create Escrow</Text>
<XStack gap="$2">
<Button
size="$2"
theme={mode === 'seller' ? 'active' : undefined}
onPress={() => { setMode('seller'); setCounterparty(''); setError(null); setSuccess(null) }}
flex={1}
>
I am Seller
</Button>
<Button
size="$2"
theme={mode === 'buyer' ? 'active' : undefined}
onPress={() => { setMode('buyer'); setCounterparty(''); setError(null); setSuccess(null) }}
flex={1}
>
I am Buyer
</Button>
</XStack>
{mode === 'buyer' && (
<XStack backgroundColor="$purple3" padding="$2" borderRadius="$2">
<Text color="$purple9" fontSize={12}>
Buyer flow: you create and fund the escrow immediately. The seller must confirm before the escrow becomes active.
</Text>
</XStack>
)}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<YStack gap="$1"> <YStack gap="$1">
<Text fontSize={13} color="$color11">Buyer Public Key *</Text> <Text fontSize={13} color="$color11">{counterpartyLabel} *</Text>
<Input <Input
value={buyer} value={counterparty}
onChangeText={setBuyer} onChangeText={setCounterparty}
placeholder="Pubkey..." placeholder="Pubkey..."
style={{ fontFamily: 'monospace' }} style={{ fontFamily: 'monospace' }}
fontSize={13} fontSize={13}
@@ -178,7 +241,7 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
disabled={loading || !address} disabled={loading || !address}
icon={loading ? <Spinner /> : undefined} icon={loading ? <Spinner /> : undefined}
> >
{loading ? 'Creating…' : 'Create Escrow'} {loading ? 'Creating…' : buttonLabel}
</Button> </Button>
</form> </form>
</YStack> </YStack>

View File

@@ -18,13 +18,15 @@ import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/con
import { StatusBadge } from './StatusBadge' import { StatusBadge } from './StatusBadge'
import { import {
isAwaitingDeposit, isAwaitingDeposit,
isAwaitingSellerConfirm,
isActive, isActive,
isDisputed, isDisputed,
getDepositInstructionAsync, getDepositInstructionAsync,
getCancelInstruction, getCancelInstructionAsync,
getCompleteInstructionAsync, getCompleteInstructionAsync,
getDisputeInstruction, getDisputeInstruction,
getResolveInstructionAsync, getResolveInstructionAsync,
getSellerConfirmInstruction,
findResolverEntryPda, findResolverEntryPda,
DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS, DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS,
Winner, Winner,
@@ -93,7 +95,22 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda })) runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda }))
const handleCancel = () => const handleCancel = () =>
runTx(async () => getCancelInstruction({ seller: signer!, escrowAccount: pda })) runTx(() => getCancelInstructionAsync({ canceller: signer!, buyer: escrow.buyer, escrowAccount: pda }))
const handleSellerConfirm = () =>
runTx(async () => {
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Parameters<typeof getSellerConfirmInstruction>[0]['resolver']
const resolver = resolverAddr ?? SYSTEM_PROGRAM
const [resolverEntryAddr] = resolverAddr
? await findResolverEntryPda({ authority: resolverAddr })
: [SYSTEM_PROGRAM]
return getSellerConfirmInstruction({
seller: signer!,
resolver,
escrowAccount: pda,
resolverEntry: resolverEntryAddr,
})
})
const handleComplete = () => const handleComplete = () =>
runTx(() => runTx(() =>
@@ -152,7 +169,17 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
Deposit Deposit
</Button> </Button>
)} )}
{isAwaitingDeposit(escrow.state) && isSeller && ( {isAwaitingDeposit(escrow.state) && (isSeller || isBuyer) && (
<Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Cancel
</Button>
)}
{isAwaitingSellerConfirm(escrow.state) && isSeller && (
<Button theme="green" onPress={handleSellerConfirm} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Confirm Escrow
</Button>
)}
{isAwaitingSellerConfirm(escrow.state) && (isSeller || isBuyer) && (
<Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}> <Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Cancel Cancel
</Button> </Button>

View File

@@ -20,6 +20,7 @@ import {
getRegisterResolverInstructionAsync, getRegisterResolverInstructionAsync,
resolverTypeLabel, resolverTypeLabel,
ResolverType, ResolverType,
AcceptancePolicy,
} from '@descro/sdk' } from '@descro/sdk'
import type { ResolverEntryWithPda } from '@descro/sdk' import type { ResolverEntryWithPda } from '@descro/sdk'
@@ -49,6 +50,13 @@ export function ResolverPanel() {
const [feeRecipient, setFeeRecipient] = useState('') const [feeRecipient, setFeeRecipient] = useState('')
const [metadataUri, setMetadataUri] = useState('') const [metadataUri, setMetadataUri] = useState('')
const [resolverTypeIdx, setResolverTypeIdx] = useState(0) const [resolverTypeIdx, setResolverTypeIdx] = useState(0)
const [acceptancePolicyIdx, setAcceptancePolicyIdx] = useState(0)
const ACCEPTANCE_POLICY_OPTIONS: { label: string; value: AcceptancePolicy }[] = [
{ label: 'Open (anyone can assign you)', value: AcceptancePolicy.Open },
{ label: 'SignatureGated (you must co-sign)', value: AcceptancePolicy.SignatureGated },
{ label: 'ProgramGated', value: AcceptancePolicy.ProgramGated },
]
const [formLoading, setFormLoading] = useState(false) const [formLoading, setFormLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -110,6 +118,7 @@ export function ResolverPanel() {
const ix = await getRegisterResolverInstructionAsync({ const ix = await getRegisterResolverInstructionAsync({
authority: signer, authority: signer,
resolverType, resolverType,
acceptancePolicy: ACCEPTANCE_POLICY_OPTIONS[acceptancePolicyIdx].value,
name: name.trim(), name: name.trim(),
description: description.trim(), description: description.trim(),
feeBps: fee, feeBps: fee,
@@ -217,6 +226,19 @@ export function ResolverPanel() {
</select> </select>
</YStack> </YStack>
<YStack gap="$1">
<Text fontSize={13} color="$color11">Acceptance Policy</Text>
<select
value={acceptancePolicyIdx}
onChange={(e) => setAcceptancePolicyIdx(parseInt(e.target.value, 10))}
style={{ padding: '8px', borderRadius: 6, border: '1px solid #ccc', fontSize: 13 }}
>
{ACCEPTANCE_POLICY_OPTIONS.map((opt, i) => (
<option key={i} value={i}>{opt.label}</option>
))}
</select>
</YStack>
<YStack gap="$1"> <YStack gap="$1">
<Text fontSize={13} color="$color11">Fee (BPS)</Text> <Text fontSize={13} color="$color11">Fee (BPS)</Text>
<Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} /> <Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} />

View File

@@ -7,6 +7,7 @@ import type { EscrowState } from '@descro/sdk'
const STATE_COLORS: Record<string, string> = { const STATE_COLORS: Record<string, string> = {
AwaitingDeposit: '$blue9', AwaitingDeposit: '$blue9',
AwaitingSellerConfirm: '$purple9',
Active: '$green9', Active: '$green9',
Disputed: '$orange9', Disputed: '$orange9',
Complete: '$gray9', Complete: '$gray9',

View File

@@ -24,8 +24,20 @@ export {
} from "./generated/descro/src/generated/instructions/complete"; } from "./generated/descro/src/generated/instructions/complete";
export { export {
getCancelInstruction, getCancelInstruction,
getCancelInstructionAsync,
type CancelInput, type CancelInput,
type CancelAsyncInput,
} from "./generated/descro/src/generated/instructions/cancel"; } from "./generated/descro/src/generated/instructions/cancel";
export {
getBuyerCreateEscrowInstructionAsync,
getBuyerCreateEscrowInstruction,
type BuyerCreateEscrowAsyncInput,
type BuyerCreateEscrowInput,
} from "./generated/descro/src/generated/instructions/buyerCreateEscrow";
export {
getSellerConfirmInstruction,
type SellerConfirmInput,
} from "./generated/descro/src/generated/instructions/sellerConfirm";
export { export {
getDisputeInstruction, getDisputeInstruction,
type DisputeInput, type DisputeInput,
@@ -43,4 +55,5 @@ export {
type RegisterResolverInput, type RegisterResolverInput,
} from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver"; } from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver";
export { Winner } from "./generated/descro/src/generated/types/winner"; export { Winner } from "./generated/descro/src/generated/types/winner";
export { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType"; export { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType";
export { AcceptancePolicy } from "./generated/descro_ext_resolvers/src/generated/types/acceptancePolicy";

View File

@@ -32,6 +32,9 @@ export type ResolverEntryWithPda = { pda: Address; account: ResolverEntry };
export function isAwaitingDeposit(state: EscrowState): boolean { export function isAwaitingDeposit(state: EscrowState): boolean {
return state === EscrowState.AwaitingDeposit; return state === EscrowState.AwaitingDeposit;
} }
export function isAwaitingSellerConfirm(state: EscrowState): boolean {
return state === EscrowState.AwaitingSellerConfirm;
}
export function isActive(state: EscrowState): boolean { export function isActive(state: EscrowState): boolean {
return state === EscrowState.Active; return state === EscrowState.Active;
} }
@@ -48,6 +51,7 @@ export function isCancelled(state: EscrowState): boolean {
export function escrowStateLabel(state: EscrowState): string { export function escrowStateLabel(state: EscrowState): string {
switch (state) { switch (state) {
case EscrowState.AwaitingDeposit: return "AwaitingDeposit"; case EscrowState.AwaitingDeposit: return "AwaitingDeposit";
case EscrowState.AwaitingSellerConfirm: return "AwaitingSellerConfirm";
case EscrowState.Active: return "Active"; case EscrowState.Active: return "Active";
case EscrowState.Disputed: return "Disputed"; case EscrowState.Disputed: return "Disputed";
case EscrowState.Complete: return "Complete"; case EscrowState.Complete: return "Complete";