From 2ec5420456f6434765885a22282d9d8fd46de64e Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Thu, 26 Mar 2026 22:58:10 +0530 Subject: [PATCH 1/7] fix(pdf): render dynamic native token and network fee symbol --- frontend/src/utils/generateInvoicePDF.js | 58 +++++++++-------- frontend/src/utils/invoicePaymentSymbols.js | 70 +++++++++++++++++++++ 2 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 frontend/src/utils/invoicePaymentSymbols.js diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 52a0a14d..22ab4f0c 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -1,6 +1,10 @@ import jsPDF from "jspdf"; -import { ethers } from "ethers"; import { getWagmiChainName, getWagmiChainInfo } from "./wagmiChainHelpers"; +import { + buildInvoiceTotalText, + formatNetworkFeeValue, + resolveInvoicePaymentContext, +} from "./invoicePaymentSymbols"; /** * Load logo image with multiple fallback methods @@ -340,8 +344,17 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); - const tokenName = invoice.paymentToken?.name || "Ether"; - const tokenSymbol = invoice.paymentToken?.symbol || "ETH"; + const chainId = invoice.paymentToken?.chainId || invoice.chainId; + const network = getWagmiChainInfo(chainId); + const chainName = network?.name || getWagmiChainName(chainId) || "Unknown network"; + const { + nativeSymbol, + nativeDecimals, + isNativePayment, + tokenName, + tokenSymbol, + tokenDecimals, + } = resolveInvoicePaymentContext(invoice, network); pdf.text(`${tokenName} (${tokenSymbol})`, 25, yPos + 14); pdf.setFontSize(8); @@ -350,11 +363,8 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { const contractAddr = invoice.paymentToken.address; const shortAddr = `${contractAddr.substring(0, 10)}......${contractAddr.substring(contractAddr.length - 8)}`; pdf.text(shortAddr, 25, yPos + 19); - const chainId = invoice.paymentToken?.chainId || invoice.chainId; - const network = getWagmiChainInfo(chainId); - const chainName = network?.name || getWagmiChainName(chainId) || "Unknown network"; pdf.text( - `Decimals: ${invoice.paymentToken.decimals || 18} | Chain: ${chainName}`, + `Decimals: ${tokenDecimals} | Chain: ${chainName}`, 120, yPos + 14 ); @@ -523,8 +533,8 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { pdf.setFont("helvetica", "normal"); pdf.text("Network Fee:", 25, yPos + 13); pdf.setFont("helvetica", "bold"); - const networkFee = ethers.formatUnits(fee); - pdf.text(`${networkFee} ETH`, 185, yPos + 13, { align: "right" }); + const networkFee = formatNetworkFeeValue(fee, nativeDecimals); + pdf.text(`${networkFee} ${nativeSymbol}`, 185, yPos + 13, { align: "right" }); pdf.setDrawColor(...mediumGray); pdf.setLineWidth(0.5); @@ -540,27 +550,15 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { pdf.setTextColor(...darkGray); pdf.text("TOTAL AMOUNT:", 25, yPos + 25); - - let totalText; - if (tokenSymbol === "ETH") { - // Use ethers.BigNumber for precise addition in wei - let amountDueWei, networkFeeWei; - try { - amountDueWei = ethers.parseUnits(invoice.amountDue || "0", 18); - } catch { - amountDueWei = 0n; - } - try { - networkFeeWei = ethers.parseUnits(networkFee || "0", 18); - } catch { - networkFeeWei = 0n; - } - const totalWei = amountDueWei + networkFeeWei; - const totalEth = ethers.formatUnits(totalWei, 18); - totalText = `${Number(totalEth).toFixed(6)} ETH`; - } else { - totalText = `${invoice.amountDue} ${tokenSymbol} + ${networkFee} ETH`; - } + const totalText = buildInvoiceTotalText({ + isNativePayment, + amountDue: invoice.amountDue, + tokenSymbol, + tokenDecimals, + fee, + networkFee, + nativeSymbol, + }); pdf.setFontSize(11); pdf.text(totalText, 185, yPos + 25, { align: "right" }); diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js new file mode 100644 index 00000000..edb9a3da --- /dev/null +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -0,0 +1,70 @@ +import { ethers } from "ethers"; + +export const resolveInvoicePaymentContext = (invoice, network) => { + // If no payment token is set, it's a native currency payment + const isNativePayment = !invoice.paymentToken || !invoice.paymentToken.address; + + // Use Wagmi network info to get correct native token symbol, name, and decimals + // wagmi chain object typically has nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 } + const nativeSymbol = network?.nativeCurrency?.symbol || "ETH"; + const nativeDecimals = network?.nativeCurrency?.decimals || 18; + const nativeName = network?.nativeCurrency?.name || "Ether"; + + // Use provided token info, or default to the native chain info + const tokenName = invoice.paymentToken?.name || nativeName; + const tokenSymbol = invoice.paymentToken?.symbol || nativeSymbol; + const tokenDecimals = invoice.paymentToken?.decimals || nativeDecimals; + + return { + nativeSymbol, + nativeDecimals, + isNativePayment, + tokenName, + tokenSymbol, + tokenDecimals, + }; +}; + +export const formatNetworkFeeValue = (fee, nativeDecimals) => { + try { + return ethers.formatUnits(fee || "0", nativeDecimals || 18); + } catch { + return "0.0"; + } +}; + +export const buildInvoiceTotalText = ({ + isNativePayment, + amountDue, + tokenSymbol, + tokenDecimals, + fee, + networkFee, + nativeSymbol, +}) => { + // If payment is in native currency, sum up the amount due and network fee safely using BigInt + if (isNativePayment) { + let amountDueWei, networkFeeWei; + + try { + amountDueWei = ethers.parseUnits(amountDue || "0", tokenDecimals || 18); + } catch { + amountDueWei = 0n; + } + + try { + networkFeeWei = BigInt(fee || "0"); + } catch { + networkFeeWei = 0n; + } + + const totalWei = amountDueWei + networkFeeWei; + const totalAmount = ethers.formatUnits(totalWei, tokenDecimals || 18); + + // Using .toFixed(6) to prevent extremely long decimals + return `${Number(totalAmount).toFixed(6)} ${nativeSymbol}`; + } + + // If ERC20 payment, show token amount + network fee in native token + return `${amountDue || "0"} ${tokenSymbol} + ${networkFee} ${nativeSymbol}`; +}; From 2a93697b9901bd94f64d4ce26a9cf73d50f342a9 Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Thu, 26 Mar 2026 23:35:25 +0530 Subject: [PATCH 2/7] refactor(pdf): implement string-based decimal truncation for huge BigInts --- frontend/src/utils/generateInvoicePDF.js | 1 + frontend/src/utils/invoicePaymentSymbols.js | 23 ++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 22ab4f0c..6304bbe3 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -558,6 +558,7 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { fee, networkFee, nativeSymbol, + nativeDecimals, }); pdf.setFontSize(11); diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js index edb9a3da..6d3b79c0 100644 --- a/frontend/src/utils/invoicePaymentSymbols.js +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -6,14 +6,14 @@ export const resolveInvoicePaymentContext = (invoice, network) => { // Use Wagmi network info to get correct native token symbol, name, and decimals // wagmi chain object typically has nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 } - const nativeSymbol = network?.nativeCurrency?.symbol || "ETH"; - const nativeDecimals = network?.nativeCurrency?.decimals || 18; - const nativeName = network?.nativeCurrency?.name || "Ether"; + const nativeSymbol = network?.nativeCurrency?.symbol ?? "ETH"; + const nativeDecimals = network?.nativeCurrency?.decimals ?? 18; + const nativeName = network?.nativeCurrency?.name ?? "Ether"; // Use provided token info, or default to the native chain info - const tokenName = invoice.paymentToken?.name || nativeName; - const tokenSymbol = invoice.paymentToken?.symbol || nativeSymbol; - const tokenDecimals = invoice.paymentToken?.decimals || nativeDecimals; + const tokenName = isNativePayment ? nativeName : (invoice.paymentToken?.name ?? nativeName); + const tokenSymbol = isNativePayment ? nativeSymbol : (invoice.paymentToken?.symbol ?? nativeSymbol); + const tokenDecimals = isNativePayment ? nativeDecimals : (invoice.paymentToken?.decimals ?? nativeDecimals); return { nativeSymbol, @@ -41,13 +41,14 @@ export const buildInvoiceTotalText = ({ fee, networkFee, nativeSymbol, + nativeDecimals, }) => { // If payment is in native currency, sum up the amount due and network fee safely using BigInt if (isNativePayment) { let amountDueWei, networkFeeWei; try { - amountDueWei = ethers.parseUnits(amountDue || "0", tokenDecimals || 18); + amountDueWei = ethers.parseUnits(amountDue || "0", nativeDecimals ?? 18); } catch { amountDueWei = 0n; } @@ -59,10 +60,12 @@ export const buildInvoiceTotalText = ({ } const totalWei = amountDueWei + networkFeeWei; - const totalAmount = ethers.formatUnits(totalWei, tokenDecimals || 18); + const totalAmount = ethers.formatUnits(totalWei, nativeDecimals ?? 18); - // Using .toFixed(6) to prevent extremely long decimals - return `${Number(totalAmount).toFixed(6)} ${nativeSymbol}`; + // Truncate to 6 decimal places without Number conversion + const [intPart, decPart = ""] = totalAmount.split("."); + const truncatedDec = decPart.slice(0, 6).padEnd(6, "0"); + return `${intPart}.${truncatedDec} ${nativeSymbol}`; } // If ERC20 payment, show token amount + network fee in native token From 05bd589e4c6336ef275b87b5baacbdac17607fad Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Fri, 27 Mar 2026 16:06:57 +0530 Subject: [PATCH 3/7] Fix native payment detection and chain metadata handling --- frontend/src/utils/generateInvoicePDF.js | 2 +- frontend/src/utils/invoicePaymentSymbols.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 6304bbe3..2ba7aa6b 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -359,7 +359,7 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { pdf.setFontSize(8); pdf.setTextColor(...mediumGray); - if (invoice.paymentToken?.address) { + if (!isNativePayment && invoice.paymentToken?.address) { const contractAddr = invoice.paymentToken.address; const shortAddr = `${contractAddr.substring(0, 10)}......${contractAddr.substring(contractAddr.length - 8)}`; pdf.text(shortAddr, 25, yPos + 19); diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js index 6d3b79c0..35976a44 100644 --- a/frontend/src/utils/invoicePaymentSymbols.js +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -1,14 +1,20 @@ import { ethers } from "ethers"; export const resolveInvoicePaymentContext = (invoice, network) => { - // If no payment token is set, it's a native currency payment - const isNativePayment = !invoice.paymentToken || !invoice.paymentToken.address; + const tokenAddress = invoice.paymentToken?.address; + const isNativePayment = !tokenAddress || tokenAddress === ethers.ZeroAddress; + + if (!network?.nativeCurrency) { + throw new Error("Missing native currency metadata for invoice payment context"); + } // Use Wagmi network info to get correct native token symbol, name, and decimals // wagmi chain object typically has nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 } - const nativeSymbol = network?.nativeCurrency?.symbol ?? "ETH"; - const nativeDecimals = network?.nativeCurrency?.decimals ?? 18; - const nativeName = network?.nativeCurrency?.name ?? "Ether"; + const { + symbol: nativeSymbol, + decimals: nativeDecimals, + name: nativeName, + } = network.nativeCurrency; // Use provided token info, or default to the native chain info const tokenName = isNativePayment ? nativeName : (invoice.paymentToken?.name ?? nativeName); From e2909cac916e8cdbbd0b99f9ba572588bcca2f9f Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Fri, 27 Mar 2026 16:17:57 +0530 Subject: [PATCH 4/7] Handle unsupported chain metadata in PDF generation --- frontend/src/utils/generateInvoicePDF.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 2ba7aa6b..1794c8fb 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -347,6 +347,14 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { const chainId = invoice.paymentToken?.chainId || invoice.chainId; const network = getWagmiChainInfo(chainId); const chainName = network?.name || getWagmiChainName(chainId) || "Unknown network"; + let paymentContext; + try { + paymentContext = resolveInvoicePaymentContext(invoice, network); + } catch (err) { + throw new Error( + `Cannot generate PDF: unsupported chain (ID: ${chainId}). ${err.message}` + ); + } const { nativeSymbol, nativeDecimals, @@ -354,7 +362,7 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { tokenName, tokenSymbol, tokenDecimals, - } = resolveInvoicePaymentContext(invoice, network); + } = paymentContext; pdf.text(`${tokenName} (${tokenSymbol})`, 25, yPos + 14); pdf.setFontSize(8); From 1e82a2907fc582f823ae3af75993c5c649bc23f0 Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Fri, 27 Mar 2026 16:29:03 +0530 Subject: [PATCH 5/7] Harden PDF token metadata validation and total precision --- frontend/src/utils/generateInvoicePDF.js | 8 ++++++++ frontend/src/utils/invoicePaymentSymbols.js | 7 ++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index 1794c8fb..d7929e95 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -363,6 +363,14 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { tokenSymbol, tokenDecimals, } = paymentContext; + if ( + !isNativePayment && + (!invoice.paymentToken?.symbol || invoice.paymentToken?.decimals == null) + ) { + throw new Error( + `Cannot generate PDF: missing ERC-20 token metadata for ${invoice.paymentToken?.address || "unknown token"} on chain ${chainId}.` + ); + } pdf.text(`${tokenName} (${tokenSymbol})`, 25, yPos + 14); pdf.setFontSize(8); diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js index 35976a44..3db8fc65 100644 --- a/frontend/src/utils/invoicePaymentSymbols.js +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -67,11 +67,8 @@ export const buildInvoiceTotalText = ({ const totalWei = amountDueWei + networkFeeWei; const totalAmount = ethers.formatUnits(totalWei, nativeDecimals ?? 18); - - // Truncate to 6 decimal places without Number conversion - const [intPart, decPart = ""] = totalAmount.split("."); - const truncatedDec = decPart.slice(0, 6).padEnd(6, "0"); - return `${intPart}.${truncatedDec} ${nativeSymbol}`; + + return `${totalAmount} ${nativeSymbol}`; } // If ERC20 payment, show token amount + network fee in native token From e80d04a0305bf4709380cdb823476f262497bf54 Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Fri, 27 Mar 2026 16:55:42 +0530 Subject: [PATCH 6/7] Remove unused tokenDecimals from PDF total builder --- frontend/src/utils/generateInvoicePDF.js | 1 - frontend/src/utils/invoicePaymentSymbols.js | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index d7929e95..b4197936 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -570,7 +570,6 @@ export const generateInvoicePDF = async (invoice, fee = 0) => { isNativePayment, amountDue: invoice.amountDue, tokenSymbol, - tokenDecimals, fee, networkFee, nativeSymbol, diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js index 3db8fc65..3e92fdc5 100644 --- a/frontend/src/utils/invoicePaymentSymbols.js +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -43,7 +43,6 @@ export const buildInvoiceTotalText = ({ isNativePayment, amountDue, tokenSymbol, - tokenDecimals, fee, networkFee, nativeSymbol, From 92164f3ca7f3035ed407b9da697d6b79c2ec6eb6 Mon Sep 17 00:00:00 2001 From: Atharva Naik Date: Fri, 27 Mar 2026 18:57:35 +0530 Subject: [PATCH 7/7] Use nullish coalescing for nativeDecimals in formatNetworkFeeValue --- frontend/src/utils/invoicePaymentSymbols.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/invoicePaymentSymbols.js b/frontend/src/utils/invoicePaymentSymbols.js index 3e92fdc5..94a54480 100644 --- a/frontend/src/utils/invoicePaymentSymbols.js +++ b/frontend/src/utils/invoicePaymentSymbols.js @@ -33,7 +33,7 @@ export const resolveInvoicePaymentContext = (invoice, network) => { export const formatNetworkFeeValue = (fee, nativeDecimals) => { try { - return ethers.formatUnits(fee || "0", nativeDecimals || 18); + return ethers.formatUnits(fee || "0", nativeDecimals ?? 18); } catch { return "0.0"; }