Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17,366 changes: 2,851 additions & 14,515 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.0",
"typescript": "^5.9.3",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/ClaimableBalanceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useSorokit } from "@/context/useSorokit";
import { getClient } from "@/lib/client";
import { getClient, hasClient } from "@/lib/client";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { truncateAddress } from "@/lib/utils";
Expand All @@ -18,6 +18,7 @@ function BalanceRow({ cb }: { cb: ClaimableBalance }) {
setClaiming(true);
setClaimError(null);
try {
if (!hasClient()) { setError("[sorokit-ui] Client not initialized."); return; }
const { error } = await getClient().account.claimBalance(cb.id);
if (!error) {
setClaimed(true);
Expand Down Expand Up @@ -86,6 +87,7 @@ export function ClaimableBalanceCard() {
let active = true;
const timerId = window.setTimeout(() => {
setLoading(true);
if (!hasClient()) { setClaimError("[sorokit-ui] Client not initialized."); return; }
getClient()
.account.getClaimableBalances(address)
.then(({ data, error: err }) => {
Expand Down
118 changes: 117 additions & 1 deletion src/components/FeeEstimator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,123 @@ export function FeeEstimator({
network,
onEstimate
}: FeeEstimatorProps) {
// Component implementation
import { useEffect, useRef, useState } from "react";
import { getClient, hasClient } from "@/lib/client";

/**
* Internal type for fee data returned by the Soroban client.
*/
interface FeeData {
baseFee: string;
recommended: string;
}

export function FeeEstimator({
operations = 1,
network,
onEstimate,
}: FeeEstimatorProps) {
const [fee, setFee] = useState<FeeData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const inFlightRef = useRef(false);

// Load fee estimate from client
const load = async () => {
if (inFlightRef.current) return; // guard against concurrent calls
if (!hasClient()) {
setError("[sorokit-ui] Client not initialized.");
return;
}
setLoading(true);
setError(null);
inFlightRef.current = true;
try {
const result = await getClient().transaction.estimateFee({ network, operations });
if (result.error) {
setError(result.error);
} else if (result.data) {
setFee(result.data);
if (onEstimate) onEstimate(result.data.recommended);
}
} catch (e) {
setError((e as Error).message ?? "Unexpected error");
} finally {
setLoading(false);
inFlightRef.current = false;
}
};

// Initial load and polling every 10 seconds
useEffect(() => {
load(); // initial load
const interval = setInterval(load, 10_000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [network, operations]); // re‑load when props change

return (
<div className="rounded-xl border border-line bg-surface p-4">
{/* Section title */}
<h3 className="text-lg font-medium text-ink mb-2">Network Fee</h3>

{/* Live region for screen readers */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{fee ? `${fee.baseFee} base, ${fee.recommended} recommended` : ""}
</div>

{/* Fee display */}
<div className="flex items-center gap-2">
{/* Show last known fee or placeholder */}
{fee ? (
<div className="flex flex-col">
<span className="text-sm text-ink-2">Base Fee</span>
<span className="text-xl font-mono text-ink">{fee.baseFee}</span>
<span className="text-sm text-ink-2">Recommended</span>
<span className="text-xl font-mono text-ink">{fee.recommended}</span>
</div>
) : (
<div className="text-sm text-ink-3">—</div>
)}

{/* Loading indicator – does not hide fee */}
{loading && (
<div className="w-5 h-5 border-2 border-ink-3 border-t-transparent rounded-full animate-spin" />
)}

{/* Error message */}
{error && <p className="text-sm text-red-600">{error}</p>}

{/* Retry button – visible on error or when not loading */}
<button
type="button"
title="Refresh fee estimate"
aria-label="Refresh fee estimate"
onClick={load}
disabled={loading}
className="ml-auto flex items-center gap-1 rounded px-2 py-1 text-sm text-ink-2 bg-surface-2 hover:bg-surface-3 disabled:opacity-50"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.01M20 20v-5h-.01M4 20l5-5M20 4l-5 5" />
</svg>
Refresh
</button>
</div>
</div>
);
}

interface FeeEstimatorProps {
operations?: number;
network: "testnet" | "public";
onEstimate?: (fee: string) => void;
}
}

interface FeeEstimatorProps {
Expand Down
3 changes: 2 additions & 1 deletion src/components/SorobanInvokeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef, useState } from "react";
import { useSorokit } from "@/context/useSorokit";
import { getClient } from "@/lib/client";
import { getClient, hasClient } from "@/lib/client";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { cn, friendlyError } from "@/lib/utils";
Expand Down Expand Up @@ -49,6 +49,7 @@ export function SorobanInvokeButton({
setError(null);

try {
if (!hasClient()) { setError("[sorokit-ui] Client not initialized."); return; }
const { data, error: err } =
await getClient().soroban.invokeContract(params);
if (err) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/TransactionHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useSorokit } from "@/context/useSorokit";
import { getClient } from "@/lib/client";
import { getClient, hasClient } from "@/lib/client";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { truncateAddress } from "@/lib/utils";
Expand Down Expand Up @@ -81,6 +81,7 @@ export function TransactionHistory() {
let active = true;
const timerId = window.setTimeout(() => {
setLoading(true);
if (!hasClient()) { setError("[sorokit-ui] Client not initialized."); return; }
getClient()
.transaction.getHistory(address, page, PAGE_SIZE)
.then(({ data, error: err, total: t }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, hint, className, id, ...props }, ref) => {
const generatedId = useId();
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-") ?? generatedId;
const inputId = id ?? `${generatedId}-${label?.toLowerCase().replace(/\s+/g, "-")}`;

const [lastError, setLastError] = useState<string | undefined>(error);
const [lastHint, setLastHint] = useState<string | undefined>(hint);
Expand Down
6 changes: 2 additions & 4 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,8 @@ export type NetworkInfo = {
horizonUrl: string;
};

let _client: SorokitClient | null = null;

export function initClient(client: SorokitClient): void {
_client = client;
export function hasClient(): boolean {
return _client !== null;
}

export function getClient(): SorokitClient {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) {
}

/** Truncate a Stellar address or tx hash for display */
export function truncateAddress(address: string, start = 6, end = 4): string {
export function truncateAddress(address: string | null | undefined, start = 6, end = 4): string {
if (!address) return "";
const chars = Array.from(address);
if (chars.length <= start + end) return address;
Expand Down