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 7107659d..a9a1b456 100644 --- a/packages/create-invoice-form/src/lib/create-invoice-form.svelte +++ b/packages/create-invoice-form/src/lib/create-invoice-form.svelte @@ -20,6 +20,7 @@ import { calculateInvoiceTotals } from "@requestnetwork/shared-utils/invoiceTotals"; import { getCurrencySupportedNetworksForConversion, + initializeCreateInvoiceCurrencyManager, initializeCurrencyManager, } from "@requestnetwork/shared-utils/initCurrencyManager"; // Components @@ -29,6 +30,7 @@ import Modal from "@requestnetwork/shared-components/modal.svelte"; import { EncryptionTypes, CipherProviderTypes } from "@requestnetwork/types"; import { onDestroy, onMount, tick } from "svelte"; + import { CurrencyManager } from "@requestnetwork/currency"; interface CipherProvider extends CipherProviderTypes.ICipherProvider { disconnectWallet: () => void; @@ -37,7 +39,7 @@ export let config: IConfig; export let wagmiConfig: WagmiConfig; export let requestNetwork: RequestNetwork | null | undefined; - export let currencies: CurrencyTypes.CurrencyInput[] = []; + export let currencies: string[] = []; let cipherProvider: CipherProvider | undefined; let account: GetAccountReturnType | undefined = @@ -46,7 +48,7 @@ let activeConfig = config ? config : defaultConfig; let mainColor = activeConfig.colors.main; let secondaryColor = activeConfig.colors.secondary; - let currencyManager = initializeCurrencyManager(currencies); + let currencyManager: CurrencyManager; let invoiceCurrencyDropdown: { clear: () => void }; let networkDropdown: { clear: () => void }; @@ -58,10 +60,54 @@ let currency: CurrencyTypes.CurrencyDefinition | undefined = undefined; let invoiceCurrency: CurrencyTypes.CurrencyDefinition | undefined = undefined; + let defaultCurrencies: any[] = []; + + onMount(async () => { + currencyManager = await initializeCreateInvoiceCurrencyManager(currencies); + + defaultCurrencies = Object.values( + currencyManager.knownCurrencies.reduce( + ( + unique: { [x: string]: any }, + currency: { symbol: string | number } + ) => { + const baseSymbol = String(currency.symbol).split("-")[0]; + if (!unique[baseSymbol]) { + unique[baseSymbol] = { + ...currency, + symbol: baseSymbol, + }; + } + return unique; + }, + {} + ) + ); + + unwatchAccount = watchAccount(wagmiConfig, { + onChange( + account: GetAccountReturnType, + previousAccount: GetAccountReturnType + ) { + tick().then(() => { + handleWalletChange(account, previousAccount); + }); + }, + }); + }); + const handleNetworkChange = (newNetwork: string) => { if (newNetwork) { currencyDropdown.clear(); - invoiceCurrency = invoiceCurrency?.type !== Types.RequestLogic.CURRENCY.ISO4217 ? currencyManager.knownCurrencies.find(currency => invoiceCurrency?.symbol === currency.symbol && currency.network === newNetwork) : invoiceCurrency; + invoiceCurrency = + invoiceCurrency?.type !== Types.RequestLogic.CURRENCY.ISO4217 + ? currencyManager.knownCurrencies.find( + (currency) => + currency.symbol.split("-")[0] === + invoiceCurrency?.symbol.split("-")[0] && + currency.network === newNetwork + ) + : invoiceCurrency; network = newNetwork; currency = undefined; @@ -77,7 +123,7 @@ currencyManager?.getConversionPath( invoiceCurrency, currency, - currency?.network, + currency?.network )?.length > 0; return ( @@ -88,8 +134,14 @@ } // For other currency types (like ERC20) - return invoiceCurrency.hash === currency?.hash; - }, + // Compare base symbols (without network suffix) + const invoiceBaseSymbol = invoiceCurrency.symbol.split("-")[0]; + const currencyBaseSymbol = currency.symbol.split("-")[0]; + return ( + invoiceBaseSymbol === currencyBaseSymbol && + currency.network === newNetwork + ); + } ); } }; @@ -98,18 +150,9 @@ let canSubmit = false; let appStatus: APP_STATUS[] = []; let formData = getInitialFormData(); - // Remove duplicate currencies and filter out currencies with '-' in the symbol - let defaultCurrencies = Object.values(currencyManager.knownCurrencies.reduce( - (unique: { [x: string]: any; }, currency: { symbol: string | number; }) => { - if (!unique[currency.symbol] && !currency.symbol.includes('-')) unique[currency.symbol] = currency; - - return unique; - }, - {}, - )); const handleInvoiceCurrencyChange = ( - value: CurrencyTypes.CurrencyDefinition, + value: CurrencyTypes.CurrencyDefinition ) => { if (value !== invoiceCurrency) { networkDropdown.clear(); @@ -124,10 +167,16 @@ if (invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217) { networks = (getCurrencySupportedNetworksForConversion( invoiceCurrency.hash, - currencyManager, + currencyManager ) ?? []) as string[]; } else { - networks = currencyManager.knownCurrencies.filter(currency => currency.symbol === invoiceCurrency?.symbol).map(currency => currency.network); + const baseSymbol = invoiceCurrency.symbol.split("-")[0]; + networks = currencyManager.knownCurrencies + .filter((currency) => { + const currencyBaseSymbol = currency.symbol.split("-")[0]; + return currencyBaseSymbol === baseSymbol; + }) + .map((currency) => currency.network); } } }; @@ -152,7 +201,10 @@ cipherProvider?.disconnectWallet(); }; - const handleWalletChange = (account: GetAccountReturnType, previousAccount: GetAccountReturnType) => { + const handleWalletChange = ( + account: GetAccountReturnType, + previousAccount: GetAccountReturnType + ) => { if (account?.address !== previousAccount?.address) { handleWalletDisconnection(); handleWalletConnection(); @@ -163,16 +215,6 @@ } }; - onMount(() => { - unwatchAccount = watchAccount(wagmiConfig, { - onChange(account: GetAccountReturnType, previousAccount: GetAccountReturnType) { - tick().then(() => { - handleWalletChange(account, previousAccount); - }); - }, - }); - }); - let unwatchAccount: WatchAccountReturnType | undefined; onDestroy(() => { @@ -261,7 +303,7 @@ paymentNetwork: requestCreateParameters.paymentNetwork, contentData: requestCreateParameters.contentData, }, - [payeeEncryptionParams, payerEncryptionParams], + [payeeEncryptionParams, payerEncryptionParams] ); } else { request = await requestNetwork.createRequest({ diff --git a/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts b/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts index ff69873a..de0404b9 100644 --- a/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts +++ b/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts @@ -8,7 +8,7 @@ export interface CreateInvoiceFormProps { config: IConfig; wagmiConfig: WagmiConfig; requestNetwork: RequestNetwork | null | undefined; - currencies?: CurrencyTypes.CurrencyInput[]; + currencies: string[]; } /** @@ -25,7 +25,7 @@ export interface CreateInvoiceFormProps { * config={config} * wagmiConfig={wagmiConfig} * requestNetwork={requestNetwork} - * currencies={currencies} + * currencies={['ETH-MAINNET', 'USDC-MAINNET', 'USDC-MATIC']} * /> */ declare const CreateInvoiceForm: React.FC; diff --git a/packages/create-invoice-form/src/lib/utils/prepareRequest.ts b/packages/create-invoice-form/src/lib/utils/prepareRequest.ts index 86e59699..83e38d72 100644 --- a/packages/create-invoice-form/src/lib/utils/prepareRequest.ts +++ b/packages/create-invoice-form/src/lib/utils/prepareRequest.ts @@ -15,7 +15,11 @@ interface IRequestParams { address: `0x${string}` | undefined; } -const getPaymentNetwork = (invoiceCurrency: CurrencyTypes.CurrencyDefinition, currency: CurrencyTypes.CurrencyDefinition, formData: CustomFormData) => { +const getPaymentNetwork = ( + invoiceCurrency: CurrencyTypes.CurrencyDefinition, + currency: CurrencyTypes.CurrencyDefinition, + formData: CustomFormData +) => { if ( invoiceCurrency.type === Types.RequestLogic.CURRENCY.ISO4217 && currency.type === Types.RequestLogic.CURRENCY.ETH @@ -52,7 +56,7 @@ const getPaymentNetwork = (invoiceCurrency: CurrencyTypes.CurrencyDefinition, cu feeAddress: zeroAddress, feeAmount: "0", }, - } + }; } else if (currency.type === Types.RequestLogic.CURRENCY.ERC20) { return { id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, @@ -62,7 +66,7 @@ const getPaymentNetwork = (invoiceCurrency: CurrencyTypes.CurrencyDefinition, cu feeAddress: zeroAddress, feeAmount: "0", }, - } + }; } else { throw new Error("Unsupported payment network"); } @@ -76,13 +80,19 @@ export const prepareRequestParams = ({ invoiceTotals, }: IRequestParams): Types.ICreateRequestParameters => { const isERC20 = currency.type === Types.RequestLogic.CURRENCY.ERC20; - const isERC20InvoiceCurrency = invoiceCurrency.type === Types.RequestLogic.CURRENCY.ERC20; + const isERC20InvoiceCurrency = + invoiceCurrency.type === Types.RequestLogic.CURRENCY.ERC20; return { requestInfo: { currency: { type: invoiceCurrency.type, - value: isERC20InvoiceCurrency ? invoiceCurrency.address : invoiceCurrency.symbol, - network: invoiceCurrency.network, + value: isERC20InvoiceCurrency + ? invoiceCurrency.address + : invoiceCurrency.symbol, + network: + invoiceCurrency.network === "fiat" + ? undefined + : invoiceCurrency.network, }, expectedAmount: parseUnits( invoiceTotals.totalAmount.toString(), @@ -122,15 +132,17 @@ export const prepareRequestParams = ({ discount: item.discount != null ? parseUnits( - item.discount.toString(), - invoiceCurrency.decimals - ).toString() + item.discount.toString(), + invoiceCurrency.decimals + ).toString() : undefined, tax: { type: "percentage", amount: item.tax.amount.toString(), }, - currency: isERC20InvoiceCurrency ? invoiceCurrency.address : invoiceCurrency.symbol, + currency: isERC20InvoiceCurrency + ? invoiceCurrency.address + : invoiceCurrency.symbol, })), paymentTerms: { dueDate: new Date(formData.dueDate).toISOString(), diff --git a/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts b/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts index b575570c..9067cf18 100644 --- a/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts +++ b/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts @@ -8,7 +8,6 @@ export interface InvoiceDashboardProps { config: IConfig; wagmiConfig: WagmiConfig; requestNetwork: RequestNetwork | null | undefined; - currencies?: CurrencyTypes.CurrencyInput[]; } /** * InvoiceDashboard is a React component that integrates with the Request Network to manage and display invoices. @@ -25,9 +24,6 @@ export interface InvoiceDashboardProps { * config={config} * wagmiConfig={wagmiConfig} * requestNetwork={requestNetwork} - * currencies={currencies} - * isDecryptionEnabled={isDecryptionEnabled} - * enableDecryption={enableDecryption} * /> */ declare const InvoiceDashboard: React.FC; diff --git a/packages/invoice-dashboard/src/lib/view-requests.svelte b/packages/invoice-dashboard/src/lib/view-requests.svelte index a702f49b..59d23d0b 100644 --- a/packages/invoice-dashboard/src/lib/view-requests.svelte +++ b/packages/invoice-dashboard/src/lib/view-requests.svelte @@ -63,7 +63,6 @@ export let config: IConfig; export let wagmiConfig: WagmiConfig; export let requestNetwork: RequestNetwork | null | undefined; - export let currencies: CurrencyTypes.CurrencyInput[] = []; let cipherProvider: CipherProvider | undefined; @@ -182,8 +181,8 @@ $: isRequestPayed, getOneRequest(activeRequest); - onMount(() => { - currencyManager = initializeCurrencyManager(currencies); + onMount(async () => { + currencyManager = await initializeCurrencyManager(); }); const getRequests = async ( @@ -901,7 +900,7 @@ {wagmiConfig} bind:isRequestPayed {requestNetwork} - {currencyManager} + bind:currencyManager config={activeConfig} request={activeRequest} /> diff --git a/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts b/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts index 4afc8b6e..f9d306d9 100644 --- a/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts +++ b/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts @@ -3,7 +3,7 @@ import { CurrencyTypes } from "@requestnetwork/types"; import { getAnyErc20Balance } from "@requestnetwork/payment-processor"; import { BigNumber, BigNumberish, providers, utils } from "ethers"; -import { getConversionRate, getSlippageMargin } from './conversion' +import { getConversionRate, getSlippageMargin } from "./conversion"; export type SlippageLevel = "safe" | "risky"; @@ -27,7 +27,7 @@ interface ConversionPaymentValues { export const formatUnits = ( amount: BigNumber, - token: CurrencyTypes.CurrencyDefinition, + token: CurrencyTypes.CurrencyDefinition ): string => { return utils.formatUnits(amount, token.decimals); }; @@ -40,11 +40,11 @@ export const toFixedDecimal = (numberToFormat: number, decimals?: number) => { export const amountToFixedDecimal = ( amount: BigNumberish, currency: CurrencyTypes.CurrencyDefinition, - decimals?: number, + decimals?: number ) => { return toFixedDecimal( Number.parseFloat(formatUnits(BigNumber.from(amount), currency)), - decimals, + decimals ); }; @@ -55,7 +55,7 @@ export const amountToFixedDecimal = ( */ export const bigAmountify = ( amount: number, - token: Pick, + token: Pick ): BigNumber => { let [whole, decimals] = amount.toString().split("."); let pow = "0"; @@ -108,30 +108,33 @@ export const getConversionPaymentValues = async ({ const minConversionAmount = bigAmountify( baseAmount * Number(conversionRate), - selectedPaymentCurrency, + selectedPaymentCurrency ); - const safeConversionAmount = bigAmountify( - baseAmount * Number(conversionRate) * getSlippageMargin(selectedPaymentCurrency), - selectedPaymentCurrency, + baseAmount * + Number(conversionRate) * + getSlippageMargin(selectedPaymentCurrency), + selectedPaymentCurrency ); const userBalance = BigNumber.from( fromAddress && provider && + selectedPaymentCurrency.type === "ERC20" && "address" in selectedPaymentCurrency && selectedPaymentCurrency.address ? await getAnyErc20Balance( selectedPaymentCurrency.address, fromAddress, - provider, + provider ) - : safeConversionAmount, + : safeConversionAmount ); const hasEnoughForSlippage = userBalance.gte(safeConversionAmount); const hasEnough = userBalance.gte(minConversionAmount); const isRisky = hasEnough && !hasEnoughForSlippage; + const slippageLevel = isRisky ? "risky" : ("safe" as SlippageLevel); const conversionAmount = isRisky ? userBalance : safeConversionAmount; @@ -145,7 +148,7 @@ export const getConversionPaymentValues = async ({ value: amountToFixedDecimal( minConversionAmount, selectedPaymentCurrency, - 4, + 4 ), currency: selectedPaymentCurrency, }; @@ -153,7 +156,7 @@ export const getConversionPaymentValues = async ({ value: amountToFixedDecimal( safeConversionAmount, selectedPaymentCurrency, - 4, + 4 ), currency: selectedPaymentCurrency, }; diff --git a/shared/utils/initCurrencyManager.ts b/shared/utils/initCurrencyManager.ts index 93acf6d5..109a5c78 100644 --- a/shared/utils/initCurrencyManager.ts +++ b/shared/utils/initCurrencyManager.ts @@ -61,33 +61,31 @@ const defaultCurrencyIds = [ "fUSDC-sepolia", ]; -import { Types } from "@requestnetwork/request-client.js"; import { formattedCurrencyConversionPairs } from "./currencyConversionPairs"; -export function initializeCurrencyManager( - customCurrencies: CurrencyTypes.CurrencyInput[] = [] -): CurrencyManager { - // If customCurrencies is provided, use only those - if (customCurrencies?.length > 0) { - return new CurrencyManager( - customCurrencies, - {}, - formattedCurrencyConversionPairs +const TOKEN_LIST_URL = + "https://requestnetwork.github.io/request-token-list/latest.json"; + +const fetchTokenList = async () => { + try { + const requestNetworkTokenList = await fetch(TOKEN_LIST_URL).then((res) => + res.json() ); + + return requestNetworkTokenList.tokens; + } catch (err) { + console.error("Failed to fetch token list", err); + return []; } +}; - // Otherwise, use default currencies - const defaultCurrencies = CurrencyManager.getDefaultList().filter( - (currency) => defaultCurrencyIds.includes(currency.id) - ); +export async function initializeCurrencyManager(): Promise { + const tokens = await fetchTokenList(); - return new CurrencyManager( - defaultCurrencies, - {}, - formattedCurrencyConversionPairs - ); + return new CurrencyManager(tokens, {}, formattedCurrencyConversionPairs); } +// Note: this function is used in the Payment Widget, I did not want to change it to not cause any unintended side effects. export function initializeCurrencyManagerWithCurrencyIDS( customCurrencyIds: string[] ): any { @@ -105,6 +103,26 @@ export function initializeCurrencyManagerWithCurrencyIDS( }; } +export async function initializeCreateInvoiceCurrencyManager( + customCurrencyIds: string[] +): Promise> { + const tokens = await fetchTokenList(); + + const tokenMap = new Map(tokens.map((token: any) => [token.id, token])); + + const currencies = customCurrencyIds + .map((id) => tokenMap.get(id)) + .filter((token): token is CurrencyTypes.CurrencyInput => token != null); + + const currencyManager = new CurrencyManager( + currencies, + {}, + formattedCurrencyConversionPairs + ); + + return currencyManager; +} + export const getCurrencySupportedNetworksForConversion = ( currencyHash: string, currencyManager: any