diff --git a/package.json b/package.json index bfcb9b2c..016e1338 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "clean": "find . -path './node_modules' -prune -o -name 'dist' -type d -exec rm -rf '{}' \\; -printf '%p deleted\\n' 2>/dev/null", "deep-clean": "find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \\;", "build": "turbo run build", "build:form": "turbo run build --filter=@requestnetwork/create-invoice-form", diff --git a/packages/create-invoice-form/src/lib/create-invoice-form.svelte b/packages/create-invoice-form/src/lib/create-invoice-form.svelte index c656f73a..fcb6f26f 100644 --- a/packages/create-invoice-form/src/lib/create-invoice-form.svelte +++ b/packages/create-invoice-form/src/lib/create-invoice-form.svelte @@ -8,11 +8,16 @@ import type { IConfig } from "@requestnetwork/shared-types"; import { APP_STATUS } from "@requestnetwork/shared-types/enums"; import type { RequestNetwork } from "@requestnetwork/request-client.js"; + import { Types } from "@requestnetwork/request-client.js"; + import { CurrencyTypes } from "@requestnetwork/types"; // Utils import { getInitialFormData, prepareRequestParams } from "./utils"; import { config as defaultConfig } from "@requestnetwork/shared-utils/config"; import { calculateInvoiceTotals } from "@requestnetwork/shared-utils/invoiceTotals"; - import { initializeCurrencyManager } from "@requestnetwork/shared-utils/initCurrencyManager"; + import { + getCurrencySupportedNetworksForConversion, + initializeCurrencyManager, + } from "@requestnetwork/shared-utils/initCurrencyManager"; // Components import { InvoiceForm, InvoiceView } from "./invoice"; import Button from "@requestnetwork/shared-components/button.svelte"; @@ -34,26 +39,34 @@ const extractUniqueNetworkNames = (): string[] => { const networkSet = new Set(); - currencyManager.knownCurrencies.forEach((currency: any) => { - networkSet.add(currency.network); - }); + currencyManager.knownCurrencies.forEach( + (currency: CurrencyTypes.CurrencyDefinition) => { + if (currency.network) { + networkSet.add(currency.network); + } + } + ); return Array.from(networkSet); }; - let networks = extractUniqueNetworkNames(); + let networks: string[] = extractUniqueNetworkNames(); - let network = networks[0]; + let network: string | undefined = undefined; + let currency: CurrencyTypes.CurrencyDefinition | undefined = undefined; + let invoiceCurrency: CurrencyTypes.CurrencyDefinition | undefined = undefined; - const handleNetworkChange = (network: string) => { - if (network) { + const handleNetworkChange = (newNetwork: string) => { + if (newNetwork) { const newCurrencies = currencyManager.knownCurrencies.filter( - (currency: any) => currency.network === network + (currency: CurrencyTypes.CurrencyDefinition) => + currency.type === Types.RequestLogic.CURRENCY.ISO4217 || + currency.network === newNetwork ); - network = network; + network = newNetwork; defaultCurrencies = newCurrencies; - currency = newCurrencies[0]; + currency = undefined; } }; @@ -62,12 +75,33 @@ let appStatus: APP_STATUS[] = []; let formData = getInitialFormData(); let defaultCurrencies = currencyManager.knownCurrencies.filter( - (currency: any) => currency.network === network + (currency: CurrencyTypes.CurrencyDefinition) => + currency.type === Types.RequestLogic.CURRENCY.ISO4217 || network + ? currency.network === network + : true ); - let currency = defaultCurrencies[0]; + const handleInvoiceCurrencyChange = ( + value: CurrencyTypes.CurrencyDefinition + ) => { + invoiceCurrency = value; + network = undefined; + currency = undefined; + + if ( + invoiceCurrency && + invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217 + ) { + networks = getCurrencySupportedNetworksForConversion( + invoiceCurrency.hash, + currencyManager + ); + } else { + networks = extractUniqueNetworkNames(); + } + }; - const handleCurrencyChange = (value: string) => { + const handleCurrencyChange = (value: CurrencyTypes.CurrencyDefinition) => { currency = value; }; @@ -93,7 +127,14 @@ $: { const basicDetailsFilled = - formData.payeeAddress && formData.payerAddress && formData.dueDate; + formData.payeeAddress && + formData.payerAddress && + formData.dueDate && + formData.invoiceNumber && + formData.issuedOn && + invoiceCurrency && + currency && + formData.issuedOn; const hasItems = formData.invoiceItems.length > 0 && formData.invoiceItems.every(isValidItem); @@ -132,6 +173,7 @@ const requestCreateParameters = prepareRequestParams({ address: account?.address, formData, + invoiceCurrency, currency, invoiceTotals, }); @@ -172,13 +214,19 @@ bind:formData config={activeConfig} bind:defaultCurrencies + {handleInvoiceCurrencyChange} {handleCurrencyChange} {handleNetworkChange} {networks} + {currencyManager} + {invoiceCurrency} + {currency} + {network} />
Promise; export let invoiceTotals = { amountWithoutTax: 0, @@ -164,16 +166,15 @@

Payment Chain - {currency.network[0].toUpperCase() + currency.network.slice(1)} + {currency?.network ? currency.network.charAt(0).toUpperCase() + currency.network.slice(1).toLowerCase() : ""}

Invoice Currency - {currency.symbol} - ({currency.network}) + {invoiceCurrency ? invoiceCurrency.symbol: ""}

- Invoice Type - Regular Invoice + Settlement Currency + {currency ? `${currency.symbol} (${currency.network})` : ""}

@@ -221,7 +222,7 @@ > Due: {currency.symbol} + >{currency ? currency.symbol : ""} {" "} {invoiceTotals.totalAmount.toFixed(2)} diff --git a/packages/create-invoice-form/src/lib/invoice/form.svelte b/packages/create-invoice-form/src/lib/invoice/form.svelte index 11202080..96bb541b 100644 --- a/packages/create-invoice-form/src/lib/invoice/form.svelte +++ b/packages/create-invoice-form/src/lib/invoice/form.svelte @@ -17,16 +17,26 @@ import { calculateItemTotal } from "@requestnetwork/shared-utils/invoiceTotals"; import { checkAddress } from "@requestnetwork/shared-utils/checkEthAddress"; import { inputDateFormat } from "@requestnetwork/shared-utils/formatDate"; + import { Types } from "@requestnetwork/request-client.js"; + import { CurrencyTypes } from "@requestnetwork/types"; import isEmail from "validator/es/lib/isEmail"; export let config: IConfig; export const invoiceNumber: number = 1; export let formData: CustomFormData; + export let handleInvoiceCurrencyChange: (value: string) => void; export let handleCurrencyChange: (value: string) => void; export let handleNetworkChange: (chainId: string) => void; export let networks; export let defaultCurrencies: any = []; + export let currencyManager: any; + export let invoiceCurrency: CurrencyTypes.CurrencyDefinition | undefined; + export let currency: + | CurrencyTypes.ERC20Currency + | CurrencyTypes.NativeCurrency + | undefined; + export let network: any; let validationErrors = { payeeAddress: false, @@ -110,6 +120,21 @@ } }; + const filterSettlementCurrencies = ( + currency: CurrencyTypes.CurrencyDefinition + ) => { + return invoiceCurrency + ? invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217 + ? currency.type !== Types.RequestLogic.CURRENCY.ISO4217 && + currencyManager?.getConversionPath( + invoiceCurrency, + currency, + currency.network + )?.length > 0 + : invoiceCurrency.hash === currency.hash + : false; + }; + const addInvoiceItem = () => { const newItem = { name: "", @@ -333,25 +358,45 @@ - { - return { - value: network, - label: network[0].toUpperCase() + network.slice(1), - }; - })} - onchange={handleNetworkChange} - /> ({ value: currency, - label: `${currency.symbol} (${currency.network})`, + label: `${currency.symbol} ${currency?.network ? `(${currency?.network})` : ""}`, }))} + onchange={handleInvoiceCurrencyChange} + /> + networkItem) + .map((networkItem) => { + return { + value: networkItem, + label: networkItem[0]?.toUpperCase() + networkItem?.slice(1), + }; + })} + onchange={handleNetworkChange} + /> + filterSettlementCurrencies(currency)) + .map((currency) => ({ + value: currency, + label: `${currency.symbol ?? 'Unknown'} (${currency?.network ?? 'Unknown'})`, + }))} onchange={handleCurrencyChange} /> { + if ( + invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217 && + currency.type === Types.RequestLogic.CURRENCY.ETH + ) { + return { + id: Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + parameters: { + network: currency.network, + paymentAddress: getAddress(formData.payeeAddress), + feeAddress: zeroAddress, + feeAmount: "0", + }, + }; + } else if ( + invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217 && + currency.type === Types.RequestLogic.CURRENCY.ERC20 + ) { + return { + id: Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + parameters: { + network: currency.network, + paymentAddress: getAddress(formData.payeeAddress), + feeAddress: zeroAddress, + feeAmount: "0", + acceptedTokens: [getAddress(currency.address)], + }, + }; + } else if (currency.type === Types.RequestLogic.CURRENCY.ETH) { + return { + id: Types.Extension.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT, + parameters: { + paymentNetworkName: currency.network, + paymentAddress: getAddress(formData.payeeAddress), + feeAddress: zeroAddress, + feeAmount: "0", + }, + } + } else if (currency.type === Types.RequestLogic.CURRENCY.ERC20) { + return { + id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + parameters: { + paymentNetworkName: currency.network, + paymentAddress: getAddress(formData.payeeAddress), + feeAddress: zeroAddress, + feeAmount: "0", + }, + } + } else { + throw new Error("Unsupported payment network"); + } +}; + export const prepareRequestParams = ({ + invoiceCurrency, address, currency, formData, invoiceTotals, }: IRequestParams): Types.ICreateRequestParameters => { const isERC20 = currency.type === Types.RequestLogic.CURRENCY.ERC20; - + const isERC20InvoiceCurrency = invoiceCurrency.type === Types.RequestLogic.CURRENCY.ERC20; return { requestInfo: { currency: { - type: currency.type, - value: isERC20 ? currency.address : "eth", - network: currency.network, + type: invoiceCurrency.type, + value: isERC20InvoiceCurrency ? invoiceCurrency.address : invoiceCurrency.symbol, + network: invoiceCurrency.network, }, expectedAmount: parseUnits( invoiceTotals.totalAmount.toString(), - currency.decimals + invoiceCurrency.decimals ).toString(), payee: { type: Types.Identity.TYPE.ETHEREUM_ADDRESS, @@ -42,18 +98,8 @@ export const prepareRequestParams = ({ }, timestamp: Utils.getCurrentTimestampInSecond(), }, - paymentNetwork: { - id: - currency.type === Types.RequestLogic.CURRENCY.ETH - ? Types.Extension.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT - : Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, - parameters: { - paymentNetworkName: currency.network, - paymentAddress: getAddress(formData.payeeAddress), - feeAddress: zeroAddress, - feeAmount: "0", - }, - }, + paymentNetwork: getPaymentNetwork(invoiceCurrency, currency, formData), + paymentCurrency: isERC20 ? getAddress(currency.address) : currency.symbol, contentData: { meta: { format: "rnf_invoice", @@ -71,16 +117,20 @@ export const prepareRequestParams = ({ quantity: Number(item.quantity), unitPrice: parseUnits( item.unitPrice.toString(), - currency.decimals + invoiceCurrency.decimals ).toString(), discount: - item.discount && - parseUnits(item.discount.toString(), currency.decimals).toString(), + item.discount != null + ? parseUnits( + item.discount.toString(), + invoiceCurrency.decimals + ).toString() + : undefined, tax: { type: "percentage", amount: item.tax.amount.toString(), }, - currency: isERC20 ? currency.address : currency.symbol, + currency: isERC20InvoiceCurrency ? invoiceCurrency.address : invoiceCurrency.symbol, })), paymentTerms: { dueDate: new Date(formData.dueDate).toISOString(), diff --git a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte index dcc54fac..735aa603 100644 --- a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte +++ b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte @@ -3,13 +3,16 @@ import type { GetAccountReturnType } from "@wagmi/core"; import { approveErc20, + approveErc20ForProxyConversion, hasErc20Approval, + hasErc20ApprovalForProxyConversion, payRequest, } from "@requestnetwork/payment-processor"; import { Types, type RequestNetwork, } from "@requestnetwork/request-client.js"; + import { CurrencyTypes } from "@requestnetwork/types"; import { getPaymentNetworkExtension } from "@requestnetwork/payment-detection"; // Components import Accordion from "@requestnetwork/shared-components/accordion.svelte"; @@ -25,6 +28,7 @@ import { getCurrencyFromManager } from "@requestnetwork/shared-utils/getCurrency"; import { onMount } from "svelte"; import { formatUnits } from "viem"; + import { getConversionPaymentValues } from "../../utils/getConversionPaymentValues"; import { getEthersSigner } from "../../utils"; export let config; @@ -35,9 +39,11 @@ export let isRequestPayed: boolean; export let wagmiConfig: any; - let network = request?.currencyInfo?.network || "mainnet"; + let network: string | undefined = request?.currencyInfo?.network || "mainnet"; // FIXME: Use a non deprecated function - let currency = getCurrencyFromManager(request.currencyInfo, currencyManager); + let currency: CurrencyTypes.CurrencyDefinition | undefined = + getCurrencyFromManager(request.currencyInfo, currencyManager); + let paymentCurrencies: (CurrencyTypes.CurrencyDefinition | undefined)[] = []; let statuses: any = []; let isPaid = false; let loading = false; @@ -54,6 +60,9 @@ let hexStringChain = "0x" + account.chainId.toString(16); let correctChain = hexStringChain === String(getNetworkIdFromNetworkName(network)); + let paymentNetworkExtension: + | Types.Extension.IPaymentNetworkState + | undefined; const generateDetailParagraphs = (info: any) => { return [ @@ -113,20 +122,55 @@ signer = await getEthersSigner(wagmiConfig); requestData = singleRequest?.getData(); + paymentNetworkExtension = getPaymentNetworkExtension(requestData); - if (requestData.currencyInfo.type === Types.RequestLogic.CURRENCY.ERC20) { - approved = await checkApproval(requestData, signer); + if ( + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ) { + paymentCurrencies = + paymentNetworkExtension?.values?.acceptedTokens?.map((token: any) => + currencyManager.fromAddress( + token, + paymentNetworkExtension?.values?.network + ) + ); + } else if ( + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY + ) { + paymentCurrencies = [ + currencyManager.getNativeCurrency( + Types.RequestLogic.CURRENCY.ETH, + paymentNetworkExtension?.values?.network + ), + ]; + } else if ( + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT || + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT + ) { + paymentCurrencies = [currency]; + } else { + throw new Error("Unsupported payment network"); + } + + network = paymentCurrencies[0]?.network || "mainnet"; + + if (paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20) { + approved = await checkApproval(requestData, paymentCurrencies, signer); } else { approved = true; } isPaid = requestData?.balance?.balance! >= requestData?.expectedAmount; - loading = false; } catch (err: any) { - loading = false; + console.error("Error while checking invoice: ", err); if (String(err).includes("Unsupported payment")) { unsupportedNetwork = true; - return; } + } finally { + loading = false; } }; @@ -138,7 +182,32 @@ ); statuses = [...statuses, "Waiting for payment"]; - const paymentTx = await payRequest(requestData, signer); + + let paymentSettings = undefined; + if ( + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY || + paymentNetworkExtension?.id === + Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY + ) { + const { conversion } = await getConversionPaymentValues({ + baseAmount: requestData?.expectedAmount, + denominationCurrency: currency!, + selectedPaymentCurrency: paymentCurrencies[0]!, + currencyManager, + provider: signer, + fromAddress: address, + }); + paymentSettings = conversion; + } + + const paymentTx = await payRequest( + requestData, + signer, + undefined, + undefined, + paymentSettings + ); await paymentTx.wait(); statuses = [...statuses, "Payment detected"]; @@ -159,25 +228,62 @@ } }; - const checkApproval = async (requestData: any, signer: any) => { - return await hasErc20Approval(requestData!, address!, signer); + const checkApproval = async ( + requestData: any, + paymentCurrencies: any[], + signer: any + ) => { + const approvalCheckers: { [key: string]: () => Promise } = { + [Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: () => + hasErc20Approval(requestData!, address!, signer), + [Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: () => + hasErc20ApprovalForProxyConversion( + requestData!, + address!, + paymentCurrencies[0]?.address, + signer, + requestData.expectedAmount + ), + }; + + return ( + (paymentNetworkExtension?.id && + await approvalCheckers[paymentNetworkExtension.id]?.()) || + false + ); }; async function approve() { try { loading = true; + const approvers: { [key: string]: () => Promise } = { + [Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: + async () => { + const approvalTx = await approveErc20(requestData!, signer); + await approvalTx.wait(); + approved = true; + }, + [Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: async () => { + const approvalTx = await approveErc20ForProxyConversion( + requestData!, + paymentCurrencies[0]?.address, + signer + ); + await approvalTx.wait(); + approved = true; + }, + }; + if ( - getPaymentNetworkExtension(requestData!)?.id === - Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + paymentNetworkExtension?.id && + approvers[paymentNetworkExtension.id] ) { - const approvalTx = await approveErc20(requestData!, signer); - await approvalTx.wait(); - approved = true; + await approvers[paymentNetworkExtension.id](); } - loading = false; } catch (err) { - console.error("Something went wrong while approving ERC20 : ", err); + console.error("Something went wrong while approving ERC20: ", err); + } finally { loading = false; } } @@ -245,7 +351,12 @@ { try { - await exportToPDF(request, currency, config.logo); + await exportToPDF( + request, + currency, + paymentCurrencies, + config.logo + ); } catch (error) { toast.error(`Failed to export PDF`, { description: `${error}`, @@ -292,13 +403,22 @@

Payment Chain: - {currency?.network || "-"} + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.network || "-" + : ""}

Invoice Currency: {currency?.symbol || "-"}

+

+ Settlement Currency: + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.symbol || "-" + : ""} +

+ {#if request?.contentData?.invoiceItems}
@@ -440,7 +560,7 @@ type="button" text="Switch Network" padding="px-[12px] py-[6px]" - onClick={() => switchNetworkIfNeeded(network)} + onClick={() => switchNetworkIfNeeded(network || "mainnet")} /> {:else if !approved && !isPaid && !isPayee && !unsupportedNetwork}