Skip to content

Commit fc6887f

Browse files
committed
batch conversion payment processor functions and tests
1 parent 43baa37 commit fc6887f

File tree

3 files changed

+116
-99
lines changed

3 files changed

+116
-99
lines changed

packages/payment-processor/src/payment/batch-conversion-proxy.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const currencyManager = CurrencyManager.getDefault();
3434

3535
/**
3636
* Processes a transaction to pay a batch of requests with an ERC20 currency
37-
* that is different from the request currency (eg. fiat)
37+
* that can be different from the request currency (eg. fiat)
3838
* The payment is made through ERC20 or ERC20Conversion proxies
3939
* It can be used with a Multisig contract
4040
* @param enrichedRequests List of EnrichedRequests to pay
@@ -102,13 +102,14 @@ export function encodePayBatchConversionRequest(
102102
): string {
103103
const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request);
104104

105-
const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1];
105+
const network = getPnAndNetwork(enrichedRequests[0].request)[1];
106106
let firstConversionRequestExtension: IState<any> | undefined;
107107
let firstNoConversionRequestExtension: IState<any> | undefined;
108-
const requestDetailsERC20NoConversion: PaymentTypes.RequestDetail[] = [];
109-
const requestDetailsERC20Conversion: PaymentTypes.RequestDetail[] = [];
110108

111-
// fill requestDetailsERC20Conversion and requestDetailsERC20NoConversion lists
109+
const ERC20NoConversionRequestDetails: PaymentTypes.RequestDetail[] = [];
110+
const ERC20ConversionRequestDetails: PaymentTypes.RequestDetail[] = [];
111+
112+
// fill ERC20ConversionRequestDetails and ERC20NoConversionRequestDetails lists
112113
for (const enrichedRequest of enrichedRequests) {
113114
if (
114115
enrichedRequest.paymentNetworkId ===
@@ -126,7 +127,7 @@ export function encodePayBatchConversionRequest(
126127
) {
127128
throw new Error(`wrong request currencyInfo type`);
128129
}
129-
requestDetailsERC20Conversion.push(getInputRequestDetailERC20Conversion(enrichedRequest));
130+
ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest));
130131
} else if (
131132
enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS
132133
) {
@@ -135,42 +136,44 @@ export function encodePayBatchConversionRequest(
135136

136137
// isERC20Currency is checked within getBatchArgs function
137138
comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request);
138-
requestDetailsERC20NoConversion.push(
139-
getInputRequestDetailERC20NoConversion(enrichedRequest.request),
139+
if (!isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput)) {
140+
throw new Error(`wrong request currencyInfo type`);
141+
}
142+
ERC20NoConversionRequestDetails.push(
143+
getInputERC20NoConversionRequestDetail(enrichedRequest.request),
140144
);
141145
}
142-
if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1])
146+
if (network !== getPnAndNetwork(enrichedRequest.request)[1])
143147
throw new Error('All the requests must have the same network');
144148
}
145149

146150
const metaDetails: PaymentTypes.MetaDetail[] = [];
147-
// Add requestDetailsERC20Conversion to metaDetails
148-
if (requestDetailsERC20Conversion.length > 0) {
151+
if (ERC20ConversionRequestDetails.length > 0) {
152+
// Add ERC20 conversion payments
149153
metaDetails.push({
150154
paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS,
151-
requestDetails: requestDetailsERC20Conversion,
155+
requestDetails: ERC20ConversionRequestDetails,
152156
});
153157
}
154158

155-
// Add cryptoDetails to metaDetails
156-
if (requestDetailsERC20NoConversion.length > 0) {
157-
// add ERC20 no-conversion payments
159+
if (ERC20NoConversionRequestDetails.length > 0) {
160+
// Add multi ERC20 no-conversion payments
158161
metaDetails.push({
159162
paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS,
160-
requestDetails: requestDetailsERC20NoConversion,
163+
requestDetails: ERC20NoConversionRequestDetails,
161164
});
162165
}
163166

164167
const pathsToUSD = getPathsToUSD(
165-
[...requestDetailsERC20Conversion, ...requestDetailsERC20NoConversion],
166-
firstNetwork,
168+
[...ERC20ConversionRequestDetails, ...ERC20NoConversionRequestDetails],
169+
network,
167170
skipFeeUSDLimit,
168171
);
169172

170173
const proxyContract = BatchConversionPayments__factory.createInterface();
171174
return proxyContract.encodeFunctionData('batchPayments', [
172175
metaDetails,
173-
skipFeeUSDLimit ? [] : pathsToUSD,
176+
pathsToUSD,
174177
feeAddress || constants.AddressZero,
175178
]);
176179
}
@@ -179,7 +182,7 @@ export function encodePayBatchConversionRequest(
179182
* Get the ERC20 no conversion input requestDetail from a request, that can be used by the batch contract.
180183
* @param request The request to pay.
181184
*/
182-
function getInputRequestDetailERC20NoConversion(
185+
function getInputERC20NoConversionRequestDetail(
183186
request: ClientTypes.IRequestData,
184187
): PaymentTypes.RequestDetail {
185188
validateErc20FeeProxyRequest(request);
@@ -202,7 +205,7 @@ function getInputRequestDetailERC20NoConversion(
202205
* Get the ERC20 conversion input requestDetail from an enriched request, that can be used by the batch contract.
203206
* @param enrichedRequest The enrichedRequest to pay.
204207
*/
205-
function getInputRequestDetailERC20Conversion(
208+
function getInputERC20ConversionRequestDetail(
206209
enrichedRequest: EnrichedRequest,
207210
): PaymentTypes.RequestDetail {
208211
const paymentSettings = enrichedRequest.paymentSettings;
@@ -247,18 +250,19 @@ function getPathsToUSD(
247250
network: string,
248251
skipFeeUSDLimit: boolean,
249252
): string[][] {
250-
const pathsToUSD: Array<string>[] = [];
253+
const pathsToUSD: Array<Array<string>> = [];
251254
if (!skipFeeUSDLimit) {
252255
const USDCurrency = currencyManager.fromSymbol('USD');
256+
253257
// token's addresses paid with the batch
254258
const tokenAddresses: Array<string> = [];
255259
for (const requestDetail of requestDetails) {
256260
const tokenAddress = requestDetail.path[requestDetail.path.length - 1];
257-
// Check token to only unique paths token to USD.
258-
if (!tokenAddresses.includes(tokenAddress)) {
261+
// Create a list of unique paths: token to USD.
262+
if (USDCurrency && !tokenAddresses.includes(tokenAddress)) {
259263
tokenAddresses.push(tokenAddress);
260-
const tokenCurrency = currencyManager.fromAddress(tokenAddress);
261-
const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency!, network);
264+
const tokenCurrency = currencyManager.fromAddress(tokenAddress, network);
265+
const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency, network);
262266
if (pathToUSD) {
263267
pathsToUSD.push(pathToUSD);
264268
}

packages/payment-processor/src/payment/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => {
333333
* Input of batch conversion payment processor
334334
* It contains requests, paymentSettings, amount and feeAmount.
335335
* Currently, these requests must have the same PN, version, and batchFee
336-
* Also used in Invoicing repository.
337336
* @dev next step: paymentNetworkId could get more values options, see the "ref"
338337
* in batchConversionPayment.sol
339338
*/

packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts

Lines changed: 86 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,28 @@ import { getErc20Balance } from '../../src/payment/erc20';
1111
import Utils from '@requestnetwork/utils';
1212
import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils';
1313
import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index';
14-
import { currencyManager } from './shared';
14+
// import { currencyManager } from './shared';
1515
import {
1616
approveErc20BatchConversionIfNeeded,
1717
getBatchConversionProxyAddress,
1818
payBatchConversionProxyRequest,
1919
prepareBatchConversionPaymentTransaction,
2020
} from '../../src/payment/batch-conversion-proxy';
2121
import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts';
22-
import { UnsupportedCurrencyError } from '@requestnetwork/currency';
22+
import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency';
2323
import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types';
2424

25+
const currencyManager = new CurrencyManager([
26+
...CurrencyManager.getDefaultList(),
27+
{
28+
address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35',
29+
decimals: 18,
30+
network: 'private',
31+
symbol: 'DAI',
32+
type: RequestLogicTypes.CURRENCY.ERC20,
33+
},
34+
]);
35+
2536
/* eslint-disable no-magic-numbers */
2637
/* eslint-disable @typescript-eslint/no-unused-expressions */
2738

@@ -52,7 +63,7 @@ const alphaPaymentSettings: IConversionPaymentSettings = {
5263

5364
// requests setting
5465

55-
const EURExpectedAmount = 100;
66+
const EURExpectedAmount = 55000; // 55 000 €
5667
const EURFeeAmount = 2;
5768
// amounts used for DAI and FAU requests
5869
const expectedAmount = 100000;
@@ -182,6 +193,19 @@ describe('erc20-batch-conversion-proxy', () => {
182193
FAUTokenAddress,
183194
wallet,
184195
);
196+
197+
// Approve the contract to spent DAI with a conversion request
198+
const approvalTx = await approveErc20BatchConversionIfNeeded(
199+
EURValidRequest,
200+
wallet.address,
201+
batchConvVersion,
202+
wallet.provider,
203+
alphaPaymentSettings,
204+
);
205+
expect(approvalTx).toBeDefined();
206+
if (approvalTx) {
207+
await approvalTx.wait(1);
208+
}
185209
});
186210

187211
describe(`Conversion:`, () => {
@@ -341,78 +365,70 @@ describe('erc20-batch-conversion-proxy', () => {
341365
{ gasPrice: '20000000000' },
342366
);
343367
expect(spy).toHaveBeenCalledWith({
344-
data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
368+
data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000cce41660000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
345369
gasPrice: '20000000000',
346370
to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'),
347371
value: 0,
348372
});
349373
wallet.sendTransaction = originalSendTransaction;
350374
});
351-
it('should convert and pay a request in EUR with ERC20', async () => {
352-
// Approve the contract
353-
const approvalTx = await approveErc20BatchConversionIfNeeded(
354-
EURValidRequest,
355-
wallet.address,
356-
batchConvVersion,
357-
wallet.provider,
358-
alphaPaymentSettings,
359-
);
360-
expect(approvalTx).toBeDefined();
361-
if (approvalTx) {
362-
await approvalTx.wait(1);
363-
}
364-
365-
// Get the balances to compare after payment
366-
const initialETHFromBalance = await wallet.getBalance();
367-
const initialDAIFromBalance = await getErc20Balance(
368-
DAIValidRequest,
369-
wallet.address,
370-
provider,
371-
);
372-
373-
// Convert and pay
374-
const tx = await payBatchConversionProxyRequest(
375-
[
376-
{
377-
paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS,
378-
request: EURValidRequest,
379-
paymentSettings: alphaPaymentSettings,
380-
},
381-
],
382-
batchConvVersion,
383-
wallet,
384-
true,
385-
);
386-
const confirmedTx = await tx.wait(1);
387-
expect(confirmedTx.status).toEqual(1);
388-
expect(tx.hash).toBeDefined();
389-
390-
// Get the new balances
391-
const ETHFromBalance = await wallet.getBalance();
392-
const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider);
393-
394-
// Check each balance
395-
const amountToPay = expectedConversionAmount(EURExpectedAmount);
396-
const feeToPay = expectedConversionAmount(EURFeeAmount);
397-
const expectedAmountToPay = amountToPay
398-
.add(feeToPay)
399-
.mul(BATCH_DENOMINATOR + BATCH_CONV_FEE)
400-
.div(BATCH_DENOMINATOR);
401-
expect(
402-
BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(),
403-
).toBeGreaterThan(0);
404-
expect(
405-
BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)),
406-
// Calculation of expectedAmountToPay
407-
// expectedAmount: 1.00
408-
// feeAmount: + .02
409-
// = 1.02
410-
// AggEurUsd.sol x 1.20
411-
// AggDaiUsd.sol / 1.01
412-
// BATCH_CONV_FEE x 1.003
413-
// (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20)
414-
).toEqual(expectedAmountToPay);
415-
});
375+
for (const skipFeeUSDLimit in ['true', 'false']) {
376+
it(`should convert and pay a request in EUR with ERC20, ${
377+
skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit'
378+
} `, async () => {
379+
// Get the balances to compare after payment
380+
const initialETHFromBalance = await wallet.getBalance();
381+
const initialDAIFromBalance = await getErc20Balance(
382+
DAIValidRequest,
383+
wallet.address,
384+
provider,
385+
);
386+
387+
// Convert and pay
388+
const tx = await payBatchConversionProxyRequest(
389+
[
390+
{
391+
paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS,
392+
request: EURValidRequest,
393+
paymentSettings: alphaPaymentSettings,
394+
},
395+
],
396+
batchConvVersion,
397+
wallet,
398+
skipFeeUSDLimit === 'true',
399+
);
400+
const confirmedTx = await tx.wait(1);
401+
expect(confirmedTx.status).toEqual(1);
402+
expect(tx.hash).toBeDefined();
403+
404+
// Get the new balances
405+
const ETHFromBalance = await wallet.getBalance();
406+
const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider);
407+
408+
// Check each balance
409+
const amountToPay = expectedConversionAmount(EURExpectedAmount);
410+
const feeToPay = expectedConversionAmount(EURFeeAmount);
411+
const totalFeeToPay =
412+
skipFeeUSDLimit === 'true'
413+
? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR)
414+
: BigNumber.from('1984229702970297029'); // eq to 150$ batch fee (USD limit) + 2$
415+
const expectedAmountToPay = amountToPay.add(totalFeeToPay);
416+
expect(
417+
BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(),
418+
).toBeGreaterThan(0);
419+
expect(
420+
BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)),
421+
// Calculation of expectedAmountToPay
422+
// expectedAmount: 1.00
423+
// feeAmount: + .02
424+
// = 1.02
425+
// AggEurUsd.sol x 1.20
426+
// AggDaiUsd.sol / 1.01
427+
// BATCH_CONV_FEE x 1.003
428+
// (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20)
429+
).toEqual(expectedAmountToPay);
430+
});
431+
}
416432
it('should convert and pay two requests in EUR with ERC20', async () => {
417433
// Get initial balances
418434
const initialETHFromBalance = await wallet.getBalance();
@@ -542,9 +558,7 @@ describe('erc20-batch-conversion-proxy', () => {
542558
FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH;
543559
await expect(
544560
payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true),
545-
).rejects.toThrowError(
546-
'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request',
547-
);
561+
).rejects.toThrowError('wrong request currencyInfo type');
548562
});
549563

550564
it("should throw an error if one request's currencyInfo has no value", async () => {

0 commit comments

Comments
 (0)