Skip to content

Commit b54524e

Browse files
committed
fix(treasury): attribute warehouse credits per split, page on demand
Three fixes found live, one of them a mislabel on the public page: - Warehouse credits are attributed by which SPLIT emitted in the credit's own transaction, via one immutable-receipt read per warehouse row. "Warehouse USDC = Morpheus Subnet" expired the day the /swap fee split first distributed — its 2026-06-22 credit ran on the page under the wrong product name. Known splits map to products (subnet, swap); a bare withdraw or an unmapped split falls to the generic `splits` tag, which no longer borrows the subnet's green. - The pager fetches deeper history when the next-arrow hits the loaded edge (Alchemy pageKey cursor via /api/treasury/inflows), with a spinner while fetching, a visible failure line that keeps the cursor for retry, and a total that stays open-ended ("34+") while a cursor remains. Exhausted history and not-yet-fetched are distinct states. - Known entities carry their real marks: Morpheus logo on receipt-proven subnet rows, the red noggles on auction rows, rider cut-outs (via the sponsorship card's exported crop) for vault/split rows. Unknown senders keep the identicon; no approximated logos. - Splits links point at explorer.splits.org — app.splits.org answers "Account not found" for these SplitV2 contracts (verified in browser).
1 parent bc40a37 commit b54524e

7 files changed

Lines changed: 412 additions & 156 deletions

File tree

messages/en/treasury.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"auctionHint": "Arrived from the auction house as an internal transfer",
7575
"viewTx": "View transaction on Basescan",
7676
"subnet": "Morpheus Subnet",
77+
"swap": "Swap fee",
7778
"splits": "Splits",
7879
"transfer": "Transfer",
7980
"subtitle": "Every credit to the treasury, tagged by where it came from",
@@ -85,7 +86,8 @@
8586
"nameAuction": "Auction house",
8687
"nameWarehouse": "Splits Warehouse",
8788
"nameVault": "Rider vault",
88-
"nameSplit": "Rider split"
89+
"nameSplit": "Rider split",
90+
"loadFailed": "Couldn't load more — try again"
8991
},
9092
"allocation": {
9193
"title": "Allocation",

messages/pt-br/treasury.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"auctionHint": "Chegou do contrato de leilão como transferência interna",
7575
"viewTx": "Ver transação no Basescan",
7676
"subnet": "Morpheus Subnet",
77+
"swap": "Taxa de swap",
7778
"splits": "Splits",
7879
"transfer": "Transferência",
7980
"subtitle": "Cada crédito no tesouro, marcado pela origem",
@@ -85,7 +86,8 @@
8586
"nameAuction": "Casa de leilão",
8687
"nameWarehouse": "Splits Warehouse",
8788
"nameVault": "Vault do rider",
88-
"nameSplit": "Split do rider"
89+
"nameSplit": "Split do rider",
90+
"loadFailed": "Não deu pra carregar mais — tenta de novo"
8991
},
9092
"allocation": {
9193
"title": "Alocação",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { loadTreasuryInflowsPage } from "@/services/treasury-inflows";
3+
4+
/**
5+
* Deeper inflow history for the client-side pager, one indexer page per call.
6+
*
7+
* `pageKey` is Alchemy's opaque cursor, handed to the client by the previous
8+
* page. Pages are append-only history, so a short CDN window is safe — but a
9+
* FAILED page must answer 500 with no-store, never an empty 200: an empty page
10+
* with no cursor reads as "history complete", and caching that lie is the same
11+
* bug class as the empty backer list on /stake.
12+
*/
13+
export const dynamic = "force-dynamic";
14+
15+
export async function GET(req: NextRequest) {
16+
const pageKey = req.nextUrl.searchParams.get("pageKey") ?? undefined;
17+
try {
18+
const page = await loadTreasuryInflowsPage(pageKey);
19+
return NextResponse.json(page, {
20+
headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" },
21+
});
22+
} catch {
23+
return NextResponse.json(
24+
{ error: "inflows_page_failed" },
25+
{ status: 500, headers: { "Cache-Control": "no-store" } },
26+
);
27+
}
28+
}

src/components/treasury/SponsorshipYield.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { RIDER_LIST, type RiderId } from "@/lib/gnars-vaults";
4141
* module graph onto the treasury page for the sake of seven thumbnails. A rider
4242
* gaining a new cut-out needs it in both.
4343
*/
44-
const AVATAR: Record<RiderId, { src: string; size: string; pos: string }> = {
44+
export const AVATAR: Record<RiderId, { src: string; size: string; pos: string }> = {
4545
vlad: { src: "/stake/cutout/vlad.png", size: "420%", pos: "50% 6%" },
4646
yan: { src: "/stake/cutout/yan.png", size: "420%", pos: "50% 5%" },
4747
r4to: { src: "/stake/cutout/r4to.png", size: "420%", pos: "50% 5%" },
@@ -52,7 +52,10 @@ const AVATAR: Record<RiderId, { src: string; size: string; pos: string }> = {
5252
ephraim: { src: "/stake/cutout/ephraim.png", size: "400%", pos: "50% 8%" },
5353
};
5454

55-
const SPLITS_APP = (split: string) => `https://app.splits.org/accounts/${split}/?chainId=8453`;
55+
// explorer.splits.org, NOT app.splits.org: the app answers "Account not found"
56+
// for these SplitV2 contracts (verified in a browser on Vlad's split), while
57+
// the explorer renders the contract, its recipients and the Distribute button.
58+
const SPLITS_APP = (split: string) => `https://explorer.splits.org/accounts/${split}/?chainId=8453`;
5659

5760
export function SponsorshipYield() {
5861
const t = useTranslations("treasury.sponsorship");

src/components/treasury/TreasuryInflows.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ import { loadTreasuryInflows } from "@/services/treasury-inflows";
1111
* This component is now only the frame and the fetch — the rows moved to
1212
* `TreasuryInflowsList`, a client component, because "show more" needs state.
1313
*
14-
* The whole window is fetched here and paged in the browser rather than sliced
15-
* on the server, and the ordering is why: inflows are newest-first, and the
16-
* auction credits are older than the newest handful. Fetching eight rows meant
17-
* the Auction badge existed in code, fired correctly, and was never once on
18-
* screen. Paging client-side costs a slightly larger payload and shows the
19-
* history that makes the badges worth having.
14+
* The first indexer page is fetched here; the browser pages through it and
15+
* fetches deeper history on demand through /api/treasury/inflows, cursor in
16+
* hand. Server-slicing to a handful was how the Auction badge once existed in
17+
* code, fired correctly, and was never once on screen.
2018
*/
21-
const WINDOW = 100;
22-
2319
export async function TreasuryInflows({ locale }: { locale: string }) {
2420
const t = await getTranslations("treasury.inflows");
25-
const inflows = await loadTreasuryInflows(WINDOW);
21+
const { inflows, nextPageKey } = await loadTreasuryInflows();
2622
// Rendered once per request on the server; the timestamp IS the snapshot, so
2723
// the relative ages cannot shift between server render and hydration.
2824
// eslint-disable-next-line react-hooks/purity -- server component, render-time clock read for relative ages
@@ -54,7 +50,12 @@ export async function TreasuryInflows({ locale }: { locale: string }) {
5450
{inflows.length === 0 ? (
5551
<p className="py-6 text-center text-sm text-muted-foreground">{t("empty")}</p>
5652
) : (
57-
<TreasuryInflowsList inflows={inflows} locale={locale} now={now} />
53+
<TreasuryInflowsList
54+
inflows={inflows}
55+
nextPageKey={nextPageKey}
56+
locale={locale}
57+
now={now}
58+
/>
5859
)}
5960
</CardContent>
6061
</Card>

0 commit comments

Comments
 (0)