From 636e7beb8e34c36b7a6af10a3b9f4790af84950b Mon Sep 17 00:00:00 2001 From: Moein Date: Sat, 15 Nov 2025 21:42:59 +0330 Subject: [PATCH 1/4] chore: create hooks --- .gitignore | 1 + src/useStellar/index.ts | 14 +++--- src/useStellar/useAccount.ts | 3 -- src/useStellar/useAccounts.ts | 51 ++++++++++++++++++++++ src/useStellar/useAssets.ts | 55 +++++++++++++++++++++++ src/useStellar/useBalances.ts | 57 ++++++++++++++++++++++++ src/useStellar/useClaimableBalances.ts | 60 ++++++++++++++++++++++++++ src/useStellar/useEffects.ts | 57 ++++++++++++++++++++++++ src/useStellar/useLedgers.ts | 47 ++++++++++++++++++++ src/useStellar/useLiquidityPools.ts | 58 +++++++++++++++++++++++++ 10 files changed, 393 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 8fd3f19..7cca5d1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist dist-ssr *.tgz *.local +demo # Editor directories and files .vscode/* diff --git a/src/useStellar/index.ts b/src/useStellar/index.ts index b3fd011..b116af8 100644 --- a/src/useStellar/index.ts +++ b/src/useStellar/index.ts @@ -1,13 +1,13 @@ export { networks } from '@bluxcc/core'; export * from './useAccount'; export * from './useNetwork'; -// import useAccounts from './useAccounts'; -// import useAssets from './useAssets'; -// import useBalances from './useBalances'; -// import useClaimableBalances from './useClaimableBalances'; -// import useEffects from './useEffects'; -// import useLedgers from './useLedgers'; -// import useLiquidityPools from './useLiquidityPools'; +export * from './useAccounts'; +// export * from './useAssets'; +export * from './useBalances'; +export * from './useClaimableBalances'; +export * from './useEffects'; +export * from './useLedgers'; +export * from './useLiquidityPools'; // import useNetwork from './useNetwork'; // import useOffers from './useOffers'; // import useOperations from './useOperations'; diff --git a/src/useStellar/useAccount.ts b/src/useStellar/useAccount.ts index 142b9ab..6ebd90e 100644 --- a/src/useStellar/useAccount.ts +++ b/src/useStellar/useAccount.ts @@ -12,9 +12,6 @@ export type UseAccountResult = { }; export function useAccount(options: GetAccountOptions): UseAccountResult { - // TODO: we need the same exact function as the core function here. - // Take the options.address, options.network, pass it to it, and wait for changes - // In useEffect. const [result, setResult] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); diff --git a/src/useStellar/useAccounts.ts b/src/useStellar/useAccounts.ts index e69de29..002ae04 100644 --- a/src/useStellar/useAccounts.ts +++ b/src/useStellar/useAccounts.ts @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import { getAccounts } from "@bluxcc/core"; // or your local path +import { + GetAccountsOptions, + GetAccountsResult, +} from "@bluxcc/core/dist/exports/core/getAccounts"; + +export type UseAccountsResult = { + loading: boolean; + error: Error | null; + result: GetAccountsResult | null; +}; + +export function useAccounts(options: GetAccountsOptions): UseAccountsResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!options.network) return; + + const fetchAccounts = async () => { + setLoading(true); + setError(null); + + try { + const accounts = await getAccounts(options); + setResult(accounts); + } catch (e) { + setError(e instanceof Error ? e : new Error(String(e))); + setResult(null); + } finally { + setLoading(false); + } + }; + + fetchAccounts(); + }, [ + options.network, + options.forSigner, + options.forAsset?.code, + options.forAsset?.issuer, + options.forLiquidityPool, + options.sponsor, + options.cursor, + options.limit, + options.order, + ]); + + return { loading, error, result }; +} diff --git a/src/useStellar/useAssets.ts b/src/useStellar/useAssets.ts index e69de29..cd0f7e1 100644 --- a/src/useStellar/useAssets.ts +++ b/src/useStellar/useAssets.ts @@ -0,0 +1,55 @@ +// Upon testing, it either gives "Bad Request" or "Custom network has no transports." + +import { useEffect, useState } from "react"; +import { getAssets } from "@bluxcc/core"; + +import { + GetAssetsOptions, + GetAssetsResult, +} from "@bluxcc/core/dist/exports/core/getAssets"; + +export type UseAssetsResult = { + loading: boolean; + error: Error | null; + result: GetAssetsResult | null; +}; + +export function useAssets(options: GetAssetsOptions): UseAssetsResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + setLoading(true); + setError(null); + + getAssets(options) + .then((r) => { + if (!cancelled) { + setResult(r); + setLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + options.network, + options.cursor, + options.limit, + options.order, + options.forCode, + options.forIssuer, + ]); + + return { loading, error, result }; +} \ No newline at end of file diff --git a/src/useStellar/useBalances.ts b/src/useStellar/useBalances.ts index e69de29..98772a7 100644 --- a/src/useStellar/useBalances.ts +++ b/src/useStellar/useBalances.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import { getBalances } from '@bluxcc/core'; + +import { GetBalancesOptions, GetBalancesResult } from '@bluxcc/core/dist/exports/core/getBalances'; + +export type UseBalancesResult = { + loading: boolean; + error: Error | null; + balances: GetBalancesResult; +}; + +export function useBalances(options: GetBalancesOptions | undefined): UseBalancesResult { + const [balances, setBalances] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!options?.address) { + setBalances([]); + setError(null); + setLoading(false); + return; + } + + let cancelled = false; + + const run = async () => { + try { + setLoading(true); + setError(null); + + const result = await getBalances(options); + + if (!cancelled) { + setBalances(result ?? []); + } + } catch (e) { + if (!cancelled) { + setError(e instanceof Error ? e : new Error(String(e))); + setBalances([]); + } + } finally { + if (!cancelled) setLoading(false); + } + }; + + run(); + + return () => { + cancelled = true; + }; + }, [options?.address, options?.network, options?.includeZeroBalances]); + + return { loading, error, balances }; +} + +export default useBalances; \ No newline at end of file diff --git a/src/useStellar/useClaimableBalances.ts b/src/useStellar/useClaimableBalances.ts index e69de29..ef6892d 100644 --- a/src/useStellar/useClaimableBalances.ts +++ b/src/useStellar/useClaimableBalances.ts @@ -0,0 +1,60 @@ +// Gives "Issuer is invalid" + +import { useEffect, useState } from "react"; +import { getClaimableBalances } from "@bluxcc/core"; + +import { + GetClaimableBalancesOptions, + GetClaimableBalancesResult, +} from "@bluxcc/core/dist/exports/core/getClaimableBalances"; + +export type UseClaimableBalancesResult = { + loading: boolean; + error: Error | null; + result: GetClaimableBalancesResult | null; +}; + +export function useClaimableBalances( + options: GetClaimableBalancesOptions +): UseClaimableBalancesResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + setLoading(true); + setError(null); + setResult(null); + + getClaimableBalances(options) + .then((r) => { + if (!cancelled) { + setResult(r); + setLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + options.network, + options.cursor, + options.limit, + options.order, + + options.asset, + options.claimant, + options.sponsor, + ]); + + return { loading, error, result }; +} diff --git a/src/useStellar/useEffects.ts b/src/useStellar/useEffects.ts index e69de29..b6cb0ed 100644 --- a/src/useStellar/useEffects.ts +++ b/src/useStellar/useEffects.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import { getEffects } from "@bluxcc/core"; + +import { + GetEffectsOptions, + GetEffectsResult, +} from "@bluxcc/core/dist/exports/core/getEffects"; + +export type UseEffectsResult = { + loading: boolean; + error: Error | null; + result: GetEffectsResult | null; +}; + +export function useEffects(options: GetEffectsOptions): UseEffectsResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + setLoading(true); + setError(null); + setResult(null); + + getEffects(options) + .then((res) => { + if (!cancelled) { + setResult(res); + setLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + options.network, + options.cursor, + options.limit, + options.order, + options.forAccount, + options.forLedger, + options.forTransaction, + options.forOperation, + options.forLiquidityPool, + ]); + + return { loading, error, result }; +} diff --git a/src/useStellar/useLedgers.ts b/src/useStellar/useLedgers.ts index e69de29..3ee25e0 100644 --- a/src/useStellar/useLedgers.ts +++ b/src/useStellar/useLedgers.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; +import { getLedgers } from "@bluxcc/core"; + +import { + GetLedgersOptions, + GetLedgersResult, +} from "@bluxcc/core/dist/exports/core/getLedgers"; + +export type UseLedgersResult = { + loading: boolean; + error: Error | null; + result: GetLedgersResult | null; +}; + +export function useLedgers(options: GetLedgersOptions): UseLedgersResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + setLoading(true); + setError(null); + setResult(null); + + getLedgers(options) + .then((r) => { + if (!cancelled) { + setResult(r); + setLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [options.network, options.cursor, options.limit, options.order, options.ledger]); + + return { loading, error, result }; +} diff --git a/src/useStellar/useLiquidityPools.ts b/src/useStellar/useLiquidityPools.ts index e69de29..3fd2c4b 100644 --- a/src/useStellar/useLiquidityPools.ts +++ b/src/useStellar/useLiquidityPools.ts @@ -0,0 +1,58 @@ +// Gives "Issuer is invalid" + +import { useEffect, useState } from "react"; +import { getLiquidityPools } from "@bluxcc/core"; + +import { + GetLiquidityPoolsOptions, + GetLiquidityPoolsResult, +} from "@bluxcc/core/dist/exports/core/getLiquidityPools"; + +export type UseLiquidityPoolsResult = { + loading: boolean; + error: Error | null; + result: GetLiquidityPoolsResult | null; +}; + +export function useLiquidityPools( + options: GetLiquidityPoolsOptions +): UseLiquidityPoolsResult { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + setLoading(true); + setError(null); + setResult(null); + + getLiquidityPools(options) + .then((r) => { + if (!cancelled) { + setResult(r); + setLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + options.network, + options.cursor, + options.limit, + options.order, + options.forAccount, + options.forAssets, + ]); + + return { loading, error, result }; +} From 244977284e1a8f0439cc8d8624d952382478982b Mon Sep 17 00:00:00 2001 From: Moein Date: Mon, 24 Nov 2025 21:26:52 +0330 Subject: [PATCH 2/4] chore: update the parameters of useTransaction --- src/useStellar/useTransactions.ts | 359 ++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) diff --git a/src/useStellar/useTransactions.ts b/src/useStellar/useTransactions.ts index e69de29..132b641 100644 --- a/src/useStellar/useTransactions.ts +++ b/src/useStellar/useTransactions.ts @@ -0,0 +1,359 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { getTransactions as coreGetTransactions } from "@bluxcc/core"; + +import type { + GetTransactionsOptions as CoreGetTransactionsOptions, + GetTransactionsResult as CoreGetTransactionsResult, +} from "@bluxcc/core/dist/exports/core/getTransactions"; + +export type QueryOptions = { + enabled?: boolean; + retry?: boolean | number | ((failureCount: number, error: Error) => boolean); + retryDelay?: number | ((retryAttempt: number, error: Error) => number); + initialData?: CoreGetTransactionsResult | (() => CoreGetTransactionsResult); + initialDataUpdatedAt?: number | (() => number | undefined); + placeholderData?: + | CoreGetTransactionsResult + | (( + previousValue: CoreGetTransactionsResult | undefined, + previousQuery: UseTransactionsBaseResult | undefined + ) => CoreGetTransactionsResult); + notifyOnChangeProps?: string[] | "all" | (() => string[] | "all"); + refetchInterval?: + | number + | false + | ((data: CoreGetTransactionsResult | undefined, query: UseTransactionsBaseResult) => number | false | undefined); + refetchIntervalInBackground?: boolean; + staleTime?: number | typeof Infinity | undefined; + refetchOnMount?: boolean; + refetchOnWindowFocus?: boolean; + refetchOnReconnect?: boolean; + select?: (data: CoreGetTransactionsResult) => TSelect; +}; + +export type UseTransactionsBaseResult = { + data: TSelect | null; + result: CoreGetTransactionsResult | null; + + loading: boolean; + error: Error | null; + updatedAt: number | null; + failureCount: number; + refetch: () => Promise; + cancel: () => void; + + isStale: boolean; +}; + +export function useTransactions( + options: CoreGetTransactionsOptions, + queryOptions?: QueryOptions +): UseTransactionsBaseResult { + const enabled = queryOptions?.enabled !== false; + + const hasInitialized = useRef(false); + const prevStateRef = useRef | null>(null); + const cancelledRef = useRef(false); + + const retryTimerRef = useRef | null>(null); + const failureCountRef = useRef(0); + + const runSelect = useCallback( + (res: CoreGetTransactionsResult): TSelect | null => { + if (!queryOptions?.select) { + return (res as unknown) as TSelect; + } + try { + return queryOptions.select(res); + } catch (e) { + console.error("select() threw an error:", e); + return null; + } + }, + [queryOptions?.select] + ); + + const [result, setResult] = useState(() => { + if (queryOptions?.initialData) { + hasInitialized.current = true; + return typeof queryOptions.initialData === "function" + ? (queryOptions.initialData as () => CoreGetTransactionsResult)() + : queryOptions.initialData; + } + if (queryOptions?.placeholderData) { + return typeof queryOptions.placeholderData === "function" + ? queryOptions.placeholderData(undefined, undefined) + : queryOptions.placeholderData; + } + return null; + }); + + const [data, setData] = useState(() => { + const initial = queryOptions?.initialData + ? typeof queryOptions.initialData === "function" + ? (queryOptions.initialData as () => CoreGetTransactionsResult)() + : queryOptions.initialData + : queryOptions?.placeholderData + ? typeof queryOptions.placeholderData === "function" + ? queryOptions.placeholderData(undefined, undefined) + : queryOptions.placeholderData + : null; + + if (!initial) return null; + return runSelect(initial); + }); + + const [updatedAt, setUpdatedAt] = useState(() => { + if (queryOptions?.initialData && queryOptions?.initialDataUpdatedAt) { + const val = + typeof queryOptions.initialDataUpdatedAt === "function" + ? queryOptions.initialDataUpdatedAt() + : queryOptions.initialDataUpdatedAt; + return typeof val === "number" ? val : null; + } + return null; + }); + + const [failureCount, setFailureCount] = useState(0); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const shouldNotifyChange = useCallback( + (nextState: UseTransactionsBaseResult) => { + const rule = queryOptions?.notifyOnChangeProps; + if (!rule) return true; + + const prev = prevStateRef.current; + if (!prev) return true; + + const keys = typeof rule === "function" ? rule() : rule; + if (keys === "all") return true; + + for (const key of keys) { + if (prev[key as keyof UseTransactionsBaseResult] !== nextState[key as keyof UseTransactionsBaseResult]) { + return true; + } + } + return false; + }, + [queryOptions?.notifyOnChangeProps] + ); + + const clearRetryTimer = () => { + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } + }; + + const computeShouldRetry = (count: number, err: Error): boolean => { + const opt = queryOptions?.retry; + if (opt === undefined) { + return count < 3; + } + if (typeof opt === "boolean") { + return opt === true; + } + if (typeof opt === "number") { + return count < opt; + } + if (typeof opt === "function") { + try { + return Boolean(opt(count, err)); + } catch (e) { + console.warn("retry function threw", e); + return false; + } + } + return false; + }; + + const computeRetryDelayMs = (attempt: number, err?: Error): number => { + const opt = queryOptions?.retryDelay; + + const defaultDelay = Math.min(1000 * 2 ** (attempt - 1), 30000); + + if (opt === undefined) return defaultDelay; + if (typeof opt === "number") { + return Math.max(0, Math.floor(opt)); + } + if (typeof opt === "function") { + try { + const val = opt(attempt, err as Error); + return typeof val === "number" && !Number.isNaN(val) ? Math.max(0, Math.floor(val)) : defaultDelay; + } catch (e) { + console.warn("retryDelay function threw", e); + return defaultDelay; + } + } + return defaultDelay; + }; + + const isStale = useCallback((): boolean => { + const staleTimeVal = queryOptions?.staleTime ?? 0; + if (staleTimeVal === Infinity) return false; + if (updatedAt === null) return true; + return Date.now() - updatedAt >= (staleTimeVal || 0); + }, [queryOptions?.staleTime, updatedAt]); + + const runFetch = useCallback(async () => { + if (!enabled) return; + + clearRetryTimer(); + + setError(null); + setLoading(true); + cancelledRef.current = false; + + try { + const res = await coreGetTransactions(options); + if (cancelledRef.current) return; + + failureCountRef.current = 0; + setFailureCount(0); + clearRetryTimer(); + + const selected = runSelect(res); + + const now = Date.now(); + + const nextState: UseTransactionsBaseResult = { + data: selected, + result: res, + loading: false, + error: null, + updatedAt: now, + failureCount: 0, + refetch: async () => {}, + cancel: () => {}, + isStale: false, + }; + + nextState.refetch = refetch as any; + nextState.cancel = cancel as any; + + hasInitialized.current = true; + + if (shouldNotifyChange(nextState)) { + setResult(res); + setData(selected); + setError(null); + setUpdatedAt(now); + } + + prevStateRef.current = nextState; + } catch (err: any) { + if (cancelledRef.current) return; + const e = err instanceof Error ? err : new Error(String(err)); + + failureCountRef.current = (failureCountRef.current || 0) + 1; + setFailureCount(failureCountRef.current); + + setResult(null); + setData(null); + setError(e); + + const shouldRetry = computeShouldRetry(failureCountRef.current, e); + + if (shouldRetry) { + const delay = computeRetryDelayMs(failureCountRef.current, e); + clearRetryTimer(); + retryTimerRef.current = setTimeout(() => { + if (cancelledRef.current) return; + runFetch().catch(() => {}); + }, delay); + setLoading(false); + } else { + setLoading(false); + } + } + }, [options, enabled, shouldNotifyChange, runSelect, queryOptions?.retry, queryOptions?.retryDelay]); + + const refetch = useCallback(async () => { + clearRetryTimer(); + failureCountRef.current = 0; + setFailureCount(0); + cancelledRef.current = false; + await runFetch(); + }, [runFetch]); + + const cancel = useCallback(() => { + cancelledRef.current = true; + clearRetryTimer(); + setLoading(false); + }, []); + + const currentState: UseTransactionsBaseResult = { + data, + result, + loading, + error, + updatedAt, + failureCount, + refetch, + cancel, + isStale: isStale(), + }; + + prevStateRef.current = currentState; + + useEffect(() => { + if (!enabled) return; + + cancelledRef.current = false; + + const shouldInitialFetch = !hasInitialized.current || (hasInitialized.current && queryOptions?.refetchOnMount && isStale()); + + if (shouldInitialFetch) { + runFetch().catch(() => {}); + } + + return () => { + cancelledRef.current = true; + clearRetryTimer(); + }; + }, [runFetch, enabled, queryOptions?.refetchOnMount, queryOptions?.staleTime, updatedAt]); + + useEffect(() => { + if (!enabled) return; + + const onFocus = () => { + if (queryOptions?.refetchOnWindowFocus && isStale()) { + runFetch().catch(() => {}); + } + }; + + const onOnline = () => { + if (queryOptions?.refetchOnReconnect && isStale()) { + runFetch().catch(() => {}); + } + }; + + if (queryOptions?.refetchOnWindowFocus) { + window.addEventListener("focus", onFocus); + } + if (queryOptions?.refetchOnReconnect) { + window.addEventListener("online", onOnline); + } + + return () => { + if (queryOptions?.refetchOnWindowFocus) { + window.removeEventListener("focus", onFocus); + } + if (queryOptions?.refetchOnReconnect) { + window.removeEventListener("online", onOnline); + } + }; + }, [enabled, queryOptions?.refetchOnWindowFocus, queryOptions?.refetchOnReconnect, queryOptions?.staleTime, updatedAt, isStale, runFetch]); + + useEffect(() => { + return () => { + clearRetryTimer(); + cancelledRef.current = true; + }; + }, []); + + return currentState; +} + +export default useTransactions; \ No newline at end of file From 9e3f1908da9ee15dfc2dff61d447cab9f6eb7f32 Mon Sep 17 00:00:00 2001 From: Moein Date: Sat, 29 Nov 2025 16:44:06 +0330 Subject: [PATCH 3/4] chore: update the return types of useTransaction --- src/useStellar/useTransactions.ts | 250 +++++++++++++++++++++++++++--- 1 file changed, 232 insertions(+), 18 deletions(-) diff --git a/src/useStellar/useTransactions.ts b/src/useStellar/useTransactions.ts index 132b641..fe5a500 100644 --- a/src/useStellar/useTransactions.ts +++ b/src/useStellar/useTransactions.ts @@ -31,18 +31,40 @@ export type QueryOptions = { select?: (data: CoreGetTransactionsResult) => TSelect; }; +export type Status = "error" | "pending" | "success"; +export type FetchStatus = "fetching" | "idle" | "paused"; + export type UseTransactionsBaseResult = { data: TSelect | null; result: CoreGetTransactionsResult | null; loading: boolean; + isFetching: boolean; + fetchStatus: FetchStatus; + status: Status; error: Error | null; + updatedAt: number | null; + dataUpdatedAt: number | null; + errorUpdatedAt: number | null; failureCount: number; - refetch: () => Promise; + + refetch: () => Promise; cancel: () => void; isStale: boolean; + + isError: boolean; + isPending: boolean; + isSuccess: boolean; + isPaused: boolean; + isFetched: boolean; + isLoading: boolean; + isLoadingError: boolean; + isRefetchError: boolean; + isRefetching: boolean; + isFetchedAfterMount: boolean; + isPlaceholderData: boolean; }; export function useTransactions( @@ -58,6 +80,8 @@ export function useTransactions( const retryTimerRef = useRef | null>(null); const failureCountRef = useRef(0); + const mountedRef = useRef(false); + const runSelect = useCallback( (res: CoreGetTransactionsResult): TSelect | null => { if (!queryOptions?.select) { @@ -104,6 +128,10 @@ export function useTransactions( }); const [updatedAt, setUpdatedAt] = useState(() => { + return queryOptions?.initialData ? Date.now() : null; + }); + + const [dataUpdatedAt, setDataUpdatedAt] = useState(() => { if (queryOptions?.initialData && queryOptions?.initialDataUpdatedAt) { const val = typeof queryOptions.initialDataUpdatedAt === "function" @@ -114,10 +142,30 @@ export function useTransactions( return null; }); + const [errorUpdatedAt, setErrorUpdatedAt] = useState(null); + const [failureCount, setFailureCount] = useState(0); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [fetchStatus, setFetchStatus] = useState(() => + enabled ? "idle" : "paused" + ); + + const [isFetched, setIsFetched] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + + const [isLoadingError, setIsLoadingError] = useState(false); + + const [isRefetchError, setIsRefetchError] = useState(false); + + const [isPlaceholderData, setIsPlaceholderData] = useState(() => { + if (queryOptions?.initialData) return false; + if (queryOptions?.placeholderData) return true; + return false; + }); + const shouldNotifyChange = useCallback( (nextState: UseTransactionsBaseResult) => { const rule = queryOptions?.notifyOnChangeProps; @@ -192,17 +240,32 @@ export function useTransactions( const isStale = useCallback((): boolean => { const staleTimeVal = queryOptions?.staleTime ?? 0; if (staleTimeVal === Infinity) return false; - if (updatedAt === null) return true; - return Date.now() - updatedAt >= (staleTimeVal || 0); - }, [queryOptions?.staleTime, updatedAt]); + if (dataUpdatedAt === null) return true; + return Date.now() - dataUpdatedAt >= (staleTimeVal || 0); + }, [queryOptions?.staleTime, dataUpdatedAt]); - const runFetch = useCallback(async () => { - if (!enabled) return; + const [isFetchedAfterMount, setIsFetchedAfterMount] = useState(false); + + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + + const runFetch = useCallback(async (): Promise => { + if (!enabled) { + setFetchStatus("paused"); + return; + } clearRetryTimer(); setError(null); setLoading(true); + setFetchStatus("fetching"); + setIsLoadingError(false); + setIsRefetchError(false); cancelledRef.current = false; try { @@ -221,12 +284,28 @@ export function useTransactions( data: selected, result: res, loading: false, + isFetching: false, + fetchStatus: "idle", + isPaused: false, + status: "success", error: null, updatedAt: now, + dataUpdatedAt: now, + errorUpdatedAt: prevStateRef.current?.errorUpdatedAt ?? null, failureCount: 0, - refetch: async () => {}, + refetch: async () => res, cancel: () => {}, isStale: false, + isError: false, + isPending: false, + isSuccess: true, + isFetched: true, + isLoading: false, + isLoadingError: false, + isRefetchError: false, + isRefetching: false, + isFetchedAfterMount: mountedRef.current ? true : false, + isPlaceholderData: false, }; nextState.refetch = refetch as any; @@ -234,14 +313,36 @@ export function useTransactions( hasInitialized.current = true; + setIsFetched(true); + setIsLoadingError(false); + setIsRefetchError(false); + setIsPlaceholderData(false); + if (mountedRef.current) { + setIsFetchedAfterMount(true); + } + if (shouldNotifyChange(nextState)) { setResult(res); setData(selected); setError(null); setUpdatedAt(now); + setDataUpdatedAt(now); + setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); + setFetchStatus("idle"); + setLoading(false); + } else { + setResult(res); + setData(selected); + setError(null); + setUpdatedAt(now); + setDataUpdatedAt(now); + setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); + setFetchStatus("idle"); + setLoading(false); } prevStateRef.current = nextState; + return res; } catch (err: any) { if (cancelledRef.current) return; const e = err instanceof Error ? err : new Error(String(err)); @@ -249,9 +350,71 @@ export function useTransactions( failureCountRef.current = (failureCountRef.current || 0) + 1; setFailureCount(failureCountRef.current); - setResult(null); - setData(null); - setError(e); + const now = Date.now(); + + const firstFetchFailed = !hasInitialized.current; + const refetchFailed = !firstFetchFailed; + + const nextState: UseTransactionsBaseResult = { + data: null, + result: null, + loading: false, + isFetching: false, + fetchStatus: "idle", + isPaused: false, + status: "error", + error: e, + updatedAt: now, + dataUpdatedAt: dataUpdatedAt, + errorUpdatedAt: now, + failureCount: failureCountRef.current, + refetch: async () => undefined, + cancel: () => {}, + isStale: isStale(), + isError: true, + isPending: false, + isSuccess: false, + isFetched: true, + isLoading: false, + isLoadingError: firstFetchFailed, + isRefetchError: refetchFailed, + isRefetching: false, + isFetchedAfterMount: mountedRef.current ? true : false, + isPlaceholderData: false, + }; + + nextState.refetch = refetch as any; + nextState.cancel = cancel as any; + + setIsFetched(true); + if (firstFetchFailed) { + setIsLoadingError(true); + } + if (refetchFailed) { + setIsRefetchError(true); + } + setIsPlaceholderData(false); + if (mountedRef.current) { + setIsFetchedAfterMount(true); + } + + if (shouldNotifyChange(nextState)) { + setResult(null); + setData(null); + setError(e); + setUpdatedAt(now); + setErrorUpdatedAt(now); + setFetchStatus("idle"); + setLoading(false); + } else { + setError(e); + setUpdatedAt(now); + setErrorUpdatedAt(now); + setFetchStatus("idle"); + setLoading(false); + } + + prevStateRef.current = nextState; const shouldRetry = computeShouldRetry(failureCountRef.current, e); @@ -262,19 +425,20 @@ export function useTransactions( if (cancelledRef.current) return; runFetch().catch(() => {}); }, delay); - setLoading(false); - } else { - setLoading(false); } + + return undefined; } - }, [options, enabled, shouldNotifyChange, runSelect, queryOptions?.retry, queryOptions?.retryDelay]); + }, [options, enabled, shouldNotifyChange, runSelect, queryOptions?.retry, queryOptions?.retryDelay, dataUpdatedAt]); const refetch = useCallback(async () => { clearRetryTimer(); failureCountRef.current = 0; setFailureCount(0); cancelledRef.current = false; - await runFetch(); + setIsLoadingError(false); + setIsRefetchError(false); + return runFetch(); }, [runFetch]); const cancel = useCallback(() => { @@ -283,20 +447,62 @@ export function useTransactions( setLoading(false); }, []); + const status: Status = error ? "error" : !hasInitialized.current ? "pending" : "success"; + + const isError = status === "error"; + const isPending = status === "pending"; + const isSuccess = status === "success"; + + const derivedIsFetching = fetchStatus === "fetching"; + const derivedIsPaused = fetchStatus === "paused"; + + const derivedIsLoading = derivedIsFetching && isPending; + const derivedIsRefetching = derivedIsFetching && !isPending; + + useEffect(() => { + setIsLoading(derivedIsLoading); + }, [derivedIsLoading]); + const currentState: UseTransactionsBaseResult = { data, result, loading, + isFetching: derivedIsFetching, + fetchStatus, + isPaused: derivedIsPaused, + status, error, updatedAt, + dataUpdatedAt, + errorUpdatedAt, failureCount, refetch, cancel, isStale: isStale(), + + isError, + isPending, + isSuccess, + + isFetched, + isLoading, + isLoadingError, + isRefetchError, + isRefetching: derivedIsRefetching, + isFetchedAfterMount, + isPlaceholderData, }; prevStateRef.current = currentState; + useEffect(() => { + if (!enabled) { + setFetchStatus("paused"); + } else { + setFetchStatus((s) => (s === "fetching" ? s : "idle")); + } + }, [enabled]); + useEffect(() => { if (!enabled) return; @@ -312,7 +518,7 @@ export function useTransactions( cancelledRef.current = true; clearRetryTimer(); }; - }, [runFetch, enabled, queryOptions?.refetchOnMount, queryOptions?.staleTime, updatedAt]); + }, [runFetch, enabled, queryOptions?.refetchOnMount, queryOptions?.staleTime, dataUpdatedAt]); useEffect(() => { if (!enabled) return; @@ -344,7 +550,15 @@ export function useTransactions( window.removeEventListener("online", onOnline); } }; - }, [enabled, queryOptions?.refetchOnWindowFocus, queryOptions?.refetchOnReconnect, queryOptions?.staleTime, updatedAt, isStale, runFetch]); + }, [ + enabled, + queryOptions?.refetchOnWindowFocus, + queryOptions?.refetchOnReconnect, + queryOptions?.staleTime, + dataUpdatedAt, + isStale, + runFetch, + ]); useEffect(() => { return () => { @@ -356,4 +570,4 @@ export function useTransactions( return currentState; } -export default useTransactions; \ No newline at end of file +export default useTransactions; From 63ecb70068d794067e6526b2bb7e393bb94f7271 Mon Sep 17 00:00:00 2001 From: Moein Date: Tue, 2 Dec 2025 20:14:23 +0330 Subject: [PATCH 4/4] feat: implement getAddress and getNetwork --- package.json | 1 + src/useStellar/index.ts | 2 +- src/useStellar/useBalances.ts | 106 ++- src/useStellar/useTransactions.ts | 1146 ++++++++++++++--------------- src/utils.ts | 34 + 5 files changed, 660 insertions(+), 629 deletions(-) create mode 100644 src/utils.ts diff --git a/package.json b/package.json index b63fb71..037336d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "peerDependencies": { "@bluxcc/core": "^0.1.16", + "@tanstack/react-query": "^5.90.11", "react": ">=17.0.0", "react-dom": ">=17.0.0" }, diff --git a/src/useStellar/index.ts b/src/useStellar/index.ts index b116af8..ec54adf 100644 --- a/src/useStellar/index.ts +++ b/src/useStellar/index.ts @@ -17,5 +17,5 @@ export * from './useLiquidityPools'; // import useStrictSendPaths from './useStrictSendPaths'; // import useTradeAggregation from './useTradeAggregation'; // import useTrades from './useTrades'; -// import useTransactions from './useTransactions'; +// export * from './useTransactions'; export { useSwitchNetwork } from './useSwitchNetwork'; diff --git a/src/useStellar/useBalances.ts b/src/useStellar/useBalances.ts index 98772a7..e4ce8e6 100644 --- a/src/useStellar/useBalances.ts +++ b/src/useStellar/useBalances.ts @@ -1,57 +1,53 @@ -import { useEffect, useState } from "react"; -import { getBalances } from '@bluxcc/core'; - -import { GetBalancesOptions, GetBalancesResult } from '@bluxcc/core/dist/exports/core/getBalances'; - -export type UseBalancesResult = { - loading: boolean; - error: Error | null; - balances: GetBalancesResult; -}; - -export function useBalances(options: GetBalancesOptions | undefined): UseBalancesResult { - const [balances, setBalances] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (!options?.address) { - setBalances([]); - setError(null); - setLoading(false); - return; - } - - let cancelled = false; - - const run = async () => { - try { - setLoading(true); - setError(null); - - const result = await getBalances(options); - - if (!cancelled) { - setBalances(result ?? []); - } - } catch (e) { - if (!cancelled) { - setError(e instanceof Error ? e : new Error(String(e))); - setBalances([]); - } - } finally { - if (!cancelled) setLoading(false); - } - }; - - run(); - - return () => { - cancelled = true; - }; - }, [options?.address, options?.network, options?.includeZeroBalances]); - - return { loading, error, balances }; +import { useMemo } from "react"; +import { useQuery, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; +import { getBalances } from "@bluxcc/core"; +import type { + GetBalancesOptions, + GetBalancesResult, +} from "@bluxcc/core/dist/exports/core/getBalances"; +import { getAddress, getNetwork } from "../utils"; + +export function useBalances( + options?: GetBalancesOptions, + queryOptions?: UseQueryOptions +): UseQueryResult { + + const address = getAddress(options?.address); + const network = getNetwork(options?.network); + + const enabled = !!address && (queryOptions?.enabled ?? true); + + const queryKey = useMemo( + () => [ + "blux", + "balances", + address ?? null, + network ?? null, + Boolean(options?.includeZeroBalances), + ], + [address, network, options?.includeZeroBalances] + ); + + const queryFn = useMemo( + () => async () => { + const opts: GetBalancesOptions = { + address: options?.address, + network: options?.network, + includeZeroBalances: options?.includeZeroBalances, + }; + return getBalances(opts); + }, + [options?.address, options?.network, options?.includeZeroBalances] + ); + + const result = useQuery({ + queryKey, + queryFn, + enabled, + ...queryOptions, + }); + + return result; } -export default useBalances; \ No newline at end of file +export default useBalances; diff --git a/src/useStellar/useTransactions.ts b/src/useStellar/useTransactions.ts index fe5a500..3a41cc8 100644 --- a/src/useStellar/useTransactions.ts +++ b/src/useStellar/useTransactions.ts @@ -1,573 +1,573 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { getTransactions as coreGetTransactions } from "@bluxcc/core"; - -import type { - GetTransactionsOptions as CoreGetTransactionsOptions, - GetTransactionsResult as CoreGetTransactionsResult, -} from "@bluxcc/core/dist/exports/core/getTransactions"; - -export type QueryOptions = { - enabled?: boolean; - retry?: boolean | number | ((failureCount: number, error: Error) => boolean); - retryDelay?: number | ((retryAttempt: number, error: Error) => number); - initialData?: CoreGetTransactionsResult | (() => CoreGetTransactionsResult); - initialDataUpdatedAt?: number | (() => number | undefined); - placeholderData?: - | CoreGetTransactionsResult - | (( - previousValue: CoreGetTransactionsResult | undefined, - previousQuery: UseTransactionsBaseResult | undefined - ) => CoreGetTransactionsResult); - notifyOnChangeProps?: string[] | "all" | (() => string[] | "all"); - refetchInterval?: - | number - | false - | ((data: CoreGetTransactionsResult | undefined, query: UseTransactionsBaseResult) => number | false | undefined); - refetchIntervalInBackground?: boolean; - staleTime?: number | typeof Infinity | undefined; - refetchOnMount?: boolean; - refetchOnWindowFocus?: boolean; - refetchOnReconnect?: boolean; - select?: (data: CoreGetTransactionsResult) => TSelect; -}; - -export type Status = "error" | "pending" | "success"; -export type FetchStatus = "fetching" | "idle" | "paused"; - -export type UseTransactionsBaseResult = { - data: TSelect | null; - result: CoreGetTransactionsResult | null; - - loading: boolean; - isFetching: boolean; - fetchStatus: FetchStatus; - status: Status; - error: Error | null; - - updatedAt: number | null; - dataUpdatedAt: number | null; - errorUpdatedAt: number | null; - failureCount: number; - - refetch: () => Promise; - cancel: () => void; - - isStale: boolean; - - isError: boolean; - isPending: boolean; - isSuccess: boolean; - isPaused: boolean; - isFetched: boolean; - isLoading: boolean; - isLoadingError: boolean; - isRefetchError: boolean; - isRefetching: boolean; - isFetchedAfterMount: boolean; - isPlaceholderData: boolean; -}; - -export function useTransactions( - options: CoreGetTransactionsOptions, - queryOptions?: QueryOptions -): UseTransactionsBaseResult { - const enabled = queryOptions?.enabled !== false; - - const hasInitialized = useRef(false); - const prevStateRef = useRef | null>(null); - const cancelledRef = useRef(false); - - const retryTimerRef = useRef | null>(null); - const failureCountRef = useRef(0); - - const mountedRef = useRef(false); - - const runSelect = useCallback( - (res: CoreGetTransactionsResult): TSelect | null => { - if (!queryOptions?.select) { - return (res as unknown) as TSelect; - } - try { - return queryOptions.select(res); - } catch (e) { - console.error("select() threw an error:", e); - return null; - } - }, - [queryOptions?.select] - ); - - const [result, setResult] = useState(() => { - if (queryOptions?.initialData) { - hasInitialized.current = true; - return typeof queryOptions.initialData === "function" - ? (queryOptions.initialData as () => CoreGetTransactionsResult)() - : queryOptions.initialData; - } - if (queryOptions?.placeholderData) { - return typeof queryOptions.placeholderData === "function" - ? queryOptions.placeholderData(undefined, undefined) - : queryOptions.placeholderData; - } - return null; - }); - - const [data, setData] = useState(() => { - const initial = queryOptions?.initialData - ? typeof queryOptions.initialData === "function" - ? (queryOptions.initialData as () => CoreGetTransactionsResult)() - : queryOptions.initialData - : queryOptions?.placeholderData - ? typeof queryOptions.placeholderData === "function" - ? queryOptions.placeholderData(undefined, undefined) - : queryOptions.placeholderData - : null; - - if (!initial) return null; - return runSelect(initial); - }); - - const [updatedAt, setUpdatedAt] = useState(() => { - return queryOptions?.initialData ? Date.now() : null; - }); - - const [dataUpdatedAt, setDataUpdatedAt] = useState(() => { - if (queryOptions?.initialData && queryOptions?.initialDataUpdatedAt) { - const val = - typeof queryOptions.initialDataUpdatedAt === "function" - ? queryOptions.initialDataUpdatedAt() - : queryOptions.initialDataUpdatedAt; - return typeof val === "number" ? val : null; - } - return null; - }); - - const [errorUpdatedAt, setErrorUpdatedAt] = useState(null); - - const [failureCount, setFailureCount] = useState(0); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const [fetchStatus, setFetchStatus] = useState(() => - enabled ? "idle" : "paused" - ); - - const [isFetched, setIsFetched] = useState(false); - - const [isLoading, setIsLoading] = useState(false); - - const [isLoadingError, setIsLoadingError] = useState(false); - - const [isRefetchError, setIsRefetchError] = useState(false); - - const [isPlaceholderData, setIsPlaceholderData] = useState(() => { - if (queryOptions?.initialData) return false; - if (queryOptions?.placeholderData) return true; - return false; - }); - - const shouldNotifyChange = useCallback( - (nextState: UseTransactionsBaseResult) => { - const rule = queryOptions?.notifyOnChangeProps; - if (!rule) return true; - - const prev = prevStateRef.current; - if (!prev) return true; - - const keys = typeof rule === "function" ? rule() : rule; - if (keys === "all") return true; - - for (const key of keys) { - if (prev[key as keyof UseTransactionsBaseResult] !== nextState[key as keyof UseTransactionsBaseResult]) { - return true; - } - } - return false; - }, - [queryOptions?.notifyOnChangeProps] - ); - - const clearRetryTimer = () => { - if (retryTimerRef.current) { - clearTimeout(retryTimerRef.current); - retryTimerRef.current = null; - } - }; - - const computeShouldRetry = (count: number, err: Error): boolean => { - const opt = queryOptions?.retry; - if (opt === undefined) { - return count < 3; - } - if (typeof opt === "boolean") { - return opt === true; - } - if (typeof opt === "number") { - return count < opt; - } - if (typeof opt === "function") { - try { - return Boolean(opt(count, err)); - } catch (e) { - console.warn("retry function threw", e); - return false; - } - } - return false; - }; - - const computeRetryDelayMs = (attempt: number, err?: Error): number => { - const opt = queryOptions?.retryDelay; - - const defaultDelay = Math.min(1000 * 2 ** (attempt - 1), 30000); - - if (opt === undefined) return defaultDelay; - if (typeof opt === "number") { - return Math.max(0, Math.floor(opt)); - } - if (typeof opt === "function") { - try { - const val = opt(attempt, err as Error); - return typeof val === "number" && !Number.isNaN(val) ? Math.max(0, Math.floor(val)) : defaultDelay; - } catch (e) { - console.warn("retryDelay function threw", e); - return defaultDelay; - } - } - return defaultDelay; - }; - - const isStale = useCallback((): boolean => { - const staleTimeVal = queryOptions?.staleTime ?? 0; - if (staleTimeVal === Infinity) return false; - if (dataUpdatedAt === null) return true; - return Date.now() - dataUpdatedAt >= (staleTimeVal || 0); - }, [queryOptions?.staleTime, dataUpdatedAt]); - - const [isFetchedAfterMount, setIsFetchedAfterMount] = useState(false); - - useEffect(() => { - mountedRef.current = true; - return () => { - mountedRef.current = false; - }; - }, []); - - const runFetch = useCallback(async (): Promise => { - if (!enabled) { - setFetchStatus("paused"); - return; - } - - clearRetryTimer(); - - setError(null); - setLoading(true); - setFetchStatus("fetching"); - setIsLoadingError(false); - setIsRefetchError(false); - cancelledRef.current = false; - - try { - const res = await coreGetTransactions(options); - if (cancelledRef.current) return; - - failureCountRef.current = 0; - setFailureCount(0); - clearRetryTimer(); - - const selected = runSelect(res); - - const now = Date.now(); - - const nextState: UseTransactionsBaseResult = { - data: selected, - result: res, - loading: false, - isFetching: false, - fetchStatus: "idle", - isPaused: false, - status: "success", - error: null, - updatedAt: now, - dataUpdatedAt: now, - errorUpdatedAt: prevStateRef.current?.errorUpdatedAt ?? null, - failureCount: 0, - refetch: async () => res, - cancel: () => {}, - isStale: false, - isError: false, - isPending: false, - isSuccess: true, - isFetched: true, - isLoading: false, - isLoadingError: false, - isRefetchError: false, - isRefetching: false, - isFetchedAfterMount: mountedRef.current ? true : false, - isPlaceholderData: false, - }; - - nextState.refetch = refetch as any; - nextState.cancel = cancel as any; - - hasInitialized.current = true; - - setIsFetched(true); - setIsLoadingError(false); - setIsRefetchError(false); - setIsPlaceholderData(false); - if (mountedRef.current) { - setIsFetchedAfterMount(true); - } - - if (shouldNotifyChange(nextState)) { - setResult(res); - setData(selected); - setError(null); - setUpdatedAt(now); - setDataUpdatedAt(now); - setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); - setFetchStatus("idle"); - setLoading(false); - } else { - setResult(res); - setData(selected); - setError(null); - setUpdatedAt(now); - setDataUpdatedAt(now); - setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); - setFetchStatus("idle"); - setLoading(false); - } - - prevStateRef.current = nextState; - return res; - } catch (err: any) { - if (cancelledRef.current) return; - const e = err instanceof Error ? err : new Error(String(err)); - - failureCountRef.current = (failureCountRef.current || 0) + 1; - setFailureCount(failureCountRef.current); - - const now = Date.now(); - - const firstFetchFailed = !hasInitialized.current; - const refetchFailed = !firstFetchFailed; - - const nextState: UseTransactionsBaseResult = { - data: null, - result: null, - loading: false, - isFetching: false, - fetchStatus: "idle", - isPaused: false, - status: "error", - error: e, - updatedAt: now, - dataUpdatedAt: dataUpdatedAt, - errorUpdatedAt: now, - failureCount: failureCountRef.current, - refetch: async () => undefined, - cancel: () => {}, - isStale: isStale(), - isError: true, - isPending: false, - isSuccess: false, - isFetched: true, - isLoading: false, - isLoadingError: firstFetchFailed, - isRefetchError: refetchFailed, - isRefetching: false, - isFetchedAfterMount: mountedRef.current ? true : false, - isPlaceholderData: false, - }; - - nextState.refetch = refetch as any; - nextState.cancel = cancel as any; - - setIsFetched(true); - if (firstFetchFailed) { - setIsLoadingError(true); - } - if (refetchFailed) { - setIsRefetchError(true); - } - setIsPlaceholderData(false); - if (mountedRef.current) { - setIsFetchedAfterMount(true); - } - - if (shouldNotifyChange(nextState)) { - setResult(null); - setData(null); - setError(e); - setUpdatedAt(now); - setErrorUpdatedAt(now); - setFetchStatus("idle"); - setLoading(false); - } else { - setError(e); - setUpdatedAt(now); - setErrorUpdatedAt(now); - setFetchStatus("idle"); - setLoading(false); - } - - prevStateRef.current = nextState; - - const shouldRetry = computeShouldRetry(failureCountRef.current, e); - - if (shouldRetry) { - const delay = computeRetryDelayMs(failureCountRef.current, e); - clearRetryTimer(); - retryTimerRef.current = setTimeout(() => { - if (cancelledRef.current) return; - runFetch().catch(() => {}); - }, delay); - } - - return undefined; - } - }, [options, enabled, shouldNotifyChange, runSelect, queryOptions?.retry, queryOptions?.retryDelay, dataUpdatedAt]); - - const refetch = useCallback(async () => { - clearRetryTimer(); - failureCountRef.current = 0; - setFailureCount(0); - cancelledRef.current = false; - setIsLoadingError(false); - setIsRefetchError(false); - return runFetch(); - }, [runFetch]); - - const cancel = useCallback(() => { - cancelledRef.current = true; - clearRetryTimer(); - setLoading(false); - }, []); - - const status: Status = error ? "error" : !hasInitialized.current ? "pending" : "success"; - - const isError = status === "error"; - const isPending = status === "pending"; - const isSuccess = status === "success"; - - const derivedIsFetching = fetchStatus === "fetching"; - const derivedIsPaused = fetchStatus === "paused"; - - const derivedIsLoading = derivedIsFetching && isPending; - const derivedIsRefetching = derivedIsFetching && !isPending; - - useEffect(() => { - setIsLoading(derivedIsLoading); - }, [derivedIsLoading]); - - const currentState: UseTransactionsBaseResult = { - data, - result, - loading, - isFetching: derivedIsFetching, - fetchStatus, - isPaused: derivedIsPaused, - status, - error, - updatedAt, - dataUpdatedAt, - errorUpdatedAt, - failureCount, - refetch, - cancel, - isStale: isStale(), - - isError, - isPending, - isSuccess, - - isFetched, - isLoading, - isLoadingError, - isRefetchError, - isRefetching: derivedIsRefetching, - isFetchedAfterMount, - isPlaceholderData, - }; - - prevStateRef.current = currentState; - - useEffect(() => { - if (!enabled) { - setFetchStatus("paused"); - } else { - setFetchStatus((s) => (s === "fetching" ? s : "idle")); - } - }, [enabled]); - - useEffect(() => { - if (!enabled) return; - - cancelledRef.current = false; - - const shouldInitialFetch = !hasInitialized.current || (hasInitialized.current && queryOptions?.refetchOnMount && isStale()); - - if (shouldInitialFetch) { - runFetch().catch(() => {}); - } - - return () => { - cancelledRef.current = true; - clearRetryTimer(); - }; - }, [runFetch, enabled, queryOptions?.refetchOnMount, queryOptions?.staleTime, dataUpdatedAt]); - - useEffect(() => { - if (!enabled) return; - - const onFocus = () => { - if (queryOptions?.refetchOnWindowFocus && isStale()) { - runFetch().catch(() => {}); - } - }; - - const onOnline = () => { - if (queryOptions?.refetchOnReconnect && isStale()) { - runFetch().catch(() => {}); - } - }; - - if (queryOptions?.refetchOnWindowFocus) { - window.addEventListener("focus", onFocus); - } - if (queryOptions?.refetchOnReconnect) { - window.addEventListener("online", onOnline); - } - - return () => { - if (queryOptions?.refetchOnWindowFocus) { - window.removeEventListener("focus", onFocus); - } - if (queryOptions?.refetchOnReconnect) { - window.removeEventListener("online", onOnline); - } - }; - }, [ - enabled, - queryOptions?.refetchOnWindowFocus, - queryOptions?.refetchOnReconnect, - queryOptions?.staleTime, - dataUpdatedAt, - isStale, - runFetch, - ]); - - useEffect(() => { - return () => { - clearRetryTimer(); - cancelledRef.current = true; - }; - }, []); - - return currentState; -} - -export default useTransactions; +// import { useCallback, useEffect, useRef, useState } from "react"; +// import { getTransactions as coreGetTransactions } from "@bluxcc/core"; + +// import type { +// GetTransactionsOptions as CoreGetTransactionsOptions, +// GetTransactionsResult as CoreGetTransactionsResult, +// } from "@bluxcc/core/dist/exports/core/getTransactions"; + +// export type QueryOptions = { +// enabled?: boolean; +// retry?: boolean | number | ((failureCount: number, error: Error) => boolean); +// retryDelay?: number | ((retryAttempt: number, error: Error) => number); +// initialData?: CoreGetTransactionsResult | (() => CoreGetTransactionsResult); +// initialDataUpdatedAt?: number | (() => number | undefined); +// placeholderData?: +// | CoreGetTransactionsResult +// | (( +// previousValue: CoreGetTransactionsResult | undefined, +// previousQuery: UseTransactionsBaseResult | undefined +// ) => CoreGetTransactionsResult); +// notifyOnChangeProps?: string[] | "all" | (() => string[] | "all"); +// refetchInterval?: +// | number +// | false +// | ((data: CoreGetTransactionsResult | undefined, query: UseTransactionsBaseResult) => number | false | undefined); +// refetchIntervalInBackground?: boolean; +// staleTime?: number | typeof Infinity | undefined; +// refetchOnMount?: boolean; +// refetchOnWindowFocus?: boolean; +// refetchOnReconnect?: boolean; +// select?: (data: CoreGetTransactionsResult) => TSelect; +// }; + +// export type Status = "error" | "pending" | "success"; +// export type FetchStatus = "fetching" | "idle" | "paused"; + +// export type UseTransactionsBaseResult = { +// data: TSelect | null; +// result: CoreGetTransactionsResult | null; + +// loading: boolean; +// isFetching: boolean; +// fetchStatus: FetchStatus; +// status: Status; +// error: Error | null; + +// updatedAt: number | null; +// dataUpdatedAt: number | null; +// errorUpdatedAt: number | null; +// failureCount: number; + +// refetch: () => Promise; +// cancel: () => void; + +// isStale: boolean; + +// isError: boolean; +// isPending: boolean; +// isSuccess: boolean; +// isPaused: boolean; +// isFetched: boolean; +// isLoading: boolean; +// isLoadingError: boolean; +// isRefetchError: boolean; +// isRefetching: boolean; +// isFetchedAfterMount: boolean; +// isPlaceholderData: boolean; +// }; + +// export function useTransactions( +// options: CoreGetTransactionsOptions, +// queryOptions?: QueryOptions +// ): UseTransactionsBaseResult { +// const enabled = queryOptions?.enabled !== false; + +// const hasInitialized = useRef(false); +// const prevStateRef = useRef | null>(null); +// const cancelledRef = useRef(false); + +// const retryTimerRef = useRef | null>(null); +// const failureCountRef = useRef(0); + +// const mountedRef = useRef(false); + +// const runSelect = useCallback( +// (res: CoreGetTransactionsResult): TSelect | null => { +// if (!queryOptions?.select) { +// return (res as unknown) as TSelect; +// } +// try { +// return queryOptions.select(res); +// } catch (e) { +// console.error("select() threw an error:", e); +// return null; +// } +// }, +// [queryOptions?.select] +// ); + +// const [result, setResult] = useState(() => { +// if (queryOptions?.initialData) { +// hasInitialized.current = true; +// return typeof queryOptions.initialData === "function" +// ? (queryOptions.initialData as () => CoreGetTransactionsResult)() +// : queryOptions.initialData; +// } +// if (queryOptions?.placeholderData) { +// return typeof queryOptions.placeholderData === "function" +// ? queryOptions.placeholderData(undefined, undefined) +// : queryOptions.placeholderData; +// } +// return null; +// }); + +// const [data, setData] = useState(() => { +// const initial = queryOptions?.initialData +// ? typeof queryOptions.initialData === "function" +// ? (queryOptions.initialData as () => CoreGetTransactionsResult)() +// : queryOptions.initialData +// : queryOptions?.placeholderData +// ? typeof queryOptions.placeholderData === "function" +// ? queryOptions.placeholderData(undefined, undefined) +// : queryOptions.placeholderData +// : null; + +// if (!initial) return null; +// return runSelect(initial); +// }); + +// const [updatedAt, setUpdatedAt] = useState(() => { +// return queryOptions?.initialData ? Date.now() : null; +// }); + +// const [dataUpdatedAt, setDataUpdatedAt] = useState(() => { +// if (queryOptions?.initialData && queryOptions?.initialDataUpdatedAt) { +// const val = +// typeof queryOptions.initialDataUpdatedAt === "function" +// ? queryOptions.initialDataUpdatedAt() +// : queryOptions.initialDataUpdatedAt; +// return typeof val === "number" ? val : null; +// } +// return null; +// }); + +// const [errorUpdatedAt, setErrorUpdatedAt] = useState(null); + +// const [failureCount, setFailureCount] = useState(0); +// const [error, setError] = useState(null); +// const [loading, setLoading] = useState(false); + +// const [fetchStatus, setFetchStatus] = useState(() => +// enabled ? "idle" : "paused" +// ); + +// const [isFetched, setIsFetched] = useState(false); + +// const [isLoading, setIsLoading] = useState(false); + +// const [isLoadingError, setIsLoadingError] = useState(false); + +// const [isRefetchError, setIsRefetchError] = useState(false); + +// const [isPlaceholderData, setIsPlaceholderData] = useState(() => { +// if (queryOptions?.initialData) return false; +// if (queryOptions?.placeholderData) return true; +// return false; +// }); + +// const shouldNotifyChange = useCallback( +// (nextState: UseTransactionsBaseResult) => { +// const rule = queryOptions?.notifyOnChangeProps; +// if (!rule) return true; + +// const prev = prevStateRef.current; +// if (!prev) return true; + +// const keys = typeof rule === "function" ? rule() : rule; +// if (keys === "all") return true; + +// for (const key of keys) { +// if (prev[key as keyof UseTransactionsBaseResult] !== nextState[key as keyof UseTransactionsBaseResult]) { +// return true; +// } +// } +// return false; +// }, +// [queryOptions?.notifyOnChangeProps] +// ); + +// const clearRetryTimer = () => { +// if (retryTimerRef.current) { +// clearTimeout(retryTimerRef.current); +// retryTimerRef.current = null; +// } +// }; + +// const computeShouldRetry = (count: number, err: Error): boolean => { +// const opt = queryOptions?.retry; +// if (opt === undefined) { +// return count < 3; +// } +// if (typeof opt === "boolean") { +// return opt === true; +// } +// if (typeof opt === "number") { +// return count < opt; +// } +// if (typeof opt === "function") { +// try { +// return Boolean(opt(count, err)); +// } catch (e) { +// console.warn("retry function threw", e); +// return false; +// } +// } +// return false; +// }; + +// const computeRetryDelayMs = (attempt: number, err?: Error): number => { +// const opt = queryOptions?.retryDelay; + +// const defaultDelay = Math.min(1000 * 2 ** (attempt - 1), 30000); + +// if (opt === undefined) return defaultDelay; +// if (typeof opt === "number") { +// return Math.max(0, Math.floor(opt)); +// } +// if (typeof opt === "function") { +// try { +// const val = opt(attempt, err as Error); +// return typeof val === "number" && !Number.isNaN(val) ? Math.max(0, Math.floor(val)) : defaultDelay; +// } catch (e) { +// console.warn("retryDelay function threw", e); +// return defaultDelay; +// } +// } +// return defaultDelay; +// }; + +// const isStale = useCallback((): boolean => { +// const staleTimeVal = queryOptions?.staleTime ?? 0; +// if (staleTimeVal === Infinity) return false; +// if (dataUpdatedAt === null) return true; +// return Date.now() - dataUpdatedAt >= (staleTimeVal || 0); +// }, [queryOptions?.staleTime, dataUpdatedAt]); + +// const [isFetchedAfterMount, setIsFetchedAfterMount] = useState(false); + +// useEffect(() => { +// mountedRef.current = true; +// return () => { +// mountedRef.current = false; +// }; +// }, []); + +// const runFetch = useCallback(async (): Promise => { +// if (!enabled) { +// setFetchStatus("paused"); +// return; +// } + +// clearRetryTimer(); + +// setError(null); +// setLoading(true); +// setFetchStatus("fetching"); +// setIsLoadingError(false); +// setIsRefetchError(false); +// cancelledRef.current = false; + +// try { +// const res = await coreGetTransactions(options); +// if (cancelledRef.current) return; + +// failureCountRef.current = 0; +// setFailureCount(0); +// clearRetryTimer(); + +// const selected = runSelect(res); + +// const now = Date.now(); + +// const nextState: UseTransactionsBaseResult = { +// data: selected, +// result: res, +// loading: false, +// isFetching: false, +// fetchStatus: "idle", +// isPaused: false, +// status: "success", +// error: null, +// updatedAt: now, +// dataUpdatedAt: now, +// errorUpdatedAt: prevStateRef.current?.errorUpdatedAt ?? null, +// failureCount: 0, +// refetch: async () => res, +// cancel: () => {}, +// isStale: false, +// isError: false, +// isPending: false, +// isSuccess: true, +// isFetched: true, +// isLoading: false, +// isLoadingError: false, +// isRefetchError: false, +// isRefetching: false, +// isFetchedAfterMount: mountedRef.current ? true : false, +// isPlaceholderData: false, +// }; + +// nextState.refetch = refetch as any; +// nextState.cancel = cancel as any; + +// hasInitialized.current = true; + +// setIsFetched(true); +// setIsLoadingError(false); +// setIsRefetchError(false); +// setIsPlaceholderData(false); +// if (mountedRef.current) { +// setIsFetchedAfterMount(true); +// } + +// if (shouldNotifyChange(nextState)) { +// setResult(res); +// setData(selected); +// setError(null); +// setUpdatedAt(now); +// setDataUpdatedAt(now); +// setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); +// setFetchStatus("idle"); +// setLoading(false); +// } else { +// setResult(res); +// setData(selected); +// setError(null); +// setUpdatedAt(now); +// setDataUpdatedAt(now); +// setErrorUpdatedAt(nextState.errorUpdatedAt ?? null); +// setFetchStatus("idle"); +// setLoading(false); +// } + +// prevStateRef.current = nextState; +// return res; +// } catch (err: any) { +// if (cancelledRef.current) return; +// const e = err instanceof Error ? err : new Error(String(err)); + +// failureCountRef.current = (failureCountRef.current || 0) + 1; +// setFailureCount(failureCountRef.current); + +// const now = Date.now(); + +// const firstFetchFailed = !hasInitialized.current; +// const refetchFailed = !firstFetchFailed; + +// const nextState: UseTransactionsBaseResult = { +// data: null, +// result: null, +// loading: false, +// isFetching: false, +// fetchStatus: "idle", +// isPaused: false, +// status: "error", +// error: e, +// updatedAt: now, +// dataUpdatedAt: dataUpdatedAt, +// errorUpdatedAt: now, +// failureCount: failureCountRef.current, +// refetch: async () => undefined, +// cancel: () => {}, +// isStale: isStale(), +// isError: true, +// isPending: false, +// isSuccess: false, +// isFetched: true, +// isLoading: false, +// isLoadingError: firstFetchFailed, +// isRefetchError: refetchFailed, +// isRefetching: false, +// isFetchedAfterMount: mountedRef.current ? true : false, +// isPlaceholderData: false, +// }; + +// nextState.refetch = refetch as any; +// nextState.cancel = cancel as any; + +// setIsFetched(true); +// if (firstFetchFailed) { +// setIsLoadingError(true); +// } +// if (refetchFailed) { +// setIsRefetchError(true); +// } +// setIsPlaceholderData(false); +// if (mountedRef.current) { +// setIsFetchedAfterMount(true); +// } + +// if (shouldNotifyChange(nextState)) { +// setResult(null); +// setData(null); +// setError(e); +// setUpdatedAt(now); +// setErrorUpdatedAt(now); +// setFetchStatus("idle"); +// setLoading(false); +// } else { +// setError(e); +// setUpdatedAt(now); +// setErrorUpdatedAt(now); +// setFetchStatus("idle"); +// setLoading(false); +// } + +// prevStateRef.current = nextState; + +// const shouldRetry = computeShouldRetry(failureCountRef.current, e); + +// if (shouldRetry) { +// const delay = computeRetryDelayMs(failureCountRef.current, e); +// clearRetryTimer(); +// retryTimerRef.current = setTimeout(() => { +// if (cancelledRef.current) return; +// runFetch().catch(() => {}); +// }, delay); +// } + +// return undefined; +// } +// }, [options, enabled, shouldNotifyChange, runSelect, queryOptions?.retry, queryOptions?.retryDelay, dataUpdatedAt]); + +// const refetch = useCallback(async () => { +// clearRetryTimer(); +// failureCountRef.current = 0; +// setFailureCount(0); +// cancelledRef.current = false; +// setIsLoadingError(false); +// setIsRefetchError(false); +// return runFetch(); +// }, [runFetch]); + +// const cancel = useCallback(() => { +// cancelledRef.current = true; +// clearRetryTimer(); +// setLoading(false); +// }, []); + +// const status: Status = error ? "error" : !hasInitialized.current ? "pending" : "success"; + +// const isError = status === "error"; +// const isPending = status === "pending"; +// const isSuccess = status === "success"; + +// const derivedIsFetching = fetchStatus === "fetching"; +// const derivedIsPaused = fetchStatus === "paused"; + +// const derivedIsLoading = derivedIsFetching && isPending; +// const derivedIsRefetching = derivedIsFetching && !isPending; + +// useEffect(() => { +// setIsLoading(derivedIsLoading); +// }, [derivedIsLoading]); + +// const currentState: UseTransactionsBaseResult = { +// data, +// result, +// loading, +// isFetching: derivedIsFetching, +// fetchStatus, +// isPaused: derivedIsPaused, +// status, +// error, +// updatedAt, +// dataUpdatedAt, +// errorUpdatedAt, +// failureCount, +// refetch, +// cancel, +// isStale: isStale(), + +// isError, +// isPending, +// isSuccess, + +// isFetched, +// isLoading, +// isLoadingError, +// isRefetchError, +// isRefetching: derivedIsRefetching, +// isFetchedAfterMount, +// isPlaceholderData, +// }; + +// prevStateRef.current = currentState; + +// useEffect(() => { +// if (!enabled) { +// setFetchStatus("paused"); +// } else { +// setFetchStatus((s) => (s === "fetching" ? s : "idle")); +// } +// }, [enabled]); + +// useEffect(() => { +// if (!enabled) return; + +// cancelledRef.current = false; + +// const shouldInitialFetch = !hasInitialized.current || (hasInitialized.current && queryOptions?.refetchOnMount && isStale()); + +// if (shouldInitialFetch) { +// runFetch().catch(() => {}); +// } + +// return () => { +// cancelledRef.current = true; +// clearRetryTimer(); +// }; +// }, [runFetch, enabled, queryOptions?.refetchOnMount, queryOptions?.staleTime, dataUpdatedAt]); + +// useEffect(() => { +// if (!enabled) return; + +// const onFocus = () => { +// if (queryOptions?.refetchOnWindowFocus && isStale()) { +// runFetch().catch(() => {}); +// } +// }; + +// const onOnline = () => { +// if (queryOptions?.refetchOnReconnect && isStale()) { +// runFetch().catch(() => {}); +// } +// }; + +// if (queryOptions?.refetchOnWindowFocus) { +// window.addEventListener("focus", onFocus); +// } +// if (queryOptions?.refetchOnReconnect) { +// window.addEventListener("online", onOnline); +// } + +// return () => { +// if (queryOptions?.refetchOnWindowFocus) { +// window.removeEventListener("focus", onFocus); +// } +// if (queryOptions?.refetchOnReconnect) { +// window.removeEventListener("online", onOnline); +// } +// }; +// }, [ +// enabled, +// queryOptions?.refetchOnWindowFocus, +// queryOptions?.refetchOnReconnect, +// queryOptions?.staleTime, +// dataUpdatedAt, +// isStale, +// runFetch, +// ]); + +// useEffect(() => { +// return () => { +// clearRetryTimer(); +// cancelledRef.current = true; +// }; +// }, []); + +// return currentState; +// } + +// export default useTransactions; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..408579d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,34 @@ +import { getState } from '@bluxcc/core'; + +export type CallBuilderOptions = { + cursor?: string; + limit?: number; + network?: string; + order?: 'asc' | 'desc'; +}; + +export const checkConfigCreated = () => { + const { stellar } = getState(); + + return !!stellar; +}; + +export const getAddress = (address?: string) => { + const { user } = getState(); + + if (address) { + return address; + } + + return user?.address as string; +}; + +export const getNetwork = (network?: string) => { + const { stellar } = getState(); + + if (!network && stellar) { + return stellar.activeNetwork; + } + + return network; +}; \ No newline at end of file