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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 59 additions & 42 deletions src/app/dashboard/wallets/[id]/addresses/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
"use client";

import { use, useEffect, useState } from "react";
import { use, useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { useAuth } from "@/lib/useAuth";
import { useWallet } from "@/lib/useWallet";
import {
listAddresses,
listRecentAddresses,
listAddressesPage,
createAddress,
type Address,
} from "@/lib/wallets";
import { Modal, CopyField } from "@/components/dashboard/Modal";
import { Stat, ActionButton, Panel, Empty } from "@/components/dashboard/WalletUI";
import { PageSpinner } from "@/components/OctoSpinner";
import { Pagination } from "@/components/dashboard/Pagination";
import { AddressSearchBox } from "@/components/addresses/AddressSearchBox";
import { filterAddressesByRef } from "@/lib/addressSearch";
import { NewAddressModal } from "@/components/addresses/CustomerReferenceField";
import { formatStroops, sumStroops } from "@/lib/amount";
import { formatStroops } from "@/lib/amount";

// Dynamic render so the strict nonce CSP (src/proxy.ts) applies — matches the other
// /dashboard/wallets/:id/* pages, which all read wallet-scoped data.
Expand All @@ -40,35 +40,50 @@ export default function AddressesPage({
const [selected, setSelected] = useState<Address | null>(null);
const [error, setError] = useState<string | null>(null);

// `cursors[i]` is the `before` cursor for page i+1; the API has no offsets, so Prev is a pop.
const [cursors, setCursors] = useState<(string | null)[]>([null]);
const [pageIndex, setPageIndex] = useState(0);
const [nextCursor, setNextCursor] = useState<string | null>(null);

const load = useCallback(
(before: string | null) => {
if (!token) return;
setRefreshing(true);
listAddressesPage(token, id, { before })
.then((page) => {
setAddresses(page.data);
setNextCursor(page.next_cursor);
setError(null);
})
.catch((e) => {
setError(e instanceof Error ? e.message : "Could not load addresses.");
})
.finally(() => setRefreshing(false));
},
[token, id],
);

function refresh() {
if (!token) return;
setRefreshing(true);
listRecentAddresses(token, id)
.then((a) => {
setAddresses(a);
setError(null);
})
.catch((e) =>
setError(e instanceof Error ? e.message : "Could not load addresses."),
)
.finally(() => setRefreshing(false));
load(cursors[pageIndex] ?? null);
}

useEffect(() => {
if (!token) return;
const controller = new AbortController();
listAddresses(token, id, { signal: controller.signal })
.then((a) => {
setAddresses(a);
setError(null);
})
.catch((e) => {
if (e instanceof DOMException && e.name === "AbortError") return;
setAddresses([]);
setError(e instanceof Error ? e.message : "Could not load addresses.");
});
return () => controller.abort();
}, [token, id]);
load(null);
}, [load]);

function goNext() {
if (!nextCursor) return;
setCursors((c) => [...c.slice(0, pageIndex + 1), nextCursor]);
setPageIndex((i) => i + 1);
load(nextCursor);
}

function goPrev() {
if (pageIndex === 0) return;
const target = cursors[pageIndex - 1];
setPageIndex((i) => i - 1);
load(target ?? null);
}

async function handleNewAddress(customerRef?: string) {
if (!token) return;
Expand All @@ -84,8 +99,6 @@ export default function AddressesPage({
}
}

const totalReceived = sumStroops(addresses.map((a) => a.received_stroops));

if (loading || !user) {
return (
<PageSpinner />
Expand All @@ -112,13 +125,8 @@ export default function AddressesPage({
</div>

{/* stat cards */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<Stat label="Total addresses" value={String(addresses.length)} />
<Stat
label="Total received"
value={`${formatStroops(totalReceived)} XLM`}
sub="Across all addresses"
/>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
<Stat label="Addresses on this page" value={String(addresses.length)} />
<Stat
label="Address type"
value="Muxed (M…)"
Expand Down Expand Up @@ -180,6 +188,14 @@ export default function AddressesPage({
</table>
</div>
)}
<Pagination
page={pageIndex + 1}
hasPrev={pageIndex > 0}
hasNext={nextCursor !== null}
loading={refreshing}
onPrev={goPrev}
onNext={goNext}
/>
</Panel>
</div>

Expand Down Expand Up @@ -212,9 +228,7 @@ function AddressRow({
</td>
<td className="py-3 pr-4 font-mono text-muted">{address.memo_id}</td>
<td className="py-3 font-medium text-foreground">
{address.received_stroops > 0
? `${formatStroops(address.received_stroops)} XLM`
: "$0"}
{formatStroops(address.received_stroops)}
</td>
</tr>
);
Expand All @@ -233,7 +247,10 @@ function AddressDetail({
<div className="rounded-lg bg-surface-sunken p-3 text-center">
<p className="text-xs text-muted">Total received</p>
<p className="mt-1 text-xl font-semibold text-foreground">
{formatStroops(address.received_stroops)} XLM
{formatStroops(address.received_stroops)}
</p>
<p className="mt-1 text-[11px] text-muted">
Deposits across all assets; see Transactions for per-asset amounts.
</p>
</div>

Expand Down
22 changes: 7 additions & 15 deletions src/app/dashboard/wallets/[id]/api/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
import { PageSpinner } from "@/components/OctoSpinner";
import { CopyButton } from "@/components/CopyButton";
import { Modal } from "@/components/dashboard/Modal";
import { ApiError } from "@/lib/api";
import { ApiError, API_URL } from "@/lib/api";
import { Code } from "@/components/docs/DocsUI";
import { toast } from "sonner";

import { WalletPageShell } from "@/components/dashboard/WalletPageShell";
Expand Down Expand Up @@ -183,17 +184,12 @@ export default function DevelopersPage({

{/* quickstart */}
{keyInfo?.configured && (
<div className="mt-8 overflow-hidden rounded-xl border border-border bg-code-bg">
<div className="border-b border-border px-4 py-2 text-xs text-muted">
Quickstart — generate a deposit address
</div>
<pre className="overflow-x-auto p-4 font-mono text-xs leading-relaxed text-foreground/80">
<code>{`curl -X POST \\
${apiBase()}/v1/wallets/${id}/addresses \\
-H "authorization: Bearer <YOUR_LOGIN_TOKEN>" \\
<div className="mt-3">
<Code label="Quickstart — generate a deposit address">{`curl -X POST \\
${API_URL}/v1/wallets/${id}/addresses \\
-H "authorization: Bearer <YOUR_API_KEY>" \\
-H "content-type: application/json" \\
-d '{"customer_ref":"user_123"}'`}</code>
</pre>
-d '{"customer_ref":"user_123"}'`}</Code>
</div>
)}
</div>
Expand Down Expand Up @@ -277,10 +273,6 @@ function RegenerateKeyModal({
);
}

function apiBase() {
return process.env.NEXT_PUBLIC_OCTO_API_URL ?? "http://localhost:8080";
}

function maskFrom(key: string) {
const prefix = key.split("_").slice(0, 3).join("_") + "_";
return prefix + "•".repeat(Math.max(0, key.length - prefix.length));
Expand Down
5 changes: 2 additions & 3 deletions src/app/dashboard/wallets/[id]/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,14 @@ export default function AssetsPage({
</div>

{/* stat cards */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
<Stat label="Total XLM" value={xlm ? `${xlm.balance} XLM` : "0 XLM"} />
<Stat label="No. of assets" value={String(balances.length || (xlm ? 1 : 0))} />
<Stat label="No. of assets" value={String(balances.length)} />
<Stat
label="Trustlines held"
value={String(credits.length)}
sub="Non-native assets"
/>
<Stat label="Reserve" value="1.0 XLM" sub="Base account reserve" />
</div>

<SpendableReservedBreakdown token={token} walletId={id} balances={balances} />
Expand Down
Loading