From 4c38fee7c7ababec514d0d0182b47d365ade6e5d Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Tue, 25 Aug 2026 15:06:03 -0500 Subject: [PATCH 1/2] Fix two pre-existing type errors breaking next build on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPoolHistory's declared return type was a single object (Promise<{date,tvl,truncated?}>) while every actual return path (the success-path Array.from(...).map(...) and the catch-all []) is an array — the annotation was just missing its []. TvlChart.tsx's setData(history) call surfaced this as a real type error. soroban.ts also uses the Account type (Promise, Map) in 4 places without importing it from @stellar/stellar-sdk. Found while investigating why PR #215's CI was failing - main itself does not currently pass 'next build' or 'tsc --noEmit', independent of that PR. A third, unrelated error remains after these two fixes: useLeaderboard.ts calls sorobanService.getLeaderboard(offset, limit, sortKey, search) with a 4th argument that method doesn't accept - the frontend search UI (debounce, state) is fully wired up but the backend method was never extended to filter by it. That's incomplete feature work, not a typo, so it's left for whoever owns that feature to finish rather than guessed at here. --- src/lib/soroban.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/soroban.ts b/src/lib/soroban.ts index 0b75857..f0e8378 100644 --- a/src/lib/soroban.ts +++ b/src/lib/soroban.ts @@ -4,6 +4,7 @@ */ import { + Account, Contract, TransactionBuilder, BASE_FEE, @@ -1484,7 +1485,7 @@ export class SorobanService { async getPoolHistory( poolId: string, days: number = 7, - ): Promise<{ date: string; tvl: string; truncated?: boolean }> { + ): Promise<{ date: string; tvl: string; truncated?: boolean }[]> { try { const latest = await this.rpcServer.getLatestLedger(); // ~5 s per ledger; days * 86400 / 5 From cf7b6353bc5052509c831737202d5cd5619248bf Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Tue, 25 Aug 2026 15:46:31 -0500 Subject: [PATCH 2/2] Add the missing search parameter to getLeaderboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useLeaderboard.ts already called sorobanService.getLeaderboard() with a 4th `search` argument, but the method only accepted 3 params — this was issue #3 flagged (and deliberately left unfixed) in this PR's original description, and it's what's failing Netlify's preview build/deploy for this PR: `next build` type-checks the whole app, and the extra argument is a TS2345 error regardless of the two fixes already in this branch. - API path (fetchLeaderboardFromApi): forwards `search` as a `search` query param, same pattern as offset/limit/sort. - Event-scan fallback (fetchLeaderboardFromEvents): filters the aggregated rows by a case-insensitive substring match on address, applied before pagination so `total` reflects the filtered count. --- src/lib/soroban.service.test.ts | 77 +++++++++++++++++++++++++++++++++ src/lib/soroban.ts | 11 +++-- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/lib/soroban.service.test.ts b/src/lib/soroban.service.test.ts index 3e72adb..8d8b547 100644 --- a/src/lib/soroban.service.test.ts +++ b/src/lib/soroban.service.test.ts @@ -1536,6 +1536,56 @@ describe("SorobanService leaderboard", () => { }); }); + it("getLeaderboard filters event-derived rows by a case-insensitive address search", async () => { + const otherUser = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 8)); + const { service, rpcServer } = makeService({ pool: false }); + vi.spyOn(service, "getFactoryPools").mockResolvedValue([ + { + id: "factory-pool", + contractAddress: POOL_CONTRACT_ID, + asset: { code: "XLM", isNative: true }, + dailyRate: "0", + minLockPeriod: 0, + totalLocked: "0", + totalUsers: 0, + isActive: true, + createdAt: 1, + }, + ]); + rpcServer.getLatestLedger.mockResolvedValue({ sequence: 200_000 }); + rpcServer.getEvents.mockResolvedValue({ + events: [ + makeContractEvent({ + action: "update_credits", + address: USER_PUBLIC_KEY, + value: { credits: 120 }, + }), + makeContractEvent({ + action: "update_credits", + address: otherUser, + value: 50, + }), + ], + }); + + const needle = USER_PUBLIC_KEY.slice(4, 14).toLowerCase(); + await expect(service.getLeaderboard(0, 10, "credits", needle)).resolves.toEqual({ + entries: [ + { + address: USER_PUBLIC_KEY, + totalCredits: 120, + totalStake: 0, + boostUtilization: null, + }, + ], + total: 1, + }); + + await expect( + service.getLeaderboard(0, 10, "credits", "not-a-real-address"), + ).resolves.toEqual({ entries: [], total: 0 }); + }); + it("fetchLeaderboardFromEvents returns an empty page without pool IDs and on RPC errors", async () => { const warnSpy = vi .spyOn(console, "warn") @@ -1629,6 +1679,33 @@ describe("SorobanService leaderboard", () => { ); }); + it("getLeaderboard forwards a search term as a query param to the API", async () => { + const previousApi = process.env.NEXT_PUBLIC_LEADERBOARD_API_URL; + process.env.NEXT_PUBLIC_LEADERBOARD_API_URL = + "https://leaderboard.example/rankings"; + vi.resetModules(); + const { SorobanService: ApiSorobanService } = await import("./soroban"); + const service = new ApiSorobanService(); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ entries: [], total: 0 }), + } as Response); + + try { + await service.getLeaderboard(0, 10, "credits", "GABC123"); + } finally { + if (previousApi === undefined) + delete process.env.NEXT_PUBLIC_LEADERBOARD_API_URL; + else process.env.NEXT_PUBLIC_LEADERBOARD_API_URL = previousApi; + vi.resetModules(); + } + + expect(fetchSpy).toHaveBeenCalledWith( + "https://leaderboard.example/rankings?offset=0&limit=10&sort=credits&search=GABC123", + { headers: { accept: "application/json" } }, + ); + }); + it("getLeaderboard falls back to event scanning when the API responds non-OK", async () => { const previousApi = process.env.NEXT_PUBLIC_LEADERBOARD_API_URL; process.env.NEXT_PUBLIC_LEADERBOARD_API_URL = diff --git a/src/lib/soroban.ts b/src/lib/soroban.ts index f0e8378..f20cd22 100644 --- a/src/lib/soroban.ts +++ b/src/lib/soroban.ts @@ -1628,26 +1628,29 @@ export class SorobanService { offset: number, limit: number, sortKey: LeaderboardSortKey = 'credits', + search?: string, ): Promise { if (LEADERBOARD_API_URL) { try { - return await this.fetchLeaderboardFromApi(offset, limit, sortKey); + return await this.fetchLeaderboardFromApi(offset, limit, sortKey, search); } catch (err) { console.warn('[SmartDrop] leaderboard API failed, falling back to event scan:', err); } } - return this.fetchLeaderboardFromEvents(offset, limit, sortKey); + return this.fetchLeaderboardFromEvents(offset, limit, sortKey, search); } private async fetchLeaderboardFromApi( offset: number, limit: number, sortKey: LeaderboardSortKey, + search?: string, ): Promise { const url = new URL(LEADERBOARD_API_URL); url.searchParams.set('offset', String(offset)); url.searchParams.set('limit', String(limit)); url.searchParams.set('sort', sortKey); + if (search) url.searchParams.set('search', search); const res = await fetch(url.toString(), { headers: { accept: 'application/json' } }); if (!res.ok) throw new Error(`Leaderboard API responded ${res.status}`); @@ -1669,6 +1672,7 @@ export class SorobanService { offset: number, limit: number, sortKey: LeaderboardSortKey, + search?: string, ): Promise { const poolIds = await this.getLeaderboardPoolIds(); if (poolIds.length === 0) return { entries: [], total: 0 }; @@ -1736,7 +1740,8 @@ export class SorobanService { totalStake: Math.round(stake), boostUtilization: null, })) - .filter((e) => e.totalStake > 0 || e.totalCredits > 0); + .filter((e) => e.totalStake > 0 || e.totalCredits > 0) + .filter((e) => !search || e.address.toLowerCase().includes(search.toLowerCase())); all.sort((a, b) => sortKey === 'credits'