diff --git a/packages/currency/src/aggregators/private.json b/packages/currency/src/aggregators/private.json index c1829cf411..4626c46197 100644 --- a/packages/currency/src/aggregators/private.json +++ b/packages/currency/src/aggregators/private.json @@ -5,16 +5,16 @@ "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { "0x38cf23c52bb4b13f051aec09580a2de845a7fa35": 1, "0x17b4158805772ced11225e77339f90beb5aae968": 1, - "0xf5af88e117747e87fc5929f2ff87221b1447652e": 1 + "0xa65ded58a0afee8241e788c5115ca53ef3925fd2": 1 }, "0x17b4158805772ced11225e77339f90beb5aae968": { "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 }, - "0xf5af88e117747e87fc5929f2ff87221b1447652e": { + "0xa65ded58a0afee8241e788c5115ca53ef3925fd2": { "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1, "0x8acee021a27779d8e98b9650722676b850b25e11": 1 }, "0x8acee021a27779d8e98b9650722676b850b25e11": { - "0xf5af88e117747e87fc5929f2ff87221b1447652e": 1 + "0xa65ded58a0afee8241e788c5115ca53ef3925fd2": 1 } } diff --git a/packages/currency/src/native.ts b/packages/currency/src/native.ts index 40f6d40adf..083a736b38 100644 --- a/packages/currency/src/native.ts +++ b/packages/currency/src/native.ts @@ -9,6 +9,12 @@ type NativeBtcCurrency = NamedNativeCurrency & { network: CurrencyTypes.BtcChain export const nativeCurrencies: Record & Record = { [RequestLogicTypes.CURRENCY.ETH]: [ + { + symbol: 'ETH-private', + decimals: 18, + name: 'Ether', + network: 'private', + }, { symbol: 'ETH', decimals: 18, diff --git a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts index 8f81445381..224ae89192 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -93,11 +93,9 @@ export function encodePayAnyToErc20ProxyRequest( * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. * @param feeAmountOverride Optionally, the fee amount to pay. Defaults to the fee amount of the request. */ -export function checkRequestAndGetPathAndCurrency( +export function getConversionPathForErc20Request( request: ClientTypes.IRequestData, paymentSettings: IConversionPaymentSettings, - amount?: BigNumberish, - feeAmountOverride?: BigNumberish, ): { path: string[]; requestCurrency: CurrencyDefinition } { if (!paymentSettings.currency) { throw new Error('currency must be provided in the paymentSettings'); @@ -130,9 +128,6 @@ export function checkRequestAndGetPathAndCurrency( `Impossible to find a conversion path between from ${requestCurrency.symbol} (${requestCurrency.hash}) to ${paymentCurrency.symbol} (${paymentCurrency.hash})`, ); } - - // Check request - validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); return { path, requestCurrency }; } @@ -157,12 +152,8 @@ function prepareAnyToErc20Arguments( amountToPay: BigNumber; feeToPay: BigNumber; } { - const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( - request, - paymentSettings, - amount, - feeAmountOverride, - ); + const { path, requestCurrency } = getConversionPathForErc20Request(request, paymentSettings); + validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues(request); diff --git a/packages/payment-processor/src/payment/any-to-eth-proxy.ts b/packages/payment-processor/src/payment/any-to-eth-proxy.ts index 96e602c6db..6c311160d6 100644 --- a/packages/payment-processor/src/payment/any-to-eth-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-eth-proxy.ts @@ -1,6 +1,10 @@ import { constants, ContractTransaction, Signer, providers, BigNumberish, BigNumber } from 'ethers'; -import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { + CurrencyDefinition, + CurrencyManager, + UnsupportedCurrencyError, +} from '@requestnetwork/currency'; import { AnyToEthFeeProxyPaymentDetector } from '@requestnetwork/payment-detection'; import { EthConversionProxy__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, RequestLogicTypes } from '@requestnetwork/types'; @@ -54,40 +58,11 @@ export function encodePayAnyToEthProxyRequest( amount?: BigNumberish, feeAmountOverride?: BigNumberish, ): string { - const currencyManager = paymentSettings.currencyManager || CurrencyManager.getDefault(); - - if (!request.currencyInfo) { - throw new Error(`currency not specified`); - } + const { path, requestCurrency } = getConversionPathForEthRequest(request, paymentSettings); - const requestCurrency = currencyManager.fromStorageCurrency(request.currencyInfo); - if (!requestCurrency) { - throw new UnsupportedCurrencyError(request.currencyInfo); - } - - const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan, network } = + const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues(request); - if (!network) { - throw new Error(`missing network`); - } - - const paymentCurrency = currencyManager.getNativeCurrency( - RequestLogicTypes.CURRENCY.ETH, - network, - ); - if (!paymentCurrency) { - throw new UnsupportedCurrencyError({ value: 'ETH', network }); - } - - // Compute the path automatically - const path = currencyManager.getConversionPath(requestCurrency, paymentCurrency, network); - if (!path) { - throw new Error( - `Impossible to find a conversion path between from ${requestCurrency.symbol} (${requestCurrency.hash}) to ${paymentCurrency.symbol} (${paymentCurrency.hash})`, - ); - } - const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); @@ -125,3 +100,43 @@ export function prepareAnyToEthProxyPaymentTransaction( value: BigNumber.from(paymentSettings.maxToSpend), }; } + +export function getConversionPathForEthRequest( + request: ClientTypes.IRequestData, + paymentSettings: IConversionPaymentSettings, +): { path: string[]; requestCurrency: CurrencyDefinition } { + const currencyManager = paymentSettings.currencyManager || CurrencyManager.getDefault(); + + if (!request.currencyInfo) { + throw new Error(`currency not specified`); + } + + const requestCurrency = currencyManager.fromStorageCurrency(request.currencyInfo); + if (!requestCurrency) { + throw new UnsupportedCurrencyError(request.currencyInfo); + } + + const { network } = getRequestPaymentValues(request); + + if (!network) { + throw new Error(`missing network`); + } + + const paymentCurrency = currencyManager.getNativeCurrency( + RequestLogicTypes.CURRENCY.ETH, + network, + ); + if (!paymentCurrency) { + throw new UnsupportedCurrencyError({ value: 'ETH', network }); + } + + // Compute the path automatically + const path = currencyManager.getConversionPath(requestCurrency, paymentCurrency, network); + if (!path) { + throw new Error( + `Impossible to find a conversion path between from ${requestCurrency.symbol} (${requestCurrency.hash}) to ${paymentCurrency.symbol} (${paymentCurrency.hash})`, + ); + } + + return { path, requestCurrency }; +} diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 86f4649798..897f84ce16 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -18,6 +18,7 @@ import { getRequestPaymentValues, getSigner, MAX_ALLOWANCE, + validateConversionFeeProxyRequest, validateErc20FeeProxyRequest, } from './utils'; import { @@ -25,12 +26,19 @@ import { getPaymentNetworkExtension, } from '@requestnetwork/payment-detection'; import { IPreparedTransaction } from './prepared-transaction'; -import { EnrichedRequest, IConversionPaymentSettings } from './index'; -import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; +import { IConversionPaymentSettings } from './index'; +import { getConversionPathForErc20Request } from './any-to-erc20-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { IState } from 'types/dist/extension-types'; -import { CurrencyDefinition, ICurrencyManager } from '@requestnetwork/currency'; -import { IConversionSettings, IRequestPaymentOptions } from './settings'; +import { CurrencyDefinition, CurrencyManager, ICurrencyManager } from '@requestnetwork/currency'; +import { + BatchPaymentNetworks, + EnrichedRequest, + IConversionSettings, + IRequestPaymentOptions, +} from '../types'; +import { validateEthFeeProxyRequest } from './eth-fee-proxy'; +import { getConversionPathForEthRequest } from './any-to-eth-proxy'; const CURRENCY = RequestLogicTypes.CURRENCY; @@ -81,14 +89,67 @@ export function prepareBatchConversionPaymentTransaction( options.skipFeeUSDLimit, options.conversion, ); + const value = getBatchTxValue(enrichedRequests); const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, options.version); return { data: encodedTx, to: proxyAddress, - value: 0, + value, }; } +const mapPnToDetailsBuilder: Record< + BatchPaymentNetworks, + (req: EnrichedRequest, isNative: boolean) => PaymentTypes.RequestDetail +> = { + 'pn-any-to-erc20-proxy': getRequestDetailWithConversion, + 'pn-any-to-eth-proxy': getRequestDetailWithConversion, + 'pn-erc20-fee-proxy-contract': getRequestDetailWithoutConversion, + 'pn-eth-fee-proxy-contract': getRequestDetailWithoutConversion, +}; + +const mapPnToAllowedCurrencies: Record = { + 'pn-any-to-erc20-proxy': [CURRENCY.ERC20, CURRENCY.ISO4217, CURRENCY.ETH], + 'pn-any-to-eth-proxy': [CURRENCY.ERC20, CURRENCY.ISO4217], + 'pn-erc20-fee-proxy-contract': [CURRENCY.ERC20], + 'pn-eth-fee-proxy-contract': [CURRENCY.ETH], +}; + +const mapPnToBatchId: Record = { + 'pn-any-to-erc20-proxy': + PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + 'pn-any-to-eth-proxy': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + 'pn-erc20-fee-proxy-contract': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + 'pn-eth-fee-proxy-contract': PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, +}; + +const computeRequestDetails = ({ + enrichedRequest, + extension, +}: { + enrichedRequest: EnrichedRequest; + extension: IState | undefined; +}) => { + const paymentNetworkId = enrichedRequest.paymentNetworkId; + const allowedCurrencies = mapPnToAllowedCurrencies[paymentNetworkId]; + const detailsBuilder = mapPnToDetailsBuilder[paymentNetworkId]; + const isNative = + paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY || + paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT; + + extension = extension ?? getPaymentNetworkExtension(enrichedRequest.request); + + comparePnTypeAndVersion(extension, enrichedRequest.request); + if (!allowedCurrencies.includes(enrichedRequest.request.currencyInfo.type)) { + throw new Error(`wrong request currencyInfo type`); + } + + return { + input: detailsBuilder(enrichedRequest, isNative), + extension, + }; +}; + /** * Encodes a transaction to pay a batch of requests with an ERC20 currency * that can be different from the request currency (eg. fiat). @@ -108,65 +169,56 @@ function encodePayBatchConversionRequest( const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); const { network } = getPnAndNetwork(enrichedRequests[0].request); - let firstConversionRequestExtension: IState | undefined; - let firstNoConversionRequestExtension: IState | undefined; - const ERC20NoConversionRequestDetails: PaymentTypes.RequestDetail[] = []; - const ERC20ConversionRequestDetails: PaymentTypes.RequestDetail[] = []; + const requestDetails: Record = { + 'pn-any-to-erc20-proxy': [], + 'pn-any-to-eth-proxy': [], + 'pn-erc20-fee-proxy-contract': [], + 'pn-eth-fee-proxy-contract': [], + }; + + const requestExtensions: Record | undefined> = { + 'pn-any-to-erc20-proxy': undefined, + 'pn-any-to-eth-proxy': undefined, + 'pn-erc20-fee-proxy-contract': undefined, + 'pn-eth-fee-proxy-contract': undefined, + }; - // fill ERC20ConversionRequestDetails and ERC20NoConversionRequestDetails lists for (const enrichedRequest of enrichedRequests) { const request = enrichedRequest.request; - if (enrichedRequest.paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY) { - enrichedRequest.paymentSettings.currencyManager = conversion.currencyManager; - firstConversionRequestExtension = - firstConversionRequestExtension ?? getPaymentNetworkExtension(request); - - comparePnTypeAndVersion(firstConversionRequestExtension, request); - if (![CURRENCY.ERC20, CURRENCY.ISO4217].includes(request.currencyInfo.type)) { - throw new Error(`wrong request currencyInfo type`); - } - ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest)); - } else if ( - enrichedRequest.paymentNetworkId === - ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - ) { - firstNoConversionRequestExtension = - firstNoConversionRequestExtension ?? getPaymentNetworkExtension(request); - - // isERC20Currency is checked within getBatchArgs function - comparePnTypeAndVersion(firstNoConversionRequestExtension, request); - if (!(request.currencyInfo.type === CURRENCY.ERC20)) { - throw new Error(`wrong request currencyInfo type`); - } - ERC20NoConversionRequestDetails.push(getInputERC20NoConversionRequestDetail(request)); - } + const { input, extension } = computeRequestDetails({ + enrichedRequest, + extension: requestExtensions[enrichedRequest.paymentNetworkId], + }); + requestDetails[enrichedRequest.paymentNetworkId].push(input); + requestExtensions[enrichedRequest.paymentNetworkId] = extension; + if (network !== getPnAndNetwork(request).network) throw new Error('All the requests must have the same network'); } - const metaDetails: PaymentTypes.MetaDetail[] = []; - if (ERC20ConversionRequestDetails.length > 0) { - // Add ERC20 conversion payments - metaDetails.push({ - paymentNetworkId: PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: ERC20ConversionRequestDetails, - }); - } - - if (ERC20NoConversionRequestDetails.length > 0) { - // Add multi ERC20 no-conversion payments - metaDetails.push({ - paymentNetworkId: PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: ERC20NoConversionRequestDetails, - }); - } + /** + * The native with conversion payment inputs must be the last element. + * See BatchConversionPayment batchPayments method in @requestnetwork/smart-contracts + */ + const metaDetails = Object.entries(requestDetails) + .map(([pn, details]) => ({ + paymentNetworkId: mapPnToBatchId[pn as BatchPaymentNetworks], + requestDetails: details, + })) + .filter((details) => details.requestDetails.length > 0) + .sort((a, b) => a.paymentNetworkId - b.paymentNetworkId); + + const hasNativePayment = + requestDetails['pn-any-to-eth-proxy'].length > 0 || + requestDetails['pn-eth-fee-proxy-contract'].length > 0; const pathsToUSD = getUSDPathsForFeeLimit( - [...ERC20ConversionRequestDetails, ...ERC20NoConversionRequestDetails], + [...metaDetails.map((details) => details.requestDetails).flat()], network, skipFeeUSDLimit, conversion.currencyManager, + hasNativePayment, ); const proxyContract = BatchConversionPayments__factory.createInterface(); @@ -178,15 +230,27 @@ function encodePayBatchConversionRequest( } /** - * Get the ERC20 no conversion input requestDetail from a request, that can be used by the batch contract. - * @param request The request to pay. + * Get the batch input associated to a request without conversion. + * @param enrichedRequest The enrichedRequest to pay. */ -function getInputERC20NoConversionRequestDetail( - request: ClientTypes.IRequestData, +function getRequestDetailWithoutConversion( + enrichedRequest: EnrichedRequest, + isNative: boolean, ): PaymentTypes.RequestDetail { - validateErc20FeeProxyRequest(request); - - const tokenAddress = request.currencyInfo.value; + const request = enrichedRequest.request; + isNative ? validateEthFeeProxyRequest(request) : validateErc20FeeProxyRequest(request); + + const currencyManager = + enrichedRequest.paymentSettings?.currencyManager || CurrencyManager.getDefault(); + const tokenAddress = isNative + ? currencyManager.getNativeCurrency( + RequestLogicTypes.CURRENCY.ETH, + request.currencyInfo.network as string, + )?.hash + : request.currencyInfo.value; + if (!tokenAddress) { + throw new Error('Could not find the request currency'); + } const { paymentReference, paymentAddress, feeAmount } = getRequestPaymentValues(request); return { @@ -201,24 +265,31 @@ function getInputERC20NoConversionRequestDetail( } /** - * Get the ERC20 conversion input requestDetail from an enriched request, that can be used by the batch contract. + * Get the batch input associated to a request with conversion. * @param enrichedRequest The enrichedRequest to pay. */ -function getInputERC20ConversionRequestDetail( +function getRequestDetailWithConversion( enrichedRequest: EnrichedRequest, + isNative: boolean, ): PaymentTypes.RequestDetail { - const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( - enrichedRequest.request, - enrichedRequest.paymentSettings, - ); + const { request, paymentSettings } = enrichedRequest; + const { path, requestCurrency } = ( + isNative ? getConversionPathForEthRequest : getConversionPathForErc20Request + )(request, paymentSettings); - const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( - enrichedRequest.request, - ); + isNative + ? validateEthFeeProxyRequest( + request, + undefined, + undefined, + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + ) + : validateConversionFeeProxyRequest(request, path); - const requestAmount = BigNumber.from(enrichedRequest.request.expectedAmount).sub( - enrichedRequest.request.balance?.balance || 0, - ); + const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = + getRequestPaymentValues(request); + + const requestAmount = BigNumber.from(request.expectedAmount).sub(request.balance?.balance || 0); const padRequestAmount = padAmountForChainlink(requestAmount, requestCurrency); const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency); @@ -228,11 +299,26 @@ function getInputERC20ConversionRequestDetail( path: path, paymentReference: `0x${paymentReference}`, feeAmount: padFeeAmount.toString(), - maxToSpend: enrichedRequest.paymentSettings.maxToSpend.toString(), + maxToSpend: paymentSettings.maxToSpend.toString(), maxRateTimespan: maxRateTimespan || '0', }; } +const getBatchTxValue = (enrichedRequests: EnrichedRequest[]) => { + return enrichedRequests.reduce((prev, curr) => { + if ( + curr.paymentNetworkId !== ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY && + curr.paymentNetworkId !== ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT + ) + return prev; + return prev.add( + curr.paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY + ? curr.paymentSettings.maxToSpend + : getAmountToPay(curr.request), + ); + }, BigNumber.from(0)); +}; + /** * Get the list of conversion paths from tokens to the USD address through currencyManager. * If there is no path to USD for a token, it goes to the next token. @@ -246,12 +332,25 @@ function getUSDPathsForFeeLimit( network: string, skipFeeUSDLimit: boolean, currencyManager: ICurrencyManager, + hasNativePayment: boolean, ): string[][] { if (skipFeeUSDLimit) return []; const USDCurrency = currencyManager.fromSymbol('USD'); if (!USDCurrency) throw 'Cannot find the USD currency information'; + // Native to USD conversion path + let nativeConversionPath: string[] = []; + if (hasNativePayment) { + const nativeCurrencyHash = currencyManager.getNativeCurrency( + RequestLogicTypes.CURRENCY.ETH, + network, + )?.hash; + if (!nativeCurrencyHash) throw 'Cannot find the Native currency information'; + nativeConversionPath = + currencyManager.getConversionPath({ hash: nativeCurrencyHash }, USDCurrency, network) || []; + } + // get a list of unique token addresses const tokenAddresses = requestDetails .map((rd) => rd.path[rd.path.length - 1]) @@ -263,9 +362,10 @@ function getUSDPathsForFeeLimit( .filter((value): value is CurrencyDefinition => !!value); // get all the conversion paths to USD when it exists and return it - return tokenCurrencies + const path = tokenCurrencies .map((t) => currencyManager.getConversionPath(t, USDCurrency, network)) .filter((value): value is string[] => !!value); + return hasNativePayment ? path.concat([nativeConversionPath]) : path; } /** diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index 2afa09f20f..4149f0e6c2 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -1,4 +1,4 @@ -import { IRequestPaymentOptions } from './settings'; +import { IRequestPaymentOptions } from '../types'; import { IPreparedTransaction } from './prepared-transaction'; import { providers, BigNumber } from 'ethers'; import { hasErc20Approval, prepareApproveErc20 } from './erc20'; diff --git a/packages/payment-processor/src/payment/encoder-payment.ts b/packages/payment-processor/src/payment/encoder-payment.ts index 3791b82d9f..bee279094f 100644 --- a/packages/payment-processor/src/payment/encoder-payment.ts +++ b/packages/payment-processor/src/payment/encoder-payment.ts @@ -1,4 +1,4 @@ -import { IRequestPaymentOptions } from './settings'; +import { IRequestPaymentOptions } from '../types'; import { IPreparedTransaction } from './prepared-transaction'; import { providers } from 'ethers'; import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 7487efa31d..0f2218e163 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -20,7 +20,7 @@ import { ICurrencyManager, NearChains } from '@requestnetwork/currency'; import { encodeRequestErc20Approval } from './encoder-approval'; import { encodeRequestPayment } from './encoder-payment'; import { IPreparedTransaction } from './prepared-transaction'; -import { IRequestPaymentOptions } from './settings'; +import { IRequestPaymentOptions } from '../types'; export { INearTransactionCallback } from './utils-near'; export const noConversionNetworks = [ @@ -338,19 +338,3 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { throw new UnsupportedPaymentChain(request.currencyInfo.network); } }; - -/** - * Input of batch conversion payment processor - * It contains requests, paymentSettings, amount and feeAmount. - * Currently, these requests must have the same PN, version, and batchFee - * @dev next step: paymentNetworkId could get more values options to pay Native tokens. - */ -export interface EnrichedRequest { - paymentNetworkId: - | ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY - | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT; - request: ClientTypes.IRequestData; - paymentSettings: IConversionPaymentSettings; - amount?: BigNumberish; - feeAmount?: BigNumberish; -} diff --git a/packages/payment-processor/src/payment/swap-any-to-erc20.ts b/packages/payment-processor/src/payment/swap-any-to-erc20.ts index 5b69b4989a..24e0ef2200 100644 --- a/packages/payment-processor/src/payment/swap-any-to-erc20.ts +++ b/packages/payment-processor/src/payment/swap-any-to-erc20.ts @@ -14,7 +14,7 @@ import { validateConversionFeeProxyRequest, } from './utils'; import { CurrencyManager, EvmChains, UnsupportedCurrencyError } from '@requestnetwork/currency'; -import { IRequestPaymentOptions } from './settings'; +import { IRequestPaymentOptions } from '../types'; import { IPreparedTransaction } from './prepared-transaction'; export { ISwapSettings } from './swap-erc20-fee-proxy'; diff --git a/packages/payment-processor/src/payment/settings.ts b/packages/payment-processor/src/types.ts similarity index 69% rename from packages/payment-processor/src/payment/settings.ts rename to packages/payment-processor/src/types.ts index 8dac067680..c82e94872a 100644 --- a/packages/payment-processor/src/payment/settings.ts +++ b/packages/payment-processor/src/types.ts @@ -1,7 +1,8 @@ import { ICurrencyManager } from '@requestnetwork/currency'; -import { RequestLogicTypes } from '@requestnetwork/types'; +import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import { BigNumber, BigNumberish } from 'ethers'; -import { ITransactionOverrides } from './transaction-overrides'; +import { IConversionPaymentSettings } from './payment'; +import { ITransactionOverrides } from './payment/transaction-overrides'; /** * Approval settings @@ -66,3 +67,23 @@ export interface IRequestPaymentOptions { /** Optional, only for batch payment to define the proxy to use. */ version?: string; } + +export type BatchPaymentNetworks = + | ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + | ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY + | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + | ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT; + +/** + * Input of batch conversion payment processor + * It contains requests, paymentSettings, amount and feeAmount. + * Currently, these requests must have the same PN, version, and batchFee + * @dev next step: paymentNetworkId could get more values options to pay Native tokens. + */ +export interface EnrichedRequest { + paymentNetworkId: BatchPaymentNetworks; + request: ClientTypes.IRequestData; + paymentSettings: IConversionPaymentSettings; + amount?: BigNumberish; + feeAmount?: BigNumberish; +} diff --git a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts index cebfde614b..c957f05ef2 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts +++ b/packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts @@ -205,11 +205,11 @@ describe('conversion-erc20-fee-proxy', () => { it('should convert and pay a request in ETH with ERC20', async () => { const validEthRequest = validEuroRequest; - validEthRequest.currency = 'ETH'; + validEthRequest.currency = 'ETH-private'; validEthRequest.currencyInfo = { type: RequestLogicTypes.CURRENCY.ETH, - value: 'ETH', - network: 'mainnet', + value: 'ETH-private', + network: 'private', }; validEthRequest.expectedAmount = '1000000000000000000'; // 1 ETH validEthRequest.extensions[ diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/batch-proxy.test.ts similarity index 70% rename from packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts rename to packages/payment-processor/test/payment/batch-proxy.test.ts index acd897b2c4..094291f24d 100644 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/batch-proxy.test.ts @@ -8,7 +8,6 @@ import { } from '@requestnetwork/types'; import { approveErc20BatchConversionIfNeeded, - EnrichedRequest, getBatchConversionProxyAddress, getErc20Balance, IConversionPaymentSettings, @@ -19,8 +18,8 @@ import { deepCopy } from '@requestnetwork/utils'; import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; -import { IRequestPaymentOptions } from 'payment-processor/src/payment/settings'; import { CurrencyTypes } from '@requestnetwork/types/src'; +import { EnrichedRequest, IRequestPaymentOptions } from 'payment-processor/src/types'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -55,14 +54,24 @@ const currency: RequestLogicTypes.ICurrency = { value: DAITokenAddress, network: 'private', }; -const paymentSettings: IConversionPaymentSettings = { + +const nativeCurrency: RequestLogicTypes.ICurrency = { + type: RequestLogicTypes.CURRENCY.ETH, + value: 'ETH-private', + network: 'private', +}; + +const conversionPaymentSettings: IConversionPaymentSettings = { currency: currency, maxToSpend: '10000000000000000000000000000', currencyManager: currencyManager, }; -const conversionPaymentSettings = deepCopy(paymentSettings); -// conversionPaymentSettings.currencyManager = undefined; +const ethConversionPaymentSettings: IConversionPaymentSettings = { + currency: nativeCurrency, + maxToSpend: '200000000000000000000', + currencyManager: currencyManager, +}; const options: IRequestPaymentOptions = { conversion: { @@ -80,7 +89,7 @@ const EURFeeAmount = 2_00; // 2 € const expectedAmount = 100000; const feeAmount = 100; -const EURValidRequest: ClientTypes.IRequestData = { +const EURToERC20ValidRequest: ClientTypes.IRequestData = { balance: { balance: '0', events: [], @@ -135,11 +144,7 @@ const DAIValidRequest: ClientTypes.IRequestData = { value: wallet.address, }, currency: 'DAI', - currencyInfo: { - network: 'private', - type: RequestLogicTypes.CURRENCY.ERC20 as any, - value: DAITokenAddress, - }, + currencyInfo: currency, events: [], expectedAmount: expectedAmount, extensions: { @@ -167,6 +172,46 @@ const DAIValidRequest: ClientTypes.IRequestData = { version: '1.0', }; +const EURToETHValidRequest: ClientTypes.IRequestData = { + ...EURToERC20ValidRequest, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: EURFeeAmount, + paymentAddress, + salt: 'salt', + network: 'private', + }, + version: '0.1.0', + }, + }, +}; + +const ETHValidRequest: ClientTypes.IRequestData = { + ...DAIValidRequest, + currency: 'ETH-private', + currencyInfo: nativeCurrency, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress, + salt: 'salt', + network: 'private', + }, + version: '0.1.0', + }, + }, +}; + const FAUValidRequest = deepCopy(DAIValidRequest) as ClientTypes.IRequestData; FAUValidRequest.currencyInfo = { network: 'private', @@ -183,15 +228,22 @@ let FAURequest: ClientTypes.IRequestData; * Calcul the expected amount to pay for X euro into Y tokens * @param amount in fiat: EUR */ -const expectedConversionAmount = (amount: number): BigNumber => { +const expectedConversionAmount = (amount: number, isNative?: boolean): BigNumber => { // token decimals 10**18 // amount amount / 100 // AggEurUsd.sol x 1.20 - // AggDaiUsd.sol / 1.01 - return BigNumber.from(10).pow(18).mul(amount).div(100).mul(120).div(100).mul(100).div(101); + // AggDaiUsd.sol / 1.01 OR AggEthUsd.sol / 500 + return BigNumber.from(10) + .pow(18) + .mul(amount) + .div(100) + .mul(120) + .div(100) + .mul(100) + .div(isNative ? 50000 : 101); }; -describe('erc20-batch-conversion-proxy', () => { +describe('batch-proxy', () => { beforeAll(async () => { // Revoke DAI and FAU approvals await revokeErc20Approval( @@ -207,7 +259,7 @@ describe('erc20-batch-conversion-proxy', () => { // Approve the contract to spent DAI with a conversion request const approvalTx = await approveErc20BatchConversionIfNeeded( - EURValidRequest, + EURToERC20ValidRequest, wallet.address, wallet.provider, undefined, @@ -222,11 +274,11 @@ describe('erc20-batch-conversion-proxy', () => { describe(`Conversion:`, () => { beforeEach(() => { jest.restoreAllMocks(); - EURRequest = deepCopy(EURValidRequest); + EURRequest = deepCopy(EURToERC20ValidRequest); enrichedRequests = [ { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }, { @@ -244,7 +296,7 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: { ...conversionPaymentSettings, currency: { @@ -281,12 +333,6 @@ describe('erc20-batch-conversion-proxy', () => { ), ).rejects.toThrowError('currency must be provided in the paymentSettings'); }); - it('should throw an error if the request is ETH', async () => { - EURRequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - await expect( - payBatchConversionProxyRequest(enrichedRequests, wallet, options), - ).rejects.toThrowError(`wrong request currencyInfo type`); - }); it('should throw an error if the request has a wrong network', async () => { EURRequest.extensions = { // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY @@ -320,6 +366,7 @@ describe('erc20-batch-conversion-proxy', () => { feeAddress, feeAmount: feeAmount, paymentAddress: paymentAddress, + network: 'private', salt: 'salt', }, version: '0.1.0', @@ -366,7 +413,7 @@ describe('erc20-batch-conversion-proxy', () => { [ { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }, ], @@ -377,11 +424,12 @@ describe('erc20-batch-conversion-proxy', () => { expect(spy).toHaveBeenCalledWith({ data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000500918bd80000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000bebc2000000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', - to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'), - value: 0, + to: getBatchConversionProxyAddress(EURToERC20ValidRequest, '0.1.0'), + value: BigNumber.from(0), }); wallet.sendTransaction = originalSendTransaction; }); + for (const skipFeeUSDLimit of ['true', 'false']) { it(`should convert and pay a request in EUR with ERC20, ${ skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit' @@ -394,20 +442,20 @@ describe('erc20-batch-conversion-proxy', () => { provider, ); - options.skipFeeUSDLimit = skipFeeUSDLimit === 'true'; - // Convert and pay const tx = await payBatchConversionProxyRequest( [ { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }, ], wallet, - options, + { + ...options, + skipFeeUSDLimit: skipFeeUSDLimit === 'true', + }, ); - options.skipFeeUSDLimit = true; const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); @@ -430,16 +478,70 @@ describe('erc20-batch-conversion-proxy', () => { expect( BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)), // Calculation of expectedAmountToPay when there there is no fee USD limit - // expectedAmount: 1.00 - // feeAmount: + .02 - // = 1.02 + // expectedAmount: 55 000 + // feeAmount: + 2 // AggEurUsd.sol x 1.20 // AggDaiUsd.sol / 1.01 // BATCH_CONV_FEE x 1.003 - // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) ).toEqual(expectedAmountToPay); }); + it(`should convert and pay a request in EUR with ETH, ${ + skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit' + } `, async () => { + const fromOldBalance = await provider.getBalance(wallet.address); + const toOldBalance = await provider.getBalance(paymentAddress); + const feeOldBalance = await provider.getBalance(feeAddress); + + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + request: EURToETHValidRequest, + paymentSettings: ethConversionPaymentSettings, + }, + ], + wallet, + { + ...options, + skipFeeUSDLimit: skipFeeUSDLimit === 'true', + }, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + + // Get the new balances + const fromNewBalance = await provider.getBalance(wallet.address); + const toNewBalance = await provider.getBalance(paymentAddress); + const feeNewBalance = await provider.getBalance(feeAddress); + const gasPrice = confirmedTx.effectiveGasPrice; + + const amountToPay = expectedConversionAmount(EURExpectedAmount, true); + const feeToPay = expectedConversionAmount(EURFeeAmount, true); + const totalFeeToPay = + skipFeeUSDLimit === 'true' + ? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR).add(feeToPay) + : // Capped fee total: + // 2 (Fees in €) + // x 1.20 + // + 150 (Batch Fees capped to 150$) + // / 500 + BigNumber.from('304800000000000000'); + const expectedAmountToPay = amountToPay.add(totalFeeToPay); + + expect( + fromOldBalance + .sub(fromNewBalance) + .sub(confirmedTx.cumulativeGasUsed.mul(gasPrice)) + .toString(), + ).toEqual(expectedAmountToPay.toString()); + + expect(feeNewBalance.sub(feeOldBalance).toString()).toEqual(totalFeeToPay.toString()); + + expect(toNewBalance.sub(toOldBalance).toString()).toEqual(amountToPay.toString()); + }); } + it('should convert and pay two requests in EUR with ERC20', async () => { // Get initial balances const initialETHFromBalance = await wallet.getBalance(); @@ -452,7 +554,7 @@ describe('erc20-batch-conversion-proxy', () => { const tx = await payBatchConversionProxyRequest( Array(2).fill({ paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }), wallet, @@ -482,26 +584,72 @@ describe('erc20-batch-conversion-proxy', () => { expectedAmoutToPay, ); }); - it('should pay 3 heterogeneous ERC20 payments with and without conversion', async () => { - // Get initial balances - const initialETHFromBalance = await wallet.getBalance(); - const initialDAIFromBalance = await getErc20Balance( - DAIValidRequest, - wallet.address, - provider, + + it('should convert and pay two requests in EUR with ETH', async () => { + const fromOldBalance = await provider.getBalance(wallet.address); + const toOldBalance = await provider.getBalance(paymentAddress); + const feeOldBalance = await provider.getBalance(feeAddress); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + Array(2).fill({ + paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + request: EURToETHValidRequest, + paymentSettings: ethConversionPaymentSettings, + }), + wallet, + options, ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + + // Get the new balances + const fromNewBalance = await provider.getBalance(wallet.address); + const toNewBalance = await provider.getBalance(paymentAddress); + const feeNewBalance = await provider.getBalance(feeAddress); + const gasPrice = confirmedTx.effectiveGasPrice; + + const amountToPay = expectedConversionAmount(EURExpectedAmount, true).mul(2); + const feeToPay = expectedConversionAmount(EURFeeAmount, true).mul(2); + const totalFeeToPay = amountToPay + .add(feeToPay) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + .add(feeToPay); + const expectedAmountToPay = amountToPay.add(totalFeeToPay); + + expect( + fromOldBalance + .sub(fromNewBalance) + .sub(confirmedTx.cumulativeGasUsed.mul(gasPrice)) + .toString(), + ).toEqual(expectedAmountToPay.toString()); + + expect(feeNewBalance.sub(feeOldBalance).toString()).toEqual(totalFeeToPay.toString()); + + expect(toNewBalance.sub(toOldBalance).toString()).toEqual(amountToPay.toString()); + }); + + it('should pay heterogeneous payments (ETH/ERC20 with/without conversion)', async () => { + const fromOldBalanceETH = await provider.getBalance(wallet.address); + const toOldBalanceETH = await provider.getBalance(paymentAddress); + const feeOldBalanceETH = await provider.getBalance(feeAddress); + const fromOldBalanceDAI = await getErc20Balance(DAIValidRequest, wallet.address, provider); + const toOldBalanceDAI = await getErc20Balance(DAIValidRequest, paymentAddress, provider); + const feeOldBalanceDAI = await getErc20Balance(DAIValidRequest, feeAddress, provider); // Convert the two first requests and pay the three requests const tx = await payBatchConversionProxyRequest( [ { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }, { paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - request: EURValidRequest, + request: EURToERC20ValidRequest, paymentSettings: conversionPaymentSettings, }, { @@ -509,6 +657,19 @@ describe('erc20-batch-conversion-proxy', () => { request: DAIValidRequest, paymentSettings: { maxToSpend: '0' }, }, + { + paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT, + request: ETHValidRequest, + paymentSettings: { + ...ethConversionPaymentSettings, + maxToSpend: '0', + }, + }, + { + paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + request: EURToETHValidRequest, + paymentSettings: ethConversionPaymentSettings, + }, ], wallet, options, @@ -517,31 +678,64 @@ describe('erc20-batch-conversion-proxy', () => { expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); - // Get balances - const ETHFromBalance = await wallet.getBalance(); - const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); - - // Checks ETH balances + const gasPrice = confirmedTx.effectiveGasPrice; + + const fromNewBalanceETH = await provider.getBalance(wallet.address); + const toNewBalanceETH = await provider.getBalance(paymentAddress); + const feeNewBalanceETH = await provider.getBalance(feeAddress); + const fromNewBalanceDAI = await getErc20Balance(DAIValidRequest, wallet.address, provider); + const toNewBalanceDAI = await getErc20Balance(DAIValidRequest, paymentAddress, provider); + const feeNewBalanceDAI = await getErc20Balance(DAIValidRequest, feeAddress, provider); + + // Computes amount related to DAI with conversion payments + const DAIConvAmount = expectedConversionAmount(EURExpectedAmount).mul(2); + const DAIConvFeeAmount = expectedConversionAmount(EURFeeAmount).mul(2); + const DAIConvTotalFees = DAIConvAmount.add(DAIConvFeeAmount) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + .add(DAIConvFeeAmount); + const DAIConvTotal = DAIConvAmount.add(DAIConvTotalFees); + + // Computes amount related to payments without conversion (same for ETH or ERC20) + const NoConvAmount = BigNumber.from(expectedAmount); + const NoConvFeeAmount = BigNumber.from(feeAmount); + const NoConvTotalFees = NoConvAmount.add(NoConvFeeAmount) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + .add(NoConvFeeAmount); + const NoConvTotal = NoConvAmount.add(NoConvTotalFees); + + // Computes amount related to ETH with conversion payments + const ETHConvAmount = expectedConversionAmount(EURExpectedAmount, true); + const ETHConvFeeAmount = expectedConversionAmount(EURFeeAmount, true); + const ETHConvTotalFees = ETHConvAmount.add(ETHConvFeeAmount) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + .add(ETHConvFeeAmount); + const ETHConvTotal = ETHConvAmount.add(ETHConvTotalFees); + + // Totals + const DAIAmount = DAIConvAmount.add(NoConvAmount); + const DAIFeesTotal = DAIConvTotalFees.add(NoConvTotalFees); + const DAITotal = DAIConvTotal.add(NoConvTotal); + const ETHAmount = ETHConvAmount.add(NoConvAmount); + const ETHFeesTotal = ETHConvTotalFees.add(NoConvTotalFees); + const ETHTotal = ETHConvTotal.add(NoConvTotal); + + // DAI Checks + expect(BigNumber.from(fromOldBalanceDAI).sub(fromNewBalanceDAI)).toEqual(DAITotal); + expect(BigNumber.from(toNewBalanceDAI).sub(toOldBalanceDAI)).toEqual(DAIAmount); + expect(BigNumber.from(feeNewBalanceDAI).sub(feeOldBalanceDAI)).toEqual(DAIFeesTotal); + + // ETH Checks expect( - BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), - ).toBeGreaterThan(0); - - // Checks DAI balances - let expectedConvAmountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of conversion requests: 2 - const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of conversion requests: 2 - // expectedConvAmountToPay with fees and batch fees - expectedConvAmountToPay = expectedConvAmountToPay - .add(feeToPay) - .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) - .div(BATCH_DENOMINATOR); - const expectedNoConvAmountToPay = BigNumber.from(DAIValidRequest.expectedAmount) - .add(feeAmount) - .mul(BATCH_DENOMINATOR + BATCH_FEE) - .div(BATCH_DENOMINATOR); - - expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( - expectedConvAmountToPay.add(expectedNoConvAmountToPay), - ); + fromOldBalanceETH + .sub(fromNewBalanceETH) + .sub(confirmedTx.cumulativeGasUsed.mul(gasPrice)) + .toString(), + ).toEqual(ETHTotal.toString()); + expect(toNewBalanceETH.sub(toOldBalanceETH)).toEqual(ETHAmount); + expect(feeNewBalanceETH.sub(feeOldBalanceETH).toString()).toEqual(ETHFeesTotal.toString()); }); }); }); @@ -623,11 +817,11 @@ describe('erc20-batch-conversion-proxy', () => { data: '0x92cddb9100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(DAIValidRequest, '0.1.0'), - value: 0, + value: BigNumber.from(0), }); wallet.sendTransaction = originalSendTransaction; }); - it(`should pay 2 differents ERC20 requests with fees`, async () => { + it(`should pay 2 different ERC20 requests with fees`, async () => { // Approve the contract for DAI and FAU tokens const FAUApprovalTx = await approveErc20BatchConversionIfNeeded( FAUValidRequest, @@ -722,6 +916,7 @@ describe('erc20-batch-conversion-proxy', () => { } as any, } as EnrichedRequest, { + paymentNetworkId: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, request: { ...FAUValidRequest, extensions: { diff --git a/packages/payment-processor/test/payment/encoder-approval.test.ts b/packages/payment-processor/test/payment/encoder-approval.test.ts index e7a95dabde..69f6d606ba 100644 --- a/packages/payment-processor/test/payment/encoder-approval.test.ts +++ b/packages/payment-processor/test/payment/encoder-approval.test.ts @@ -14,7 +14,7 @@ import { erc20SwapConversionArtifact, erc20SwapToPayArtifact, } from '@requestnetwork/smart-contracts'; -import { IConversionSettings } from '../../src/payment/settings'; +import { IConversionSettings } from '../../src/types'; /* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable @typescript-eslint/await-thenable */ @@ -158,7 +158,7 @@ const validRequestERC20ConversionProxy: ClientTypes.IRequestData = { const validRequestEthProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, @@ -181,7 +181,7 @@ const validRequestEthProxy: ClientTypes.IRequestData = { const validRequestEthFeeProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/encoder-payment.test.ts b/packages/payment-processor/test/payment/encoder-payment.test.ts index d2a85f9f32..58f1ceed1d 100644 --- a/packages/payment-processor/test/payment/encoder-payment.test.ts +++ b/packages/payment-processor/test/payment/encoder-payment.test.ts @@ -24,7 +24,7 @@ import { erc20SwapConversionArtifact, erc20SwapToPayArtifact, } from '@requestnetwork/smart-contracts'; -import { IConversionSettings } from '../../src/payment/settings'; +import { IConversionSettings } from '../../src/types'; /* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable @typescript-eslint/await-thenable */ @@ -47,7 +47,7 @@ const alphaConversionSettings: IConversionSettings = { const ethConversionSettings = { currency: { type: RequestLogicTypes.CURRENCY.ETH, - value: 'ETH', + value: 'ETH-private', }, maxToSpend: '2500000000000000', currencyManager, @@ -181,7 +181,7 @@ const validRequestERC20ConversionProxy: ClientTypes.IRequestData = { const validRequestEthProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, @@ -204,7 +204,7 @@ const validRequestEthProxy: ClientTypes.IRequestData = { const validRequestEthFeeProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, @@ -406,7 +406,7 @@ describe('Payment encoder handles Eth Conversion Proxy', () => { ); expect(paymentTransaction).toEqual({ - data: '0xac473c8a000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b000000000000000000000000f5af88e117747e87fc5929f2ff87221b1447652e000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + data: '0xac473c8a000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b000000000000000000000000a65ded58a0afee8241e788c5115ca53ef3925fd2000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', to: proxyAddress, value: BigNumber.from(ethConversionSettings.maxToSpend), }); diff --git a/packages/payment-processor/test/payment/encoder.test.ts b/packages/payment-processor/test/payment/encoder.test.ts index b4772a0d92..d5b810b448 100644 --- a/packages/payment-processor/test/payment/encoder.test.ts +++ b/packages/payment-processor/test/payment/encoder.test.ts @@ -6,11 +6,7 @@ import { RequestLogicTypes, } from '@requestnetwork/types'; import { encodeRequestApprovalAndPayment } from '../../src'; -import { - IApprovalSettings, - IConversionSettings, - IRequestPaymentOptions, -} from '../../src/payment/settings'; +import { IApprovalSettings, IConversionSettings, IRequestPaymentOptions } from '../../src/types'; import { currencyManager } from './shared'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { MAX_ALLOWANCE } from '../../src/payment/utils'; @@ -35,7 +31,7 @@ const alphaConversionSettings: IConversionSettings = { const ethConversionSettings = { currency: { type: RequestLogicTypes.CURRENCY.ETH, - value: 'ETH', + value: 'ETH-private', }, maxToSpend: '2500000000000000', currencyManager, @@ -144,7 +140,7 @@ const validRequestERC20ConversionProxy: ClientTypes.IRequestData = { const validRequestEthProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, @@ -167,7 +163,7 @@ const validRequestEthProxy: ClientTypes.IRequestData = { const validRequestEthFeeProxy: ClientTypes.IRequestData = { ...baseValidRequest, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/eth-batch-proxy.test.ts b/packages/payment-processor/test/payment/eth-batch-proxy.test.ts index 20fd0df5f4..3123bd22a2 100644 --- a/packages/payment-processor/test/payment/eth-batch-proxy.test.ts +++ b/packages/payment-processor/test/payment/eth-batch-proxy.test.ts @@ -34,7 +34,7 @@ const validRequest: ClientTypes.IRequestData = { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: wallet.address, }, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/eth-fee-proxy.test.ts b/packages/payment-processor/test/payment/eth-fee-proxy.test.ts index 812bfca72a..260cb503d3 100644 --- a/packages/payment-processor/test/payment/eth-fee-proxy.test.ts +++ b/packages/payment-processor/test/payment/eth-fee-proxy.test.ts @@ -32,7 +32,7 @@ const validRequest: ClientTypes.IRequestData = { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: wallet.address, }, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/eth-input-data.test.ts b/packages/payment-processor/test/payment/eth-input-data.test.ts index ea7b18c369..0d38f1c37e 100644 --- a/packages/payment-processor/test/payment/eth-input-data.test.ts +++ b/packages/payment-processor/test/payment/eth-input-data.test.ts @@ -30,7 +30,7 @@ const validRequest: ClientTypes.IRequestData = { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: wallet.address, }, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/eth-proxy.test.ts b/packages/payment-processor/test/payment/eth-proxy.test.ts index e620ef284f..5a85a1842f 100644 --- a/packages/payment-processor/test/payment/eth-proxy.test.ts +++ b/packages/payment-processor/test/payment/eth-proxy.test.ts @@ -35,7 +35,7 @@ const validRequest: ClientTypes.IRequestData = { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: wallet.address, }, - currency: 'ETH', + currency: 'ETH-private', currencyInfo: { network: 'private', type: RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/payment-processor/test/payment/shared.ts b/packages/payment-processor/test/payment/shared.ts index b2a61dce98..6ebc6f1726 100644 --- a/packages/payment-processor/test/payment/shared.ts +++ b/packages/payment-processor/test/payment/shared.ts @@ -3,12 +3,6 @@ import { RequestLogicTypes } from '@requestnetwork/types'; export const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), - { - network: 'private', - symbol: 'ETH', - decimals: 18, - type: RequestLogicTypes.CURRENCY.ETH, - } as CurrencyDefinition, ...[ '0x9FBDa871d559710256a2502A2517b794B482Db40', '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35', diff --git a/packages/payment-processor/test/payment/swap-any-to-erc20.test.ts b/packages/payment-processor/test/payment/swap-any-to-erc20.test.ts index bf54913f1d..5f290ccb99 100644 --- a/packages/payment-processor/test/payment/swap-any-to-erc20.test.ts +++ b/packages/payment-processor/test/payment/swap-any-to-erc20.test.ts @@ -11,7 +11,7 @@ import { deepCopy } from '@requestnetwork/utils'; import { approveErc20ForSwapWithConversionIfNeeded } from '../../src/payment/swap-conversion-erc20'; import { ERC20, ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { swapToPayAnyToErc20Request } from '../../src/payment/swap-any-to-erc20'; -import { IConversionSettings } from '../../src/payment/settings'; +import { IConversionSettings } from '../../src/types'; import { currencyManager } from './shared'; import { UnsupportedCurrencyError } from '@requestnetwork/currency'; diff --git a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts index 5bd0b1f3a5..28db587659 100644 --- a/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts +++ b/packages/payment-processor/test/payment/swap-erc20-fee-proxy.test.ts @@ -197,7 +197,7 @@ describe('swap-erc20-fee-proxy', () => { // Swap and pay const tx = await swapErc20FeeProxyRequest(validRequest, wallet, { - deadline: Date.now() + 10000, + deadline: Date.now() + 1000000, maxInputAmount: 206, path: [alphaErc20Address, erc20ContractAddress], }); diff --git a/packages/smart-contracts/scripts/conversion-proxy.ts b/packages/smart-contracts/scripts/conversion-proxy.ts index 287cd12647..a52b4ef9e8 100644 --- a/packages/smart-contracts/scripts/conversion-proxy.ts +++ b/packages/smart-contracts/scripts/conversion-proxy.ts @@ -70,11 +70,9 @@ export async function deployEthConversionProxy( return; } - // The private native token hash is the same as on mainnet - const nativeTokenNetwork = hre.network.name === 'private' ? 'mainnet' : hre.network.name; const nativeTokenHash = CurrencyManager.getDefault().getNativeCurrency( RequestLogicTypes.CURRENCY.ETH, - nativeTokenNetwork, + hre.network.name, )?.hash; if (!nativeTokenHash) { console.error( diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts index cff4bfef7e..2cb168930f 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -82,7 +82,7 @@ export async function deployBatchConversionPayment( await batchConversion .connect(owner) .setNativeAndUSDAddress( - currencyManager.fromSymbol('ETH')!.hash, + currencyManager.fromSymbol('ETH-private')!.hash, currencyManager.fromSymbol('USD')!.hash, ); await batchConversion.connect(owner).setBatchFeeAmountUSDLimit(150 * 1e8); // 150$ diff --git a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts index be32fc4e61..406bb4ee57 100644 --- a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts +++ b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts @@ -34,7 +34,7 @@ export default async function deploy( const currencyManager = CurrencyManager.getDefault(); // all these addresses are for test purposes - const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH-private')!.hash; const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 8e74d5f268..c2b630c61e 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -52,7 +52,7 @@ describe('contract: BatchConversionPayments', async () => { // constants related to chainlink and conversion rate const currencyManager = CurrencyManager.getDefault(); - const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH-private')!.hash; const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; const DAI_address = localERC20AlphaArtifact.getAddress(network.name); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index e3d977802b..8242ec143e 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -77,7 +77,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await batch.connect(owner).setBatchFee(100); await batch.setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); // 1$ await batch.setNativeAndUSDAddress( - currencyManager.fromSymbol('ETH')!.hash, + currencyManager.fromSymbol('ETH-private')!.hash, currencyManager.fromSymbol('USD')!.hash, ); ethRequestDetail1.recipient = payee1; diff --git a/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts b/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts index faf6f2de35..1a8806e68b 100644 --- a/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts +++ b/packages/smart-contracts/test/contracts/ChainlinkConversionPath.test.ts @@ -15,7 +15,7 @@ const address5 = '0x5555555555555555555555555555555555555555'; const address6 = '0x6666666666666666666666666666666666666666'; const currencyManager = CurrencyManager.getDefault(); -const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; +const ETH_hash = currencyManager.fromSymbol('ETH-private')!.hash; const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; let DAI_address: string; diff --git a/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts b/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts index b0ccaba8b5..68d8b38891 100644 --- a/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts +++ b/packages/smart-contracts/test/contracts/Erc20ConversionProxy.test.ts @@ -30,7 +30,7 @@ describe('contract: Erc20ConversionProxy', () => { const currencyManager = CurrencyManager.getDefault(); - const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH-private')!.hash; const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; let DAI_address: string; diff --git a/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts b/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts index 0a1fcb9a05..e902d86815 100644 --- a/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthConversionProxy.test.ts @@ -32,7 +32,7 @@ describe('contract: EthConversionProxy', () => { const currencyManager = CurrencyManager.getDefault(); - const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const ETH_hash = currencyManager.fromSymbol('ETH-private')!.hash; const USD_hash = currencyManager.fromSymbol('USD')!.hash; const EUR_hash = currencyManager.fromSymbol('EUR')!.hash;