transactions-utils.tsx raw

   1  import { toast } from "sonner";
   2  import { ListTransactionsResponse, Transaction } from "src/types";
   3  import { request } from "src/utils/request";
   4  
   5  const LABEL_COLUMN_PREFIX = "label_";
   6  
   7  // Use a large page size to keep the number of round-trips low when
   8  // exporting wallets that have thousands of transactions.
   9  const EXPORT_TRANSACTIONS_PAGE_SIZE = 1000;
  10  
  11  // based on https://stackoverflow.com/a/68146412
  12  const escapeCsvCell = (raw: string) => {
  13    const safe = /^[\t\r ]*[=+\-@]/.test(raw) ? `'${raw}` : raw;
  14    return `"${safe.replaceAll('"', '""')}"`;
  15  };
  16  
  17  export const convertToCSV = (transactions: Transaction[]) => {
  18    if (!transactions.length) {
  19      return "";
  20    }
  21  
  22    // Get headers from all transactions including the user_labels in metadata
  23    const headers = Object.keys(transactions[0]);
  24    const userLabelKeys = Array.from(
  25      new Set(
  26        transactions.flatMap((tx) =>
  27          tx.metadata?.user_labels ? Object.keys(tx.metadata.user_labels) : []
  28        )
  29      )
  30    ).sort();
  31    const userLabelHeaders = userLabelKeys.map(
  32      (key) => `${LABEL_COLUMN_PREFIX}${key}`
  33    );
  34  
  35    const csvHeaders = [...headers, ...userLabelHeaders].join(",");
  36  
  37    // Convert each transaction to CSV row
  38    const csvRows = transactions.map((tx) => {
  39      return [
  40        ...headers.map((header) => {
  41          const value = tx[header as keyof typeof tx];
  42          if (value === undefined || value === null) {
  43            return "";
  44          }
  45          const stringValue =
  46            typeof value === "object" ? JSON.stringify(value) : String(value);
  47          return escapeCsvCell(stringValue);
  48        }),
  49        ...userLabelKeys.map((key) => {
  50          const value = tx.metadata?.user_labels?.[key];
  51          if (value === undefined || value === null) {
  52            return "";
  53          }
  54          return escapeCsvCell(value);
  55        }),
  56      ].join(",");
  57    });
  58  
  59    return [csvHeaders, ...csvRows].join("\n");
  60  };
  61  
  62  export const handleExportTransactions = async (appId?: number) => {
  63    const toastId = toast.loading("Exporting transactions…");
  64    try {
  65      // Fetch all transactions by paginating through all pages
  66      let allTransactions: Transaction[] = [];
  67      let offset = 0;
  68  
  69      while (true) {
  70        let url = `/api/transactions?limit=${EXPORT_TRANSACTIONS_PAGE_SIZE}&offset=${offset}`;
  71        if (appId) {
  72          url += `&appId=${appId}`;
  73        }
  74  
  75        const data = await request<ListTransactionsResponse>(url);
  76  
  77        if (!data) {
  78          throw new Error("no list transactions response");
  79        }
  80  
  81        allTransactions = [...allTransactions, ...data.transactions];
  82        toast.loading(
  83          `Exporting ${allTransactions.length.toLocaleString()} transactions…`,
  84          { id: toastId }
  85        );
  86  
  87        if (data.transactions.length < EXPORT_TRANSACTIONS_PAGE_SIZE) {
  88          break;
  89        }
  90        offset += EXPORT_TRANSACTIONS_PAGE_SIZE;
  91      }
  92  
  93      // Convert to CSV and create download
  94      const csvString = convertToCSV(allTransactions);
  95      const blob = new Blob([csvString], { type: "text/csv" });
  96      const url = window.URL.createObjectURL(blob);
  97      const link = document.createElement("a");
  98      link.href = url;
  99      const filename = appId
 100        ? `transactions_app_${appId}.csv`
 101        : `transactions_all.csv`;
 102      link.download = filename;
 103      document.body.appendChild(link);
 104      link.click();
 105      document.body.removeChild(link);
 106      window.URL.revokeObjectURL(url);
 107      toast.success("Transactions saved to your downloads folder", {
 108        id: toastId,
 109      });
 110    } catch (error) {
 111      console.error("Error downloading transactions:", error);
 112      toast.error("Failed to export transactions", { id: toastId });
 113    }
 114  };
 115