From 8c6a9327b35ac003899c30d0df4c1664b60d942f Mon Sep 17 00:00:00 2001 From: Ehonrie Date: Sun, 18 Oct 2026 18:50:56 +0100 Subject: [PATCH 1/4] fix: page through every wallet with the shared capped walker (#520) listAllWallets now uses fetchAllPages, which stops on a repeated cursor and caps the page count. Closes #520 Co-Authored-By: Claude Haiku 5.5 --- src/lib/wallets.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/lib/wallets.ts b/src/lib/wallets.ts index 284299b..b6837c4 100644 --- a/src/lib/wallets.ts +++ b/src/lib/wallets.ts @@ -5,6 +5,7 @@ import { apiFetch, path } from "./api"; import { parseAmount, toApiStroops } from "./amount"; import { pageQuery, type Paginated, type PageOpts } from "./pagination"; +import { fetchAllPages } from "./csv"; import { network } from "./network"; export { usdcForNetwork } from "./network"; @@ -153,15 +154,8 @@ export function listWalletsPage( } /** Fetch every wallet by walking all cursor pages. */ -export async function listAllWallets(token: string): Promise { - const all: WalletView[] = []; - let cursor: string | null = null; - do { - const page = await listWalletsPage(token, { before: cursor }); - all.push(...page.data); - cursor = page.next_cursor; - } while (cursor); - return all; +export function listAllWallets(token: string): Promise { + return fetchAllPages((before) => listWalletsPage(token, { before })); } export function getWallet(token: string, id: string, options: RequestOptions = {}) { From b2f52402b5ad6a6caff2a0b97791636b81e449f3 Mon Sep 17 00:00:00 2001 From: Ehonrie Date: Sun, 18 Oct 2026 18:50:58 +0100 Subject: [PATCH 2/4] docs: describe listAddresses and listTransactions as first-page helpers (#521) Closes #521 Co-Authored-By: Claude Haiku 5.5 --- src/lib/wallets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/wallets.ts b/src/lib/wallets.ts index b6837c4..9c8cd91 100644 --- a/src/lib/wallets.ts +++ b/src/lib/wallets.ts @@ -234,7 +234,7 @@ export function listAddressesPage( ); } -/** Lists all wallet addresses across cursor-paginated API responses. */ +/** Lists the first page of wallet addresses. Use `listAddressesPage` to page forward. */ export async function listAddresses( token: string, id: string, @@ -247,7 +247,7 @@ export async function listAddresses( return page.data; } -/** Lists all wallet transactions across cursor-paginated API responses. */ +/** Lists the first page of wallet transactions. Use `listTransactionsPage` to page forward. */ export async function listTransactions( token: string, id: string, From ec74c9f171a614915a095deaa8c5d967391ac450 Mon Sep 17 00:00:00 2001 From: Ehonrie Date: Sun, 18 Oct 2026 18:50:59 +0100 Subject: [PATCH 3/4] fix: export every address for customer refs and label XLM with its issuer (#522) Closes #522 Co-Authored-By: Claude Haiku 5.5 --- .../export/ExportTransactionsCsvButton.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/export/ExportTransactionsCsvButton.tsx b/src/components/export/ExportTransactionsCsvButton.tsx index 62c83a1..856bf0a 100644 --- a/src/components/export/ExportTransactionsCsvButton.tsx +++ b/src/components/export/ExportTransactionsCsvButton.tsx @@ -3,14 +3,20 @@ import { ExportCsvButton } from "./ExportCsvButton"; import { fetchAllPages, type CsvColumn } from "@/lib/csv"; import { formatStroops } from "@/lib/amount"; -import { listRecentAddresses, listTransactionsPage, type Transaction } from "@/lib/wallets"; +import { + displayAssetCode, + listAddressesPage, + listTransactionsPage, + type Transaction, +} from "@/lib/wallets"; type Row = Transaction & { customerRef: string | null }; const columns: CsvColumn[] = [ { header: "Date", value: (t) => t.created_at }, { header: "Direction", value: (t) => t.direction }, - { header: "Asset", value: (t) => t.asset_code }, + { header: "Asset", value: (t) => displayAssetCode(t.asset_code) }, + { header: "Asset issuer", value: (t) => t.asset_issuer }, { header: "Amount", value: (t) => formatStroops(t.amount_stroops) }, { header: "Status", value: (t) => t.status }, { header: "Hash", value: (t) => t.stellar_tx_hash }, @@ -33,8 +39,7 @@ export function ExportTransactionsCsvButton({ if (!token) throw new Error("Not signed in."); const [rows, addrs] = await Promise.all([ fetchAllPages((before) => listTransactionsPage(token, walletId, { before, limit: 200 })), - // Fetch up to 200 addresses for customer-ref enrichment; a failed lookup only blanks that column. - listRecentAddresses(token, walletId, 200).catch(() => []), + fetchAllPages((before) => listAddressesPage(token, walletId, { before, limit: 200 })), ]); const refs = new Map(); for (const a of addrs) if (a.customer_ref) refs.set(a.id, a.customer_ref); From 98858efc17c6c7bf46d9414b59196b30aa835978 Mon Sep 17 00:00:00 2001 From: Ehonrie Date: Sun, 18 Oct 2026 18:51:00 +0100 Subject: [PATCH 4/4] fix: share a download helper that revokes the object URL after the click (#523) Closes #523 Co-Authored-By: Claude Haiku 5.5 --- src/components/backup/DownloadBackupButton.tsx | 9 +++------ src/lib/csv.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/backup/DownloadBackupButton.tsx b/src/components/backup/DownloadBackupButton.tsx index 062a8c6..165d3f3 100644 --- a/src/components/backup/DownloadBackupButton.tsx +++ b/src/components/backup/DownloadBackupButton.tsx @@ -3,6 +3,7 @@ import { useRef, useState } from "react"; import { toast } from "sonner"; import { getBackup, loadLocalBackup, parseBackup, saveLocalBackup, decryptSeed, fromMnemonic } from "@/lib/sdk"; +import { downloadBlob } from "@/lib/csv"; /** Export the encrypted backup as a .json file, or re-import one into this browser. */ export function DownloadBackupButton({ @@ -29,14 +30,10 @@ export function DownloadBackupButton({ backup = parseBackup(remote.encrypted_backup); } const file = { wallet_id: walletId, address, backup }; - const url = URL.createObjectURL( + downloadBlob( + `octo-backup-${(address ?? walletId).slice(0, 8)}.json`, new Blob([JSON.stringify(file, null, 2)], { type: "application/json" }), ); - const a = document.createElement("a"); - a.href = url; - a.download = `octo-backup-${(address ?? walletId).slice(0, 8)}.json`; - a.click(); - URL.revokeObjectURL(url); } catch (err) { toast.error(err instanceof Error ? err.message : "Could not download the backup."); } finally { diff --git a/src/lib/csv.ts b/src/lib/csv.ts index d3bef4b..cba80ea 100644 --- a/src/lib/csv.ts +++ b/src/lib/csv.ts @@ -16,9 +16,8 @@ export function buildCsv(rows: T[], columns: CsvColumn[]): string { return [head, ...body].join("\r\n") + "\r\n"; } -/** Trigger a browser download of the CSV (with a BOM so Excel reads UTF-8). */ -export function downloadCsv(filename: string, csv: string): void { - const blob = new Blob(["", csv], { type: "text/csv;charset=utf-8" }); +/** Trigger a browser download. The URL is revoked later, since Safari can cancel a download whose URL is gone. */ +export function downloadBlob(filename: string, blob: Blob): void { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -26,7 +25,12 @@ export function downloadCsv(filename: string, csv: string): void { document.body.appendChild(a); a.click(); a.remove(); - URL.revokeObjectURL(url); + setTimeout(() => URL.revokeObjectURL(url), 1000); +} + +/** Trigger a browser download of the CSV (with a BOM so Excel reads UTF-8). */ +export function downloadCsv(filename: string, csv: string): void { + downloadBlob(filename, new Blob(["", csv], { type: "text/csv;charset=utf-8" })); } /** Walk every cursor page, capped so a misbehaving cursor cannot loop forever. */