From ee1ac32af0a113ecb8189100a5bfa0281b32710c Mon Sep 17 00:00:00 2001 From: Jefferson Date: Mon, 29 Jun 2026 15:13:29 -0600 Subject: [PATCH 1/2] fix: replace date picker with duration input for target pool deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `deadline()` view function to TargetPool smart contract (u32 ledger seq) - Add `deadlineLedger: number` field to `TargetPoolState` interface - Fetch on-chain deadline in `fetchTargetState` via `viewCall("deadline")` - Add `ledgerToEstimatedDate()` utility for ledger→wall-clock conversion - Replace date-picker with days input; `daysToLedgers(days) = days*86400/6` - Fetch current ledger at submit time for accurate `deadlineLedger` value - Show live "Ledger ~X · Est. {date}" hint below deadline field - Display human date + ledger number in group-details (falls back to DB date) - Fix pre-existing upstream TS error: cast e2eState() fee fields to number Closes #13 --- .../components/create-group/target-form.tsx | 98 +++++++++++++------ frontend/components/group/group-details.tsx | 41 ++++++-- frontend/hooks/useJointSaveContracts.ts | 24 ++++- smartcontract/contracts/target/src/lib.rs | 7 ++ 4 files changed, 128 insertions(+), 42 deletions(-) diff --git a/frontend/components/create-group/target-form.tsx b/frontend/components/create-group/target-form.tsx index 265e416..899d5eb 100644 --- a/frontend/components/create-group/target-form.tsx +++ b/frontend/components/create-group/target-form.tsx @@ -5,7 +5,7 @@ import { useState, useCallback, useRef, useEffect } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" -import { Plus, X, Loader2, AlertCircle } from "lucide-react" +import { Plus, X, Loader2, AlertCircle, Info } from "lucide-react" import { useRouter } from "next/navigation" import { useStellar } from "@/components/web3-provider" import { @@ -24,7 +24,6 @@ import { validateGroupName, validateStellarAddress, validatePositiveAmount, - validateDeadline, findDuplicateAddresses, } from "@/lib/form-validation" import { MAX_POOL_MEMBERS } from "@/lib/constants" @@ -33,17 +32,15 @@ function isValidStellarAddress(addr: string) { return /^G[A-Z2-7]{55}$/.test(addr) } -// Convert a JS Date to an approximate Stellar ledger sequence number. -// Stellar testnet: ~5 ledgers/sec. We fetch current ledger and extrapolate. -async function dateToLedger(date: Date): Promise { - const rpc = getRpc() - const ledger = await rpc.getLatestLedger() - const secsFromNow = Math.max(0, Math.floor((date.getTime() - Date.now()) / 1000)) - return ledger.sequence + Math.floor(secsFromNow * 5) +// Stellar: ~6 seconds per ledger +const SECONDS_PER_LEDGER = 6 + +function daysToLedgers(days: number): number { + return Math.floor((days * 24 * 60 * 60) / SECONDS_PER_LEDGER) } -type FieldErrors = Partial> -type Touched = Partial> +type FieldErrors = Partial> +type Touched = Partial> export function TargetForm() { const router = useRouter() @@ -62,16 +59,24 @@ export function TargetForm() { name: "", description: "", targetAmount: "", - deadline: "", + deadlineDays: "", }) const [fieldErrors, setFieldErrors] = useState({}) const [touched, setTouched] = useState({}) + const [currentLedger, setCurrentLedger] = useState(null) const errorRef = useRef(null) useEffect(() => { if (error) errorRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }) }, [error]) + useEffect(() => { + getRpc() + .getLatestLedger() + .then((l) => setCurrentLedger(l.sequence)) + .catch(() => {}) + }, []) + const { deploy } = useDeployPool() const { initTarget } = useInitializePool() const { register } = useRegisterPool("target") @@ -96,7 +101,12 @@ export function TargetForm() { if (name === "name") message = validateGroupName(value).message else if (name === "targetAmount") message = validatePositiveAmount(value, "Target amount").message - else if (name === "deadline") message = validateDeadline(value).message + else if (name === "deadlineDays") { + const d = parseInt(value) + if (!value) message = "Deadline is required" + else if (isNaN(d) || d < 1) message = "Deadline must be at least 1 day" + else if (d > 3650) message = "Deadline cannot exceed 10 years" + } setFieldErrors((prev) => ({ ...prev, [name]: message })) }, []) @@ -123,14 +133,15 @@ export function TargetForm() { e.preventDefault() setError("") - setTouched({ name: true, targetAmount: true, deadline: true }) + setTouched({ name: true, targetAmount: true, deadlineDays: true }) const nameResult = validateGroupName(formData.name) const amountResult = validatePositiveAmount(formData.targetAmount, "Target amount") - const deadlineResult = validateDeadline(formData.deadline) + const deadlineDays = parseInt(formData.deadlineDays) + const deadlineDaysValid = formData.deadlineDays && !isNaN(deadlineDays) && deadlineDays >= 1 && deadlineDays <= 3650 setFieldErrors({ name: nameResult.message, targetAmount: amountResult.message, - deadline: deadlineResult.message, + deadlineDays: deadlineDaysValid ? "" : (formData.deadlineDays ? "Deadline must be between 1 and 3650 days" : "Deadline is required"), }) if (!address) return setError("Please connect your wallet first") @@ -140,14 +151,16 @@ export function TargetForm() { ) if (validMembers.length < 2) return setError("Need at least 2 valid Stellar addresses (you + 1 other)") - if (!nameResult.valid || !amountResult.valid || !deadlineResult.valid) return + if (!nameResult.valid || !amountResult.valid || !deadlineDaysValid) return try { setStep("deploying") const contractId = await deploy("target") setStep("initializing") - const deadlineLedger = await dateToLedger(new Date(formData.deadline)) + // Fetch fresh ledger at submit time for the most accurate deadline + const ledger = await getRpc().getLatestLedger() + const deadlineLedger = ledger.sequence + daysToLedgers(deadlineDays) await initTarget(contractId, { token: resolveTokenAddress(token.address), decimals: token.decimals, @@ -165,6 +178,11 @@ export function TargetForm() { console.warn("Factory registration skipped:", (regErr as Error).message) } + // Derive ISO deadline from days so the DB has a human-readable date + const estimatedDeadlineISO = new Date( + Date.now() + deadlineDays * 24 * 60 * 60 * 1000 + ).toISOString() + setStep("saving") const res = await fetch("/api/pools", { method: "POST", @@ -180,7 +198,7 @@ export function TargetForm() { tokenDecimals: token.decimals, members: validMembers, targetAmount: formData.targetAmount, - deadline: formData.deadline, + deadline: estimatedDeadlineISO, }), }) if (!res.ok) throw new Error("Failed to save pool metadata") @@ -205,13 +223,17 @@ export function TargetForm() { ? (parseFloat(formData.targetAmount || "0") / validMembers.length).toFixed(2) : "0" + const days = parseInt(formData.deadlineDays) || 0 + const estimatedDeadlineLedger = + currentLedger !== null && days > 0 ? currentLedger + daysToLedgers(days) : null + const progressFields: ProgressField[] = [ { label: "Group name", valid: validateGroupName(formData.name).valid }, { label: "Target amount", valid: validatePositiveAmount(formData.targetAmount, "Amount").valid, }, - { label: "Deadline", valid: validateDeadline(formData.deadline).valid }, + { label: "Deadline (days)", valid: days >= 1 && days <= 3650 }, { label: "Members (2+)", valid: validMembers.length >= 2 }, ] @@ -316,22 +338,34 @@ export function TargetForm() {
{ - setFormData({ ...formData, deadline: e.target.value }) - if (touched.deadline) validateField("deadline", e.target.value) + setFormData({ ...formData, deadlineDays: e.target.value }) + if (touched.deadlineDays) validateField("deadlineDays", e.target.value) }} - onBlur={(e) => handleBlur("deadline", e.target.value)} + onBlur={(e) => handleBlur("deadlineDays", e.target.value)} /> - {touched.deadline && } + {days > 0 && ( +

+ + {estimatedDeadlineLedger + ? `Ledger ~${estimatedDeadlineLedger.toLocaleString()} · Est. ${new Date(Date.now() + days * 86_400_000).toLocaleDateString()}` + : "Fetching current ledger…"} +

+ )} + {touched.deadlineDays && }
@@ -421,7 +455,9 @@ export function TargetForm() {
  • Members: {validMembers.length}
  • Target Amount: {formData.targetAmount || "0"} XLM
  • Each member contributes: {contributionPerMember} XLM
  • -
  • Deadline: {formData.deadline || "Not set"}
  • +
  • Deadline: {days > 0 + ? `~${days} day${days !== 1 ? "s" : ""}${estimatedDeadlineLedger ? ` (ledger ~${estimatedDeadlineLedger.toLocaleString()})` : ""}` + : "Not set"}