diff --git a/app/src/components/CreateEscrowForm.tsx b/app/src/components/CreateEscrowForm.tsx index 7e4165c..7edcac2 100644 --- a/app/src/components/CreateEscrowForm.tsx +++ b/app/src/components/CreateEscrowForm.tsx @@ -16,6 +16,7 @@ import { } from '@solana/kit' import { useKitTransactionSigner, useWallet, useCluster, useSolanaClient } from '@solana/connector/react' import { getCreateEscrowInstructionAsync } from '@descro/sdk' +import { lamportsFromSol } from '@/util/lamports' import type { Address } from '@descro/sdk' function getNextEscrowId(walletAddress: string): bigint { @@ -66,7 +67,12 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) { setError('Invalid SOL amount.') return } - const amountLamports = BigInt(Math.round(amountSolNum * 1e9)) + 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) { + setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).') + return + } let resolverAddress: Address | null = null if (resolver.trim()) { diff --git a/app/src/components/EscrowCard.tsx b/app/src/components/EscrowCard.tsx index e6607f2..97b52d9 100644 --- a/app/src/components/EscrowCard.tsx +++ b/app/src/components/EscrowCard.tsx @@ -4,6 +4,7 @@ import React from 'react' import { YStack, XStack, Text } from 'tamagui' import { StatusBadge } from './StatusBadge' import type { EscrowAccountWithPda } from '@descro/sdk' +import { lamportsToSolString } from '@/util/lamports' function truncAddr(addr: string): string { return `${addr.slice(0, 4)}…${addr.slice(-4)}` } @@ -16,7 +17,7 @@ interface EscrowCardProps { export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) { const { pda, account } = item - const solAmount = (Number(account.amount.toString()) / 1e9).toFixed(4) + const solAmount = lamportsToSolString(account.amount, 4) return ( (null) const [success, setSuccess] = useState(null) - const solAmount = (Number(escrow.amount) / 1e9).toFixed(4) + const solAmount = lamportsToSolString(escrow.amount, 4) const isSeller = address != null && address === escrow.seller const isBuyer = address != null && address === escrow.buyer const resolverAddr = isSome(escrow.disputeResolver) ? escrow.disputeResolver.value : null diff --git a/app/src/util/lamports.ts b/app/src/util/lamports.ts new file mode 100644 index 0000000..7fb4825 --- /dev/null +++ b/app/src/util/lamports.ts @@ -0,0 +1,20 @@ +import { lamports, type Lamports } from "@solana/kit"; + +const LAMPORTS_PER_SOL = 1_000_000_000n; + +export function lamportsFromSol(sol: number): Lamports { + return lamports(BigInt(Math.round(sol * Number(LAMPORTS_PER_SOL)))); +} + +export function lamportsToSolString(amount: Lamports, maxDecimals = 2): string { + const whole = amount / LAMPORTS_PER_SOL; + const fractional = amount % LAMPORTS_PER_SOL; + + if (fractional === 0n) return whole.toString(); + + const decimals = fractional.toString().padStart(9, "0").slice(0, maxDecimals); + + if (decimals.replace(/0+$/, "") === "") return whole.toString(); + + return `${whole}.${decimals.replace(/0+$/, "")}`; +} diff --git a/programs/descro/src/error.rs b/programs/descro/src/error.rs index afdeaec..54a46bd 100644 --- a/programs/descro/src/error.rs +++ b/programs/descro/src/error.rs @@ -16,4 +16,6 @@ pub enum EscrowError { DisputeTimeoutNotReached, #[msg("Vault balance is insufficient for the requested escrow amount")] InsufficientVaultBalance, + #[msg("Amount must be at least rent-exempt minimum for the vault (~890880 lamports)")] + AmountBelowRentMinimum, } diff --git a/programs/descro/src/instructions/create_escrow.rs b/programs/descro/src/instructions/create_escrow.rs index 38c7263..9688244 100644 --- a/programs/descro/src/instructions/create_escrow.rs +++ b/programs/descro/src/instructions/create_escrow.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; use crate::state::{EscrowAccount, EscrowState}; +use crate::EscrowError; #[derive(Accounts)] #[instruction(amount: u64, dispute_resolver: Option, escrow_id: u64)] @@ -36,6 +37,9 @@ pub fn handler( dispute_resolver: Option, escrow_id: u64, ) -> Result<()> { + let rent_min = Rent::get()?.minimum_balance(0); + require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum); + let escrow = &mut ctx.accounts.escrow_account; escrow.seller = ctx.accounts.seller.key(); escrow.buyer = ctx.accounts.buyer.key();