mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat(wallet): implement lazy-loaded transactions observable
- Add shared wallet types (Transaction, TransactionsState) in src/types/wallet.ts - Add transactionsState$ observable to NWC service for shared tx state - Implement loadTransactions, loadMoreTransactions, and retryLoadTransactions - Auto-refresh transactions on payment notifications - Simplify WalletViewer to use observable state instead of local state - Remove manual transaction loading logic from component https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* Layout: Header → Big centered balance → Send/Receive buttons → Transaction list
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Wallet,
|
||||
@@ -62,21 +62,7 @@ import { KindRenderer } from "./nostr/kinds";
|
||||
import { RichText } from "./nostr/RichText";
|
||||
import { UserName } from "./nostr/UserName";
|
||||
import { CodeCopyButton } from "./CodeCopyButton";
|
||||
|
||||
interface Transaction {
|
||||
type: "incoming" | "outgoing";
|
||||
invoice?: string;
|
||||
description?: string;
|
||||
description_hash?: string;
|
||||
preimage?: string;
|
||||
payment_hash?: string;
|
||||
amount: number;
|
||||
fees_paid?: number;
|
||||
created_at: number;
|
||||
expires_at?: number;
|
||||
settled_at?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
import type { Transaction } from "@/types/wallet";
|
||||
|
||||
interface InvoiceDetails {
|
||||
amount?: number;
|
||||
@@ -85,7 +71,6 @@ interface InvoiceDetails {
|
||||
expiry?: number;
|
||||
}
|
||||
|
||||
const BATCH_SIZE = 20;
|
||||
const PAYMENT_CHECK_INTERVAL = 5000; // Check every 5 seconds
|
||||
|
||||
/**
|
||||
@@ -403,27 +388,24 @@ export default function WalletViewer() {
|
||||
lastError,
|
||||
support,
|
||||
walletMethods, // Combined support$ + cached info fallback
|
||||
transactionsState,
|
||||
refreshBalance,
|
||||
listTransactions,
|
||||
makeInvoice,
|
||||
payInvoice,
|
||||
lookupInvoice,
|
||||
disconnect,
|
||||
reconnect,
|
||||
loadTransactions,
|
||||
loadMoreTransactions,
|
||||
retryLoadTransactions,
|
||||
} = useWallet();
|
||||
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [refreshingBalance, setRefreshingBalance] = useState(false);
|
||||
const [connectDialogOpen, setConnectDialogOpen] = useState(false);
|
||||
const [disconnectDialogOpen, setDisconnectDialogOpen] = useState(false);
|
||||
const [txLoadAttempted, setTxLoadAttempted] = useState(false);
|
||||
const [txLoadFailed, setTxLoadFailed] = useState(false);
|
||||
|
||||
// Rate limiting refs
|
||||
// Rate limiting ref
|
||||
const lastBalanceRefreshRef = useRef(0);
|
||||
const lastTxLoadRef = useRef(0);
|
||||
|
||||
// Send dialog state
|
||||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||
@@ -456,73 +438,17 @@ export default function WalletViewer() {
|
||||
const { copy: copyRawTx, copied: rawTxCopied } = useCopy(2000);
|
||||
const { copy: copyNwc, copied: nwcCopied } = useCopy(2000);
|
||||
|
||||
// Reset transaction state when wallet disconnects
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
setHasMore(true);
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
// Reset transaction load flag when wallet connects (to trigger reload)
|
||||
useEffect(() => {
|
||||
if (connectionStatus === "connected") {
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
// Load transactions when wallet methods are available (only once)
|
||||
// walletMethods combines support$ observable with cached info fallback
|
||||
// Trigger lazy load of transactions when wallet supports it
|
||||
useEffect(() => {
|
||||
if (
|
||||
walletMethods.includes("list_transactions") &&
|
||||
!txLoadAttempted &&
|
||||
!loading
|
||||
!transactionsState.initialized
|
||||
) {
|
||||
setLoading(true);
|
||||
setTxLoadAttempted(true);
|
||||
listTransactions({
|
||||
limit: BATCH_SIZE,
|
||||
offset: 0,
|
||||
})
|
||||
.then((result) => {
|
||||
const txs = result.transactions || [];
|
||||
setTransactions(txs);
|
||||
setHasMore(txs.length === BATCH_SIZE);
|
||||
setTxLoadFailed(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load transactions:", error);
|
||||
setTxLoadFailed(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
loadTransactions();
|
||||
}
|
||||
}, [walletMethods, txLoadAttempted, loading, listTransactions]);
|
||||
|
||||
// Helper to reload transactions (resets flags to trigger reload)
|
||||
const reloadTransactions = useCallback(() => {
|
||||
// Rate limiting: minimum 5 seconds between transaction reloads
|
||||
const now = Date.now();
|
||||
const timeSinceLastLoad = now - lastTxLoadRef.current;
|
||||
if (timeSinceLastLoad < 5000) {
|
||||
const waitTime = Math.ceil((5000 - timeSinceLastLoad) / 1000);
|
||||
toast.warning(`Please wait ${waitTime}s before reloading transactions`);
|
||||
return;
|
||||
}
|
||||
|
||||
lastTxLoadRef.current = now;
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
}, []);
|
||||
}, [walletMethods, transactionsState.initialized, loadTransactions]);
|
||||
|
||||
// Poll for payment status when waiting for invoice to be paid
|
||||
useEffect(() => {
|
||||
if (!generatedPaymentHash || !receiveDialogOpen) return;
|
||||
|
||||
@@ -532,13 +458,11 @@ export default function WalletViewer() {
|
||||
setCheckingPayment(true);
|
||||
try {
|
||||
const result = await lookupInvoice(generatedPaymentHash);
|
||||
// If invoice is settled, close dialog and refresh
|
||||
// If invoice is settled, close dialog (notifications will refresh transactions)
|
||||
if (result.settled_at) {
|
||||
toast.success("Payment received!");
|
||||
setReceiveDialogOpen(false);
|
||||
resetReceiveDialog();
|
||||
// Reload transactions
|
||||
reloadTransactions();
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, will retry
|
||||
@@ -549,45 +473,7 @@ export default function WalletViewer() {
|
||||
|
||||
const intervalId = setInterval(checkPayment, PAYMENT_CHECK_INTERVAL);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [
|
||||
generatedPaymentHash,
|
||||
receiveDialogOpen,
|
||||
walletMethods,
|
||||
lookupInvoice,
|
||||
reloadTransactions,
|
||||
]);
|
||||
|
||||
const loadMoreTransactions = useCallback(async () => {
|
||||
if (
|
||||
!walletMethods.includes("list_transactions") ||
|
||||
!hasMore ||
|
||||
loadingMore
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const result = await listTransactions({
|
||||
limit: BATCH_SIZE,
|
||||
offset: transactions.length,
|
||||
});
|
||||
const newTxs = result.transactions || [];
|
||||
setTransactions((prev) => [...prev, ...newTxs]);
|
||||
setHasMore(newTxs.length === BATCH_SIZE);
|
||||
} catch (error) {
|
||||
console.error("Failed to load more transactions:", error);
|
||||
toast.error("Failed to load more transactions");
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [
|
||||
walletMethods,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
transactions.length,
|
||||
listTransactions,
|
||||
]);
|
||||
}, [generatedPaymentHash, receiveDialogOpen, walletMethods, lookupInvoice]);
|
||||
|
||||
async function handleRefreshBalance() {
|
||||
// Rate limiting: minimum 2 seconds between refreshes
|
||||
@@ -600,7 +486,7 @@ export default function WalletViewer() {
|
||||
}
|
||||
|
||||
lastBalanceRefreshRef.current = now;
|
||||
setLoading(true);
|
||||
setRefreshingBalance(true);
|
||||
try {
|
||||
await refreshBalance();
|
||||
toast.success("Balance refreshed");
|
||||
@@ -608,7 +494,7 @@ export default function WalletViewer() {
|
||||
console.error("Failed to refresh balance:", error);
|
||||
toast.error("Failed to refresh balance");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshingBalance(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,8 +688,7 @@ export default function WalletViewer() {
|
||||
toast.success("Payment sent successfully");
|
||||
resetSendDialog();
|
||||
setSendDialogOpen(false);
|
||||
// Reload transactions
|
||||
reloadTransactions();
|
||||
// Notifications will automatically refresh transactions
|
||||
} catch (error) {
|
||||
console.error("Payment failed:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Payment failed");
|
||||
@@ -902,6 +787,13 @@ export default function WalletViewer() {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
// Derive values from transactionsState for convenience
|
||||
const transactions = transactionsState.items;
|
||||
const txLoading = transactionsState.loading;
|
||||
const txLoadingMore = transactionsState.loadingMore;
|
||||
const txHasMore = transactionsState.hasMore;
|
||||
const txError = transactionsState.error;
|
||||
|
||||
// Process transactions to include day markers
|
||||
const transactionsWithMarkers = useMemo(() => {
|
||||
if (!transactions || transactions.length === 0) return [];
|
||||
@@ -1126,12 +1018,12 @@ export default function WalletViewer() {
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleRefreshBalance}
|
||||
disabled={loading}
|
||||
disabled={refreshingBalance}
|
||||
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
aria-label="Refresh balance"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`size-3 ${loading ? "animate-spin" : ""}`}
|
||||
className={`size-3 ${refreshingBalance ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -1199,11 +1091,11 @@ export default function WalletViewer() {
|
||||
<div className="flex-1 overflow-hidden flex justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
{walletMethods.includes("list_transactions") ? (
|
||||
loading ? (
|
||||
txLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<RefreshCw className="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : txLoadFailed ? (
|
||||
) : txError ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Failed to load transaction history
|
||||
@@ -1211,7 +1103,7 @@ export default function WalletViewer() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={reloadTransactions}
|
||||
onClick={retryLoadTransactions}
|
||||
>
|
||||
<RefreshCw className="mr-2 size-4" />
|
||||
Retry
|
||||
@@ -1269,11 +1161,11 @@ export default function WalletViewer() {
|
||||
}}
|
||||
components={{
|
||||
Footer: () =>
|
||||
loadingMore ? (
|
||||
txLoadingMore ? (
|
||||
<div className="flex justify-center py-4 border-b border-border">
|
||||
<RefreshCw className="size-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : !hasMore && transactions.length > 0 ? (
|
||||
) : !txHasMore && transactions.length > 0 ? (
|
||||
<div className="py-4 text-center text-xs text-muted-foreground border-b border-border">
|
||||
No more transactions
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
balance$,
|
||||
connectionStatus$,
|
||||
lastError$,
|
||||
transactionsState$,
|
||||
loadTransactions as loadTransactionsService,
|
||||
loadMoreTransactions as loadMoreTransactionsService,
|
||||
retryLoadTransactions as retryLoadTransactionsService,
|
||||
} from "@/services/nwc";
|
||||
|
||||
export function useWallet() {
|
||||
@@ -47,6 +51,7 @@ export function useWallet() {
|
||||
const balance = use$(balance$);
|
||||
const connectionStatus = use$(connectionStatus$);
|
||||
const lastError = use$(lastError$);
|
||||
const transactionsState = use$(transactionsState$);
|
||||
|
||||
// Wallet support from library's support$ observable (cached)
|
||||
const support = use$(() => wallet?.support$, [wallet]);
|
||||
@@ -145,6 +150,18 @@ export function useWallet() {
|
||||
return await refreshBalanceService();
|
||||
}
|
||||
|
||||
async function loadTransactions() {
|
||||
await loadTransactionsService();
|
||||
}
|
||||
|
||||
async function loadMoreTransactions() {
|
||||
await loadMoreTransactionsService();
|
||||
}
|
||||
|
||||
async function retryLoadTransactions() {
|
||||
await retryLoadTransactionsService();
|
||||
}
|
||||
|
||||
return {
|
||||
// State (all derived from observables)
|
||||
wallet,
|
||||
@@ -154,6 +171,7 @@ export function useWallet() {
|
||||
lastError,
|
||||
support,
|
||||
walletMethods,
|
||||
transactionsState,
|
||||
|
||||
// Operations
|
||||
payInvoice,
|
||||
@@ -166,5 +184,8 @@ export function useWallet() {
|
||||
payKeysend,
|
||||
disconnect,
|
||||
reconnect,
|
||||
loadTransactions,
|
||||
loadMoreTransactions,
|
||||
retryLoadTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
import { WalletConnect } from "applesauce-wallet-connect";
|
||||
import type { NWCConnection } from "@/types/app";
|
||||
import {
|
||||
type TransactionsState,
|
||||
INITIAL_TRANSACTIONS_STATE,
|
||||
} from "@/types/wallet";
|
||||
import pool from "./relay-pool";
|
||||
import { BehaviorSubject, Subscription, firstValueFrom, timeout } from "rxjs";
|
||||
|
||||
@@ -49,6 +53,11 @@ export const lastError$ = new BehaviorSubject<Error | null>(null);
|
||||
/** Current balance in millisats */
|
||||
export const balance$ = new BehaviorSubject<number | undefined>(undefined);
|
||||
|
||||
/** Transaction list state (lazy loaded) */
|
||||
export const transactionsState$ = new BehaviorSubject<TransactionsState>(
|
||||
INITIAL_TRANSACTIONS_STATE,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Internal helpers
|
||||
// ============================================================================
|
||||
@@ -89,8 +98,9 @@ function subscribeToNotifications(wallet: WalletConnect) {
|
||||
lastError$.next(null);
|
||||
}
|
||||
|
||||
// Refresh balance on any notification
|
||||
// Refresh balance and transactions on any notification
|
||||
refreshBalance();
|
||||
refreshTransactions();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error("[NWC] Notification error:", error);
|
||||
@@ -205,6 +215,7 @@ export function clearWallet(): void {
|
||||
balance$.next(undefined);
|
||||
connectionStatus$.next("disconnected");
|
||||
lastError$.next(null);
|
||||
resetTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,3 +276,162 @@ export async function reconnect(): Promise<void> {
|
||||
subscribeToNotifications(wallet);
|
||||
await refreshBalance();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transaction loading (lazy, paginated)
|
||||
// ============================================================================
|
||||
|
||||
const TRANSACTIONS_PAGE_SIZE = 20;
|
||||
|
||||
/**
|
||||
* Loads the initial batch of transactions.
|
||||
* Only loads if not already initialized (lazy loading).
|
||||
*/
|
||||
export async function loadTransactions(): Promise<void> {
|
||||
const wallet = wallet$.value;
|
||||
if (!wallet) return;
|
||||
|
||||
const current = transactionsState$.value;
|
||||
|
||||
// Skip if already loading or initialized
|
||||
if (current.loading || current.initialized) return;
|
||||
|
||||
transactionsState$.next({
|
||||
...current,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await wallet.listTransactions({
|
||||
limit: TRANSACTIONS_PAGE_SIZE,
|
||||
});
|
||||
|
||||
transactionsState$.next({
|
||||
items: result.transactions,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: result.transactions.length >= TRANSACTIONS_PAGE_SIZE,
|
||||
error: null,
|
||||
initialized: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[NWC] Failed to load transactions:", error);
|
||||
transactionsState$.next({
|
||||
...transactionsState$.value,
|
||||
loading: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("Failed to load transactions"),
|
||||
initialized: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads more transactions (pagination).
|
||||
*/
|
||||
export async function loadMoreTransactions(): Promise<void> {
|
||||
const wallet = wallet$.value;
|
||||
if (!wallet) return;
|
||||
|
||||
const current = transactionsState$.value;
|
||||
|
||||
// Skip if already loading or no more to load
|
||||
if (current.loading || current.loadingMore || !current.hasMore) return;
|
||||
|
||||
transactionsState$.next({
|
||||
...current,
|
||||
loadingMore: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// Get the oldest transaction timestamp for pagination
|
||||
const oldestTx = current.items[current.items.length - 1];
|
||||
const until = oldestTx?.created_at;
|
||||
|
||||
const result = await wallet.listTransactions({
|
||||
limit: TRANSACTIONS_PAGE_SIZE,
|
||||
until,
|
||||
});
|
||||
|
||||
// Filter out any duplicates (in case of overlapping timestamps)
|
||||
const existingHashes = new Set(current.items.map((tx) => tx.payment_hash));
|
||||
const newTransactions = result.transactions.filter(
|
||||
(tx) => !existingHashes.has(tx.payment_hash),
|
||||
);
|
||||
|
||||
transactionsState$.next({
|
||||
...current,
|
||||
items: [...current.items, ...newTransactions],
|
||||
loadingMore: false,
|
||||
hasMore: result.transactions.length >= TRANSACTIONS_PAGE_SIZE,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[NWC] Failed to load more transactions:", error);
|
||||
transactionsState$.next({
|
||||
...current,
|
||||
loadingMore: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("Failed to load more transactions"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the transaction list (prepends new transactions).
|
||||
* Called automatically on payment notifications.
|
||||
*/
|
||||
export async function refreshTransactions(): Promise<void> {
|
||||
const wallet = wallet$.value;
|
||||
if (!wallet) return;
|
||||
|
||||
const current = transactionsState$.value;
|
||||
|
||||
// Only refresh if already initialized
|
||||
if (!current.initialized) return;
|
||||
|
||||
try {
|
||||
// Get the newest transaction timestamp
|
||||
const newestTx = current.items[0];
|
||||
const from = newestTx?.created_at ? newestTx.created_at + 1 : undefined;
|
||||
|
||||
const result = await wallet.listTransactions({
|
||||
limit: TRANSACTIONS_PAGE_SIZE,
|
||||
from,
|
||||
});
|
||||
|
||||
// Filter out duplicates and prepend new transactions
|
||||
const existingHashes = new Set(current.items.map((tx) => tx.payment_hash));
|
||||
const newTransactions = result.transactions.filter(
|
||||
(tx) => !existingHashes.has(tx.payment_hash),
|
||||
);
|
||||
|
||||
if (newTransactions.length > 0) {
|
||||
transactionsState$.next({
|
||||
...current,
|
||||
items: [...newTransactions, ...current.items],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[NWC] Failed to refresh transactions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets transaction state (called on wallet clear).
|
||||
*/
|
||||
function resetTransactions(): void {
|
||||
transactionsState$.next(INITIAL_TRANSACTIONS_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reload transactions (used for retry after error).
|
||||
*/
|
||||
export async function retryLoadTransactions(): Promise<void> {
|
||||
resetTransactions();
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
51
src/types/wallet.ts
Normal file
51
src/types/wallet.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Wallet-related type definitions for NWC (NIP-47)
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Lightning/Bitcoin transaction from NWC list_transactions
|
||||
*/
|
||||
export interface Transaction {
|
||||
type: "incoming" | "outgoing";
|
||||
invoice?: string;
|
||||
description?: string;
|
||||
description_hash?: string;
|
||||
preimage?: string;
|
||||
payment_hash?: string;
|
||||
amount: number;
|
||||
fees_paid?: number;
|
||||
created_at: number;
|
||||
expires_at?: number;
|
||||
settled_at?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the transactions observable
|
||||
*/
|
||||
export interface TransactionsState {
|
||||
/** The list of transactions */
|
||||
items: Transaction[];
|
||||
/** Whether we're loading the initial batch */
|
||||
loading: boolean;
|
||||
/** Whether we're loading more (pagination) */
|
||||
loadingMore: boolean;
|
||||
/** Whether there are more transactions to load */
|
||||
hasMore: boolean;
|
||||
/** Error from last load attempt */
|
||||
error: Error | null;
|
||||
/** Whether initial load has been triggered */
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state for transactions
|
||||
*/
|
||||
export const INITIAL_TRANSACTIONS_STATE: TransactionsState = {
|
||||
items: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
error: null,
|
||||
initialized: false,
|
||||
};
|
||||
Reference in New Issue
Block a user