From 915b273d0a3f1dc4e08c0784cab445c106231c7c Mon Sep 17 00:00:00 2001 From: Mimi Tran <80493680+mimi-imtbl@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:18:05 +1000 Subject: [PATCH] feat: [GPR-536][Sale Widget] Get multiple currencies (#1653) Co-authored-by: Jhonatan Gonzalez --- .../sale/context/SaleContextProvider.tsx | 19 +- .../sortAndDeduplicateCurrencies.test.ts | 397 ++++++++++++++++++ .../functions/sortAndDeduplicateCurrencies.ts | 40 ++ .../functions/transformToClientConfig.test.ts | 112 +++++ .../sale/functions/transformToClientConfig.ts | 48 +++ .../src/widgets/sale/hooks/useClientConfig.ts | 130 ++++-- .../widgets-lib/src/widgets/sale/types.ts | 35 +- 7 files changed, 738 insertions(+), 43 deletions(-) create mode 100644 packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.test.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.test.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.ts diff --git a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx index bbe9146308..2d6748a13e 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx @@ -172,7 +172,9 @@ export function SaleContextProvider(props: { >(undefined); const [fundingRoutes, setFundingRoutes] = useState([]); - const [disabledPaymentTypes, setDisabledPaymentTypes] = useState([]); + const [disabledPaymentTypes, setDisabledPaymentTypes] = useState< + SalePaymentTypes[] + >([]); const disablePaymentTypes = (types: SalePaymentTypes[]) => { setDisabledPaymentTypes((prev) => Array.from(new Set([...(prev || []), ...types]))); @@ -180,12 +182,16 @@ export function SaleContextProvider(props: { const [invalidParameters, setInvalidParameters] = useState(false); - const { currency, clientConfig } = useClientConfig({ + const { + selectedCurrency, clientConfig, clientConfigError, + } = useClientConfig({ environmentId, environment: config.environment, + checkout, + provider, }); - const fromTokenAddress = currency?.erc20Address || ''; + const fromTokenAddress = selectedCurrency?.address || ''; const goBackToPaymentMethods = useCallback( (type?: SalePaymentTypes | undefined, data?: Record) => { @@ -303,6 +309,11 @@ export function SaleContextProvider(props: { goToErrorView(signError.type, signError.data); }, [signError]); + useEffect(() => { + if (!clientConfigError) return; + goToErrorView(clientConfigError.type, clientConfigError.data); + }, [clientConfigError]); + const { smartCheckout, smartCheckoutResult, smartCheckoutError } = useSmartCheckout({ provider, checkout, @@ -327,7 +338,7 @@ export function SaleContextProvider(props: { data: getTopUpViewData( smartCheckoutError, fromTokenAddress, - currency?.name!, + selectedCurrency?.name!, ), }, }, diff --git a/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.test.ts b/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.test.ts new file mode 100644 index 0000000000..6a8275db54 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.test.ts @@ -0,0 +1,397 @@ +import { SaleWidgetCurrency, SaleWidgetCurrencyType } from '../types'; +import { sortAndDeduplicateCurrencies } from './sortAndDeduplicateCurrencies'; + +describe('sortAndDeduplicateCurrencies', () => { + it('should remove duplicates and sort currencies based on priority: base -> settlement -> swappable', () => { + const allCurrencies: SaleWidgetCurrency[] = [ + { + name: 'Monopoly', + symbol: 'MPLY', + address: '0x5fc1aBC911386e2A9FEfc874ab15E20A3434D2B9', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + base: false, + decimals: 18, + name: 'GOG', + address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchangeId: 'guild-of-guardians', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: false, + decimals: 18, + name: 'ETH', + address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchangeId: 'ethereum', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'Immutable Token', + symbol: 'IMX', + address: 'native', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Wrapped IMX', + symbol: 'WIMX', + address: '0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkTDR', + symbol: 'zkTDR', + address: '0x6531F7B9158d78Ca78b46799c4Fd65C2Af8Ae506', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + address: '0xB8EE289C64C1A0DC0311364721aDA8c3180D838C', + name: 'GOG', + symbol: 'GOG', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkPSP', + symbol: 'zkPSP', + address: '0x88B35dF96CbEDF2946586147557F7D5D0CCE7e5c', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkWLT', + symbol: 'zkWLT', + address: '0x8A5b0470ee48248bEb7D1E745c1EbA0DCA77215e', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'zkCORE', + symbol: 'zkCORE', + address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'USDC', + symbol: 'USDC', + address: '0x3B2d8A1931736Fc321C24864BceEe981B11c3c57', + decimals: 6, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + ]; + + const transformedCurrencies: SaleWidgetCurrency[] = [ + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: false, + decimals: 18, + name: 'GOG', + address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchangeId: 'guild-of-guardians', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: false, + decimals: 18, + name: 'ETH', + address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchangeId: 'ethereum', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'Monopoly', + symbol: 'MPLY', + address: '0x5fc1aBC911386e2A9FEfc874ab15E20A3434D2B9', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Immutable Token', + symbol: 'IMX', + address: 'native', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Wrapped IMX', + symbol: 'WIMX', + address: '0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkTDR', + symbol: 'zkTDR', + address: '0x6531F7B9158d78Ca78b46799c4Fd65C2Af8Ae506', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkPSP', + symbol: 'zkPSP', + address: '0x88B35dF96CbEDF2946586147557F7D5D0CCE7e5c', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkWLT', + symbol: 'zkWLT', + address: '0x8A5b0470ee48248bEb7D1E745c1EbA0DCA77215e', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkCORE', + symbol: 'zkCORE', + address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + ]; + + expect(sortAndDeduplicateCurrencies(allCurrencies)).toStrictEqual( + transformedCurrencies, + ); + }); + it('should put the first found "base: true" currency in the first index of the sorted array', () => { + const allCurrencies: SaleWidgetCurrency[] = [ + { + name: 'Monopoly', + symbol: 'MPLY', + address: '0x5fc1aBC911386e2A9FEfc874ab15E20A3434D2B9', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + base: true, + decimals: 18, + name: 'GOG', + address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchangeId: 'guild-of-guardians', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: false, + decimals: 18, + name: 'ETH', + address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchangeId: 'ethereum', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'Immutable Token', + symbol: 'IMX', + address: 'native', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Wrapped IMX', + symbol: 'WIMX', + address: '0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkTDR', + symbol: 'zkTDR', + address: '0x6531F7B9158d78Ca78b46799c4Fd65C2Af8Ae506', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + address: '0xB8EE289C64C1A0DC0311364721aDA8c3180D838C', + name: 'GOG', + symbol: 'GOG', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkPSP', + symbol: 'zkPSP', + address: '0x88B35dF96CbEDF2946586147557F7D5D0CCE7e5c', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkWLT', + symbol: 'zkWLT', + address: '0x8A5b0470ee48248bEb7D1E745c1EbA0DCA77215e', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'zkCORE', + symbol: 'zkCORE', + address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'USDC', + symbol: 'USDC', + address: '0x3B2d8A1931736Fc321C24864BceEe981B11c3c57', + decimals: 6, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + ]; + + const transformedCurrencies: SaleWidgetCurrency[] = [ + { + base: true, + decimals: 18, + name: 'GOG', + address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchangeId: 'guild-of-guardians', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: false, + decimals: 18, + name: 'ETH', + address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchangeId: 'ethereum', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + }, + { + name: 'Monopoly', + symbol: 'MPLY', + address: '0x5fc1aBC911386e2A9FEfc874ab15E20A3434D2B9', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Immutable Token', + symbol: 'IMX', + address: 'native', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'Wrapped IMX', + symbol: 'WIMX', + address: '0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkTDR', + symbol: 'zkTDR', + address: '0x6531F7B9158d78Ca78b46799c4Fd65C2Af8Ae506', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkSRE', + symbol: 'zkSRE', + address: '0x43566cAB87CC147C95e2895E7b972E19993520e4', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkPSP', + symbol: 'zkPSP', + address: '0x88B35dF96CbEDF2946586147557F7D5D0CCE7e5c', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkWLT', + symbol: 'zkWLT', + address: '0x8A5b0470ee48248bEb7D1E745c1EbA0DCA77215e', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + { + name: 'zkCORE', + symbol: 'zkCORE', + address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E', + decimals: 18, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + }, + ]; + + expect(sortAndDeduplicateCurrencies(allCurrencies)).toStrictEqual( + transformedCurrencies, + ); + }); +}); diff --git a/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.ts b/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.ts new file mode 100644 index 0000000000..b9ec23d8b0 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/sale/functions/sortAndDeduplicateCurrencies.ts @@ -0,0 +1,40 @@ +import { SaleWidgetCurrency, SaleWidgetCurrencyType } from '../types'; + +export const sortAndDeduplicateCurrencies = ( + currencies: SaleWidgetCurrency[], +): SaleWidgetCurrency[] => { + const settlementCurrencies = new Map(); + const swappableCurrencies = new Map(); + + let baseCurrency: SaleWidgetCurrency | undefined; + + for (const currency of currencies) { + const currencyNameKey = currency.name.toLowerCase(); + if ( + currency.base + && currency.currencyType === SaleWidgetCurrencyType.SETTLEMENT + ) { + if (!baseCurrency) { + baseCurrency = currency; + } + } + + if (baseCurrency?.name !== currency.name) { + if (currency.currencyType === SaleWidgetCurrencyType.SETTLEMENT) { + settlementCurrencies.set(currencyNameKey, currency); + } else if (currency.currencyType === SaleWidgetCurrencyType.SWAPPABLE) { + if (!settlementCurrencies.has(currencyNameKey)) { + swappableCurrencies.set(currencyNameKey, currency); + } + } + } + } + + const sortedAndUniqueCurrencies = [ + ...(baseCurrency ? [baseCurrency] : []), + ...settlementCurrencies.values(), + ...swappableCurrencies.values(), + ]; + + return sortedAndUniqueCurrencies; +}; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.test.ts b/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.test.ts new file mode 100644 index 0000000000..6355158415 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { ClientConfig, SignPaymentTypes } from '../types'; +import { + ClientConfigResponse, + transformToClientConfig, +} from './transformToClientConfig'; + +describe('transformToClientConfig', () => { + it('should return input object with camel case keys', () => { + const fromClientConfig: ClientConfigResponse = { + contract_id: '65696ef06c55501aab4da5e7', + currencies: [ + { + base: true, + decimals: 6, + erc20_address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchange_id: 'usd-coin', + name: 'USDC', + }, + { + base: false, + decimals: 18, + erc20_address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchange_id: 'guild-of-guardians', + name: 'GOG', + }, + { + base: false, + decimals: 18, + erc20_address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchange_id: 'ethereum', + name: 'ETH', + }, + ], + currency_conversion: { + ETH: { + amount: 0.000284, + name: 'ETH', + type: 'crypto', + }, + GOG: { + amount: 6.00203, + name: 'GOG', + type: 'crypto', + }, + USD: { + amount: 0.999392, + name: 'USD', + type: 'fiat', + }, + USDC: { + amount: 1, + name: 'USDC', + type: 'crypto', + }, + }, + }; + + const toClientConfig: ClientConfig = { + contractId: '65696ef06c55501aab4da5e7', + currencies: [ + { + base: true, + decimals: 6, + name: 'USDC', + address: '0x3b2d8a1931736fc321c24864bceee981b11c3c57', + exchangeId: 'usd-coin', + }, + { + base: false, + decimals: 18, + name: 'GOG', + address: '0xb8ee289c64c1a0dc0311364721ada8c3180d838c', + exchangeId: 'guild-of-guardians', + }, + { + base: false, + decimals: 18, + name: 'ETH', + address: '0xe9e96d1aad82562b7588f03f49ad34186f996478', + exchangeId: 'ethereum', + }, + ], + currencyConversion: { + ETH: { + amount: 0.000284, + name: 'ETH', + type: SignPaymentTypes.CRYPTO, + }, + GOG: { + amount: 6.00203, + name: 'GOG', + type: SignPaymentTypes.CRYPTO, + }, + USD: { + amount: 0.999392, + name: 'USD', + type: SignPaymentTypes.FIAT, + }, + USDC: { + amount: 1, + name: 'USDC', + type: SignPaymentTypes.CRYPTO, + }, + }, + }; + + expect(transformToClientConfig(fromClientConfig)).toStrictEqual( + toClientConfig, + ); + }); +}); diff --git a/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.ts b/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.ts new file mode 100644 index 0000000000..d144f1ed12 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/sale/functions/transformToClientConfig.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + ClientConfig, + ClientConfigCurrencyConversion, + SignPaymentTypes, +} from '../types'; + +export type ClientConfigResponse = { + contract_id: string; + currencies: { + base: boolean; + decimals: number; + erc20_address: string; + exchange_id: string; + name: string; + }[]; + currency_conversion: { + [key: string]: { + amount: number; + name: string; + type: string; + }; + }; +}; + +export const transformToClientConfig = ( + response: ClientConfigResponse, +): ClientConfig => ({ + contractId: response.contract_id, + currencies: response.currencies.map( + ({ erc20_address, exchange_id, ...rest }) => ({ + ...rest, + address: erc20_address, + exchangeId: exchange_id, + }), + ), + currencyConversion: Object.entries(response.currency_conversion).reduce( + (acc, [key, { amount, name, type }]) => { + acc[key] = { + amount, + name, + type: type as SignPaymentTypes, + }; + return acc; + }, + {} as ClientConfigCurrencyConversion, + ), +}); diff --git a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useClientConfig.ts b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useClientConfig.ts index d7d814909f..4279a9d924 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useClientConfig.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useClientConfig.ts @@ -1,82 +1,140 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import { useState, useEffect } from 'react'; +import { Environment } from '@imtbl/config'; +import { Checkout, TokenFilterTypes } from '@imtbl/checkout-sdk'; +import { Web3Provider } from '@ethersproject/providers'; import { PRIMARY_SALES_API_BASE_URL } from '../utils/config'; -import { ClientConfig, ClientConfigCurrency } from '../types'; - -type ClientConfigResponse = { - contract_id: string; - currencies: { - name: string; - decimals: number; - erc20_address: string; - }[]; -}; -const toClientConfig = (response: ClientConfigResponse): ClientConfig => ({ - contractId: response.contract_id, - currencies: response.currencies.map((c) => ({ - ...c, - erc20Address: c.erc20_address, - })), -}); +import { + ClientConfig, + ClientConfigCurrency, + SaleErrorTypes, + SaleWidgetCurrency, + SaleWidgetCurrencyType, +} from '../types'; +import { sortAndDeduplicateCurrencies } from '../functions/sortAndDeduplicateCurrencies'; +import { + ClientConfigResponse, + transformToClientConfig, +} from '../functions/transformToClientConfig'; type UseClientConfigParams = { - environment: string; + environment: Environment; environmentId: string; + checkout: Checkout | undefined; + provider: Web3Provider | undefined; defaultCurrency?: 'USDC'; }; export const defaultClientConfig: ClientConfig = { contractId: '', currencies: [], + currencyConversion: {}, +}; + +export type ConfigError = { + type: SaleErrorTypes; + data?: Record; }; export const useClientConfig = ({ environment, environmentId, + checkout, + provider, defaultCurrency, }: UseClientConfigParams) => { - const [currency, setCurrency] = useState(); + const [selectedCurrency, setSelectedCurrency] = useState< + ClientConfigCurrency | undefined + >(); const [clientConfig, setClientConfig] = useState(defaultClientConfig); + const [clientConfigError, setClientConfigError] = useState< + ConfigError | undefined + >(undefined); + const [allCurrencies, setAllCurrencies] = useState([]); useEffect(() => { if (!environment || !environmentId) return; - (async () => { - const baseUrl = `${PRIMARY_SALES_API_BASE_URL[environment]}/${environmentId}/client-config`; + const fetchSwappableCurrencies = async () => { + if (!checkout || !provider) { + return []; + } + try { + const checkoutNetworkInfo = await checkout.getNetworkInfo({ + provider, + }); + const swapAllowList = await checkout.getTokenAllowList({ + type: TokenFilterTypes.SWAP, + chainId: checkoutNetworkInfo.chainId, + }); + return swapAllowList.tokens.map((token) => ({ + ...token, + currencyType: SaleWidgetCurrencyType.SWAPPABLE, + })); + } catch (error) { + console.warn("Error fetching swappable currencies", error); // eslint-disable-line + return []; + } + }; + + const fetchSettlementCurrencies = async () => { try { + const baseUrl = `${PRIMARY_SALES_API_BASE_URL[environment]}/${environmentId}/client-config`; const response = await fetch(baseUrl, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + // eslint-disable-next-line @typescript-eslint/naming-convention + headers: { 'Content-Type': 'application/json' }, }); - if (!response.ok) { - throw new Error(`${response.status} - ${response.statusText}`); - } + if (!response.ok) throw new Error(`${response.status} - ${response.statusText}`); const data: ClientConfigResponse = await response.json(); + const config = transformToClientConfig(data); + setClientConfig(config); - setClientConfig(toClientConfig(data)); + return config.currencies.map((currency) => ({ + ...currency, + currencyType: SaleWidgetCurrencyType.SETTLEMENT, + })); } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error fetching client config', error); + setClientConfigError({ + type: SaleErrorTypes.DEFAULT, + data: { reason: 'Error fetching settlement currencies' }, + }); + return []; } + }; + + (async () => { + const [swappableCurrencies, settlementCurrencies] = await Promise.all([ + fetchSwappableCurrencies(), + fetchSettlementCurrencies(), + ]); + + const combinedCurrencies: SaleWidgetCurrency[] = [ + ...settlementCurrencies, + ...swappableCurrencies, + ]; + + const transformedCurrencies = sortAndDeduplicateCurrencies(combinedCurrencies); + setAllCurrencies(transformedCurrencies); })(); - }, [environment, environmentId]); + }, [environment, environmentId, checkout, provider]); useEffect(() => { if (clientConfig.currencies.length === 0) return; - const selectedCurrency = clientConfig.currencies.find((c) => c.name === defaultCurrency) - || clientConfig.currencies[0]; - setCurrency(selectedCurrency); + const defaultSelectedCurrency = clientConfig.currencies.find((c) => c.name === defaultCurrency) + || clientConfig.currencies.find((c) => c.base); + setSelectedCurrency(defaultSelectedCurrency); }, [defaultCurrency, clientConfig]); return { clientConfig, - currency, + selectedCurrency, + setSelectedCurrency, + allCurrencies, + clientConfigError, }; }; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/types.ts b/packages/checkout/widgets-lib/src/widgets/sale/types.ts index f69a8ddbfd..b5c9bc2c63 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/types.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/types.ts @@ -60,7 +60,7 @@ export type SmartCheckoutError = { data?: { error: Error; transactionRequirements?: TransactionRequirement[]; - } + }; }; export type ExecutedTransaction = { @@ -94,16 +94,45 @@ export enum SmartCheckoutErrorTypes { } export type ClientConfigCurrency = { - name: string; + base: boolean; decimals: number; - erc20Address: string; + address: string; + exchangeId: string; + name: string; +}; + +export type CurrencyConversionDetail = { + amount: number; + name: string; + type: SignPaymentTypes; +}; + +export type ClientConfigCurrencyConversion = { + [key: string]: CurrencyConversionDetail; }; export type ClientConfig = { contractId: string; currencies: ClientConfigCurrency[]; + currencyConversion: ClientConfigCurrencyConversion; }; +export type SaleWidgetCurrency = { + name: string; + symbol?: string; + decimals: number; + address?: string; + icon?: string; + base?: boolean; + exchangeId?: string; + currencyType: SaleWidgetCurrencyType; +}; + +export enum SaleWidgetCurrencyType { + SWAPPABLE = 'swappable', + SETTLEMENT = 'settlement', +} + export enum SignPaymentTypes { CRYPTO = 'crypto', FIAT = 'fiat',