Skip to content

feat: Download receipt as csv#880

Open
C0mberry wants to merge 22 commits intosolana-foundation:masterfrom
hoodieshq:development-download-receipt-as-csv
Open

feat: Download receipt as csv#880
C0mberry wants to merge 22 commits intosolana-foundation:masterfrom
hoodieshq:development-download-receipt-as-csv

Conversation

@C0mberry
Copy link
Contributor

@C0mberry C0mberry commented Mar 16, 2026

Description

  • adding ability to download receipt as csv

Type of change

  • New feature

Screenshots

Screenshot 2026-03-16 at 18 36 56

solana-receipt.csv

Testing

  1. open http://localhost:3000/tx/4izwTCUeRGAMGReXeXDumiBzAgXPGz6KzccacCf1WU5YXCCpSDjAQT7J6D6dY45bL1NW9AiqwCuWEnz3hbGtZS2y?view=receipt&cluster=mainnet-beta
  2. click on download > csv btn
  3. see the csv

Related Issues

HOO-327/

Checklist

  • My code follows the project's style guidelines
  • I have added tests that prove my fix/feature works
  • All tests pass locally and in CI
  • I have run build:info script to update build information
  • CI/CD checks pass
  • I have included screenshots for protocol screens (if applicable)

@vercel
Copy link

vercel bot commented Mar 16, 2026

@C0mberry is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@rogaldh
Copy link
Contributor

rogaldh commented Mar 21, 2026

Let's consider adding tests for CSV into the receipt.e2e

@C0mberry C0mberry force-pushed the development-download-receipt-as-csv branch from 4daccce to 07cdfb5 Compare March 23, 2026 22:00
@C0mberry C0mberry marked this pull request as ready for review March 23, 2026 22:10
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR adds CSV export for Solana receipts, complementing the existing PDF download. A new generateReceiptCsv utility builds an 11-column row from FormattedReceipt, applies formula-injection sanitisation on the memo field, and triggers a browser download via the standard blob-URL pattern. The feature is wired into ReceiptContent with a useCallback consistent with the downloadPdf pattern, and surfaced in ReceiptView's popover as a sibling CSV button alongside PDF.

Key observations:

  • Formula injection for memo is properly guarded by sanitizeCsvField, addressing the concern raised in a previous review thread.
  • The deleted .test.ts files (normalize-search-params, use-primary-domain) are straightforward cleanup — .spec.ts equivalents already existed and continue to provide full coverage.
  • fast-csv (the meta-package) is used solely for writeToString, pulling in @fast-csv/parse and several lodash sub-packages that are never exercised in a format-only use case. Depending directly on @fast-csv/format would avoid the unused parse bundle.
  • The formula-injection sanitisation added to sanitizeCsvField has no dedicated test case verifying the guard fires for memos starting with =, +, -, or @.

Confidence Score: 4/5

  • PR is nearly ready to merge — the core feature is solid and prior security concern is addressed; two minor P2 items remain.
  • The CSV generation logic is correct, the formula-injection guard for memo is in place, the E2E and unit tests cover the happy path well, and the integration into the existing download UX is consistent. Two non-blocking P2 items remain: the fast-csv meta-package pulls in an unused parse bundle that could be trimmed, and the sanitizeCsvField guard itself lacks a targeted test case. Neither blocks shipping.
  • app/features/receipt/lib/generate-receipt-csv.ts — dependency choice (fast-csv vs @fast-csv/format) and missing sanitization test

Important Files Changed

Filename Overview
app/features/receipt/lib/generate-receipt-csv.ts New module that builds a CSV row from a FormattedReceipt and triggers a browser download. Sanitisation for formula injection is applied to memo. Minor concern: pulls in the full fast-csv meta-package (including unused parse module) when only @fast-csv/format is needed.
app/features/receipt/lib/tests/generate-receipt-csv.spec.ts Well-structured unit tests covering column ordering, optional fields, and download mechanics. Missing a test case for the formula-injection sanitisation path in sanitizeCsvField.
app/features/receipt/receipt-page.tsx Adds downloadCsv callback via useCallback, consistent with the existing downloadPdf pattern. No issues.
app/features/receipt/ui/ReceiptView.tsx Adds downloadCsv prop to interface and renders a new DownloadReceiptItem for CSV ahead of PDF. Clean, consistent change.
app/features/receipt/e2e/receipt.e2e.ts Three new E2E tests cover the download menu visibility, correct filename pattern, and the "Downloaded!" state after CSV download. All guard correctly against missing receipt before asserting.

Sequence Diagram

sequenceDiagram
    participant User
    participant ReceiptView
    participant ReceiptContent (receipt-page.tsx)
    participant generateReceiptCsv
    participant fast-csv
    participant Browser

    User->>ReceiptView: clicks "Download" popover
    ReceiptView-->>User: shows CSV + PDF buttons
    User->>ReceiptView: clicks "CSV"
    ReceiptView->>ReceiptContent (receipt-page.tsx): downloadCsv()
    ReceiptContent (receipt-page.tsx)->>generateReceiptCsv: generateReceiptCsv(receipt, signature, usdValue)
    generateReceiptCsv->>generateReceiptCsv: buildReceiptCsvRow() → sanitizeCsvField(memo)
    generateReceiptCsv->>fast-csv: writeToString([row], { headers })
    fast-csv-->>generateReceiptCsv: CSV string
    generateReceiptCsv->>Browser: new Blob → createObjectURL → <a>.click()
    Browser-->>User: file download (solana-receipt-{sig}.csv)
    generateReceiptCsv->>Browser: revokeObjectURL(url)
Loading

Reviews (2): Last reviewed commit: "added sanitizeCsvField" | Re-trigger Greptile

@C0mberry
Copy link
Contributor Author

@greptile-apps check again

package.json Outdated
"codama": "1.2.11",
"cross-fetch": "3.2.0",
"cross-spawn": "7.0.6",
"fast-csv": "^5.0.5",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Please use exact version of the package to align with the current project practices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

test('shows CSV and PDF options in download menu', async ({ page }) => {
await waitForPage(page, VALID_TX, 'receipt');

await page
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The duplicated part is pretty big and complex, some helper like navigateToReceipt would improve readability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

app/styles.css Outdated
}
}

@media print {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What are these styles for? I tried printing http://localhost:3000/tx/2toZ2zRM4DfTyi6G4bSv9M9YfNMs4NQvnhyL4vmTMK45kf77FCPRR354Safnzr99HfoDae1PPYigKpCX1kwqejkS?view=receipt, but it doesn't seem to be printable anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

export function buildReceiptCsvRow(receipt: FormattedReceipt, signature: string, usdValue?: string): string[] {
const mint = 'mint' in receipt ? receipt.mint : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: In all other cases this is like "const mint = 'mint' in receipt ? receipt.mint : undefined;", not a problem but consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

usdValue?: string,
): Promise<void> {
const row = buildReceiptCsvRow(receipt, signature, usdValue);
const csv = await writeToString([row], { headers: [...CSV_HEADERS] });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I'm not saying it's wrong, but just a heads up: this function does a lot under the hood to handle large CSVs, while we only need to write one line. So the code below is all we require (just an example; no need to use it).

function toCsv(headers: readonly string[], row: string[]): string {
    const escape = (v: string) =>                                                                                                                                                                                                 
        v.includes(',') || v.includes('"') || v.includes('\n')                                                                                                                                                                    
            ? `"${v.replace(/"/g, '""')}"`                    
            : v;                                                                                                                                                                                                                  
    return [headers, row].map(r => r.map(escape).join(',')).join('\n');
}  

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we agreed to use fast-csv with @rogaldh

@C0mberry C0mberry requested a review from askov March 25, 2026 12:42
@rogaldh
Copy link
Contributor

rogaldh commented Mar 25, 2026

Could you check, please, that by sending the event to GA we can distinct PDF receipt from the CSV one

@vercel
Copy link

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
explorer Ready Ready Preview, Comment Mar 26, 2026 0:13am

Request Review

Copy link
Contributor

@rogaldh rogaldh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except these LGTM

margin: 0.5in;
size: A4;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This media block could be deleted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants