Skip to content
Draft
26 changes: 15 additions & 11 deletions aastar-frontend/app/address-book/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/Layout";
import { addressBookAPI } from "@/lib/api";
import { getAddressBook, setAddressName, removeAddress } from "@/lib/address-book-store";
import { useDashboard } from "@/contexts/DashboardContext";
import SwipeableListItem from "@/components/SwipeableListItem";
import toast from "react-hot-toast";
import { PencilIcon, PlusIcon, BookOpenIcon } from "@heroicons/react/24/outline";
Expand All @@ -19,6 +20,8 @@ interface AddressBookEntry {

export default function AddressBookPage() {
const router = useRouter();
const { data } = useDashboard();
const accountAddress = data?.account?.address ?? null;
const [addressBook, setAddressBook] = useState<AddressBookEntry[]>([]);
const [loading, setLoading] = useState(true);
const [editingAddress, setEditingAddress] = useState<string | null>(null);
Expand All @@ -27,15 +30,16 @@ export default function AddressBookPage() {
const [newAddress, setNewAddress] = useState("");
const [newName, setNewName] = useState("");

// Re-load when the (async) account resolves so the list is scoped to this account.
useEffect(() => {
loadAddressBook();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountAddress]);

const loadAddressBook = async () => {
const loadAddressBook = () => {
setLoading(true);
try {
const response = await addressBookAPI.getAddressBook();
setAddressBook(response.data);
setAddressBook(getAddressBook(accountAddress));
} catch (error) {
console.error("Failed to load address book:", error);
toast.error("Failed to load address book");
Expand All @@ -57,8 +61,8 @@ export default function AddressBookPage() {
}

try {
await addressBookAPI.setAddressName(newAddress, newName || "");
await loadAddressBook();
setAddressName(accountAddress, newAddress, newName || "");
loadAddressBook();
setShowAddForm(false);
setNewAddress("");
setNewName("");
Expand All @@ -73,8 +77,8 @@ export default function AddressBookPage() {

const handleUpdateName = async (address: string) => {
try {
await addressBookAPI.setAddressName(address, editingName);
await loadAddressBook();
setAddressName(accountAddress, address, editingName);
loadAddressBook();
setEditingAddress(null);
setEditingName("");
toast.success("Name updated successfully!");
Expand All @@ -92,8 +96,8 @@ export default function AddressBookPage() {
}

try {
await addressBookAPI.removeAddress(address);
await loadAddressBook();
removeAddress(accountAddress, address);
loadAddressBook();
toast.success("Address deleted successfully!");
} catch (error) {
const message =
Expand Down
25 changes: 14 additions & 11 deletions aastar-frontend/app/paymaster/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import Layout from "@/components/Layout";
import { paymasterAPI } from "@/lib/api";
import {
getAvailablePaymasters,
getPaymasterPresets,
addCustomPaymaster,
removeCustomPaymaster,
} from "@/lib/paymaster-store";
import { useDashboard } from "@/contexts/DashboardContext";
import SwipeableListItem from "@/components/SwipeableListItem";
import toast from "react-hot-toast";
Expand Down Expand Up @@ -55,7 +60,9 @@ export default function PaymasterPage() {

useEffect(() => {
loadPaymasters();
}, []);
// saved paymasters are account-scoped (localStorage) — reload when the account resolves
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountAddress]);

// Re-read the account-scoped default whenever the (async-loaded) account resolves,
// so the ☆ / "Default" state reflects THIS account's preference.
Expand All @@ -81,12 +88,8 @@ export default function PaymasterPage() {
const loadPaymasters = async () => {
setLoading(true);
try {
const [available, presetList] = await Promise.all([
paymasterAPI.getAvailable(),
paymasterAPI.getPresets().catch(() => ({ data: [] })),
]);
setPaymasters(available.data);
setPresets(presetList.data);
setPaymasters(getAvailablePaymasters(accountAddress));
setPresets(getPaymasterPresets());
} catch (error) {
console.error("Failed to load paymasters:", error);
toast.error("Failed to load paymasters");
Expand All @@ -99,7 +102,7 @@ export default function PaymasterPage() {
const handleAddPreset = async (preset: PaymasterPreset) => {
setActionLoading(`preset-${preset.name}`);
try {
await paymasterAPI.addCustom({
addCustomPaymaster(accountAddress, {
name: preset.name,
address: preset.address,
type: preset.type,
Expand Down Expand Up @@ -130,7 +133,7 @@ export default function PaymasterPage() {

setActionLoading("add");
try {
await paymasterAPI.addCustom({
addCustomPaymaster(accountAddress, {
name: newPaymaster.name,
address: newPaymaster.address,
type: newPaymaster.type,
Expand Down Expand Up @@ -166,7 +169,7 @@ export default function PaymasterPage() {
setActionLoading(`remove-${name}`);
try {
const removed = paymasters.find(p => p.name === name);
await paymasterAPI.remove(name);
removeCustomPaymaster(accountAddress, name);
// If the removed paymaster was the persisted default, drop the stale preference.
if (removed && defaultPaymaster === removed.address.toLowerCase()) {
clearDefaultPaymaster(accountAddress);
Expand Down
64 changes: 37 additions & 27 deletions aastar-frontend/app/transfer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import Layout from "@/components/Layout";
import TokenSelector from "@/components/TokenSelector";
import TransferSkeleton from "@/components/TransferSkeleton";
import { useDashboard } from "@/contexts/DashboardContext";
import { transferAPI, tokenAPI, paymasterAPI, addressBookAPI } from "@/lib/api";
import { transferAPI } from "@/lib/api";
import { getAvailablePaymasters, getPaymasterPresets } from "@/lib/paymaster-store";
import { getAddressBook, setAddressName, recordSuccessfulTransfer } from "@/lib/address-book-store";
import { getTokenBalance } from "@/lib/token-balance";
import { GasEstimate, Token, TokenBalance } from "@/lib/types";
import toast from "react-hot-toast";
import { startAuthentication } from "@simplewebauthn/browser";
Expand Down Expand Up @@ -184,6 +187,9 @@ export default function TransferPage() {
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const isPollingRef = useRef(false);
const defaultPaymasterAppliedForRef = useRef<string | null>(null);
// Recipient captured at submit time so the client can record it in the address book
// when polling confirms the transfer (formData.to is cleared right after submit).
const pendingRecipientRef = useRef<string>("");
const touchStartY = useRef(0);
const containerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
Expand All @@ -208,6 +214,12 @@ export default function TransferPage() {
}
}, [account?.address, savedPaymasters]);

// Saved paymasters are account-scoped (localStorage) — (re)load whenever the account
// resolves, so a fresh page load that beats the account fetch still populates them.
useEffect(() => {
setSavedPaymasters(getAvailablePaymasters(account?.address));
}, [account?.address]);

// Judge the transfer (tier + required signatures, ETH/ERC-20 unified) whenever the
// amount or token changes, debounced. Drives the indicator + fail-fast on block.
useEffect(() => {
Expand Down Expand Up @@ -244,29 +256,17 @@ export default function TransferPage() {
const loadPageData = async () => {
setLoading(prev => ({ ...prev, page: true }));
try {
// Load saved paymasters
try {
const [paymasterResponse, presetResponse] = await Promise.all([
paymasterAPI.getAvailable(),
paymasterAPI.getPresets().catch(() => ({ data: [] })),
]);
setPaymasterPresets((presetResponse.data ?? []) as any[]);
setSavedPaymasters((paymasterResponse.data ?? []) as any[]);
// The persisted default paymaster is applied by a dedicated effect once the
// account address (its storage scope) and the saved list are both ready.
} catch (error) {
console.error("Failed to load saved paymasters:", error);
setSavedPaymasters([]);
}
// Presets are account-independent (SDK canonical). Saved paymasters are
// account-scoped and loaded by a dedicated effect (below) once the account resolves.
setPaymasterPresets(getPaymasterPresets());

// Load address book + recent transfer recipients
// Load address book (client-side store) + recent transfer recipients (history API)
try {
const [addressBookResponse, historyResponse] = await Promise.all([
addressBookAPI.getAddressBook().catch(() => ({ data: [] })),
transferAPI.getHistory(1, 50).catch(() => ({ data: { transfers: [] } })),
]);
const historyResponse = await transferAPI
.getHistory(1, 50)
.catch(() => ({ data: { transfers: [] } }));

const bookEntries = addressBookResponse.data || [];
const bookEntries = getAddressBook(account?.address);
const bookAddresses = new Set(bookEntries.map((e: any) => e.address.toLowerCase()));

// Extract unique recent recipients not already in address book
Expand Down Expand Up @@ -300,15 +300,14 @@ export default function TransferPage() {
};

const loadTokenBalance = async (token: Token | null) => {
if (!token || token.address === "ETH") {
if (!token || token.address === "ETH" || !account?.address) {
setTokenBalance(null);
return;
}

setLoadingTokenBalance(true);
try {
const response = await tokenAPI.getTokenBalance(token.address);
setTokenBalance(response.data);
setTokenBalance(await getTokenBalance(account.address, token.address));
} catch (error) {
console.error("Failed to load token balance:", error);
setTokenBalance(null);
Expand Down Expand Up @@ -561,6 +560,10 @@ export default function TransferPage() {
}
toast.success("Transfer submitted! Tracking status...");

// Capture the recipient before the form is cleared so the address-book record
// (on confirmation) has it.
pendingRecipientRef.current = formData.to;

// Start polling for status
startStatusPolling(response.data.transferId);

Expand Down Expand Up @@ -684,6 +687,14 @@ export default function TransferPage() {
// Only show toast once when polling was active
if (wasPolling) {
if (response.data.status === "completed") {
// Record the confirmed recipient in the client-side address book (usage++,
// keep tx hash) — replaces the old server-side recordSuccessfulTransfer.
recordSuccessfulTransfer(
account?.address,
pendingRecipientRef.current,
response.data.transactionHash || ""
);
pendingRecipientRef.current = "";
toast.success("Transfer completed successfully!");
} else {
toast.error(`Transfer failed: ${response.data.error || "Unknown error"}`);
Expand Down Expand Up @@ -807,11 +818,10 @@ export default function TransferPage() {
const name = prompt("Enter a name for this address (optional):");

try {
await addressBookAPI.setAddressName(formData.to, name || "");
setAddressName(account?.address, formData.to, name || "");

// Refresh address book
const addressBookResponse = await addressBookAPI.getAddressBook();
setAddressBook(addressBookResponse.data);
setAddressBook(getAddressBook(account?.address));

toast.success("Address saved to address book! 📖");
} catch (error: any) {
Expand Down
45 changes: 14 additions & 31 deletions aastar-frontend/components/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
MagnifyingGlassIcon,
} from "@heroicons/react/24/outline";
import { Token, UserTokenWithBalance } from "@/lib/types";
import { userTokenAPI } from "@/lib/api";
import { getUserTokens, addUserToken, initializeDefaultTokens } from "@/lib/user-token-store";
import TokenIcon from "@/components/TokenIcon";
import toast from "react-hot-toast";

Expand Down Expand Up @@ -77,30 +77,17 @@ export default function TokenSelector({
const loadTokens = async () => {
setLoading(true);
try {
// Load user tokens with balances
const response = await userTokenAPI.getUserTokens({
activeOnly: true,
withBalances: !!accountAddress && showBalances,
});
setUserTokens(response.data);
} catch (error: any) {
if (error.response?.status === 404) {
// User has no tokens yet, initialize with defaults
try {
await userTokenAPI.initializeDefaultTokens();
const response = await userTokenAPI.getUserTokens({
activeOnly: true,
withBalances: !!accountAddress && showBalances,
});
setUserTokens(response.data);
} catch (initError) {
console.error("Failed to initialize tokens:", initError);
toast.error("Failed to load tokens");
}
} else {
console.error("Failed to load tokens:", error);
toast.error("Failed to load tokens");
const opts = { activeOnly: true, withBalances: !!accountAddress && showBalances };
let tokens = await getUserTokens(accountAddress, opts);
// First use: seed the default preset tokens, then reload.
if (tokens.length === 0) {
initializeDefaultTokens(accountAddress);
tokens = await getUserTokens(accountAddress, opts);
}
setUserTokens(tokens);
} catch (error) {
console.error("Failed to load tokens:", error);
toast.error("Failed to load tokens");
} finally {
setLoading(false);
}
Expand All @@ -111,11 +98,7 @@ export default function TokenSelector({

setBalancesLoading(true);
try {
const response = await userTokenAPI.getUserTokens({
activeOnly: true,
withBalances: true,
});
setUserTokens(response.data);
setUserTokens(await getUserTokens(accountAddress, { activeOnly: true, withBalances: true }));
} catch (error) {
console.error("Failed to load token balances:", error);
// Don't show error toast for balances, as it's supplementary info
Expand All @@ -132,8 +115,8 @@ export default function TokenSelector({

setValidatingToken(true);
try {
const response = await userTokenAPI.addUserToken({ address: customTokenAddress });
toast.success(`Added ${response.data.symbol} token`);
const token = await addUserToken(accountAddress, customTokenAddress);
toast.success(`Added ${token.symbol} token`);
setCustomTokenAddress("");
setShowCustomTokenModal(false);
await loadTokens();
Expand Down
Loading
Loading