From e870f03e51839f4cfb6bea8a342da21eef76f619 Mon Sep 17 00:00:00 2001 From: Darko Kolev Date: Mon, 29 Aug 2022 13:12:06 +0200 Subject: [PATCH 001/207] feat: add streamEventName to streaming event parameters (#908) --- .../src/erc777/superfluid-retriever.ts | 31 +++++++++++++++---- packages/payment-detection/src/index.ts | 2 ++ .../test/erc777/superfluid-retriever.test.ts | 6 ++++ packages/types/src/payment-types.ts | 6 ++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/payment-detection/src/erc777/superfluid-retriever.ts b/packages/payment-detection/src/erc777/superfluid-retriever.ts index fe89374638..cf1b42d3f9 100644 --- a/packages/payment-detection/src/erc777/superfluid-retriever.ts +++ b/packages/payment-detection/src/erc777/superfluid-retriever.ts @@ -44,6 +44,16 @@ export class SuperFluidInfoRetriever { }; } + /** + * Chronological sorting of events having payment reference and closing events without payment reference + * @returns List of streaming events + */ + protected async getStreamingEvents(): Promise[]> { + const variables = this.getGraphVariables(); + const { flow, untagged } = await this.client.GetSuperFluidEvents(variables); + return flow.concat(untagged).sort((a, b) => a.timestamp - b.timestamp); + } + /** * First MVP version which convert : * stream events queried from SuperFluid subgraph @@ -51,10 +61,7 @@ export class SuperFluidInfoRetriever { * to compute balance from amounts in ERC20 style transactions */ public async getTransferEvents(): Promise { - const variables = this.getGraphVariables(); - const { flow, untagged } = await this.client.GetSuperFluidEvents(variables); - // Chronological sorting of events having payment reference and closing events without payment reference - const streamEvents = flow.concat(untagged).sort((a, b) => a.timestamp - b.timestamp); + const streamEvents = await this.getStreamingEvents(); const paymentEvents: PaymentTypes.ERC777PaymentNetworkEvent[] = []; if (streamEvents.length < 1) { return paymentEvents; @@ -66,7 +73,7 @@ export class SuperFluidInfoRetriever { oldFlowRate: streamEvents[streamEvents.length - 1].flowRate, flowRate: 0, timestamp: Utils.getCurrentTimestampInSecond(), - blockNumber: parseInt(streamEvents[streamEvents.length - 1].blockNumber), + blockNumber: streamEvents[streamEvents.length - 1].blockNumber, transactionHash: streamEvents[streamEvents.length - 1].transactionHash, } as FlowUpdatedEvent); } @@ -74,6 +81,17 @@ export class SuperFluidInfoRetriever { const TYPE_BEGIN = 0; // const TYPE_UPDATE = 1; const TYPE_END = 2; + const StreamEventMap: Record = { + 0: PaymentTypes.STREAM_EVENT_NAMES.START_STREAM, + 1: PaymentTypes.STREAM_EVENT_NAMES.UPDATE_STREAM, + 2: PaymentTypes.STREAM_EVENT_NAMES.END_STREAM, + }; + const getEventName = (flowEvent: Partial) => { + if (flowEvent.type) { + return StreamEventMap[flowEvent.type]; + } + }; + for (let index = 1; index < streamEvents.length; index++) { // we have to manage update of flowrate to pay different payment references with the same token // but we do not manage in the MVP updating flowrate of ongoing payment @@ -95,8 +113,9 @@ export class SuperFluidInfoRetriever { name: this.eventName, parameters: { to: this.toAddress, - block: parseInt(streamEvents[index].blockNumber), + block: streamEvents[index].blockNumber, txHash: streamEvents[index].transactionHash, + streamEventName: getEventName(streamEvents[index]), }, timestamp: streamEvents[index].timestamp, }); diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index d6664fe685..e80042f493 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -20,6 +20,7 @@ import { NearNativeTokenPaymentDetector } from './near-detector'; import { FeeReferenceBasedDetector } from './fee-reference-based-detector'; import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever'; +import { SuperFluidInfoRetriever } from './erc777/superfluid-retriever'; export type { TheGraphClient } from './thegraph'; @@ -37,6 +38,7 @@ export { SuperFluidPaymentDetector, NearNativeTokenPaymentDetector, EscrowERC20InfoRetriever, + SuperFluidInfoRetriever, setProviderFactory, initPaymentDetectionApiKeys, getDefaultProvider, diff --git a/packages/payment-detection/test/erc777/superfluid-retriever.test.ts b/packages/payment-detection/test/erc777/superfluid-retriever.test.ts index 3c72a469aa..dcc3ce6c54 100644 --- a/packages/payment-detection/test/erc777/superfluid-retriever.test.ts +++ b/packages/payment-detection/test/erc777/superfluid-retriever.test.ts @@ -54,6 +54,9 @@ const testSuiteWithDaix = (network: string, fDAIxToken: string) => { expect(transferEvents[2].amount).toEqual('40509259259259000'); expect(transferEvents[0].parameters?.txHash).toEqual(paymentData.txHash); expect(transferEvents[0].parameters?.block).toEqual(paymentData.block); + expect(transferEvents[0].parameters?.streamEventName).toEqual( + PaymentTypes.STREAM_EVENT_NAMES.END_STREAM, + ); }); }); @@ -91,6 +94,9 @@ const testSuiteWithDaix = (network: string, fDAIxToken: string) => { expect(transferEvents[0].amount).toEqual(paymentData.amount); expect(transferEvents[0].name).toEqual('payment'); expect(transferEvents[0].parameters?.to).toEqual(paymentData.to); + expect(transferEvents[0].parameters?.streamEventName).toEqual( + PaymentTypes.STREAM_EVENT_NAMES.END_STREAM, + ); }); }); }); diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 52a091b295..a0496013ac 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -138,12 +138,18 @@ export interface IPaymentNetworkBaseInfoRetriever< * ERC777 networks and events */ +export enum STREAM_EVENT_NAMES { + START_STREAM = 'start_stream', + END_STREAM = 'end_stream', + UPDATE_STREAM = 'update_stream', +} /** Parameters for events of ERC777 payments */ export interface IERC777PaymentEventParameters { from?: string; to: string; block?: number; txHash?: string; + streamEventName?: STREAM_EVENT_NAMES; } /** ERC777 Payment Network Event */ From 58bc5979f05530e163cb197a4beabe137f9dafa1 Mon Sep 17 00:00:00 2001 From: Darko Kolev Date: Tue, 30 Aug 2022 17:42:32 +0200 Subject: [PATCH 002/207] feat: mark streaming payment events (#910) --- .../payment-detection/src/erc777/superfluid-retriever.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/payment-detection/src/erc777/superfluid-retriever.ts b/packages/payment-detection/src/erc777/superfluid-retriever.ts index cf1b42d3f9..e80a232581 100644 --- a/packages/payment-detection/src/erc777/superfluid-retriever.ts +++ b/packages/payment-detection/src/erc777/superfluid-retriever.ts @@ -66,9 +66,9 @@ export class SuperFluidInfoRetriever { if (streamEvents.length < 1) { return paymentEvents; } - // if last event is ongoing stream then create end of stream to help compute balance - if (streamEvents[streamEvents.length - 1].flowRate > 0) { + const lastEventOngoing = streamEvents[streamEvents.length - 1].flowRate > 0; + if (lastEventOngoing) { streamEvents.push({ oldFlowRate: streamEvents[streamEvents.length - 1].flowRate, flowRate: 0, @@ -120,6 +120,11 @@ export class SuperFluidInfoRetriever { timestamp: streamEvents[index].timestamp, }); } + const newLastParameters = paymentEvents[paymentEvents.length - 1].parameters; + if (lastEventOngoing && newLastParameters) { + newLastParameters.streamEventName = PaymentTypes.STREAM_EVENT_NAMES.START_STREAM; + paymentEvents[paymentEvents.length - 1].parameters = newLastParameters; + } return paymentEvents; } } From f0b10878d218925a7ae32eb7511b2a17ced12adf Mon Sep 17 00:00:00 2001 From: Darko Kolev Date: Tue, 6 Sep 2022 10:33:32 +0200 Subject: [PATCH 003/207] feat: always return approval and payment transactions (#911) --- .../payment-processor/src/payment/index.ts | 18 ++++-------------- .../test/payment/encoder.test.ts | 18 ++---------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 774b0c186c..d5bc316573 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -17,7 +17,7 @@ import { payAnyToEthProxyRequest } from './any-to-eth-proxy'; import { WalletConnection } from 'near-api-js'; import { isNearNetwork, isNearAccountSolvent } from './utils-near'; import { ICurrencyManager } from '@requestnetwork/currency'; -import { encodeRequestErc20ApprovalIfNeeded } from './encoder-approval'; +import { encodeRequestErc20Approval } from './encoder-approval'; import { encodeRequestPayment } from './encoder-payment'; import { IPreparedTransaction } from './prepared-transaction'; import { IRequestPaymentOptions } from './settings'; @@ -130,28 +130,18 @@ export async function payRequest( * Encode the transactions associated to a request * @param request the request to pay. * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param from The address which will send the transaction. * @param options encoding options * @returns */ export async function encodeRequestApprovalAndPayment( request: ClientTypes.IRequestData, signerOrProvider: providers.Provider, - from?: string, options?: IRequestPaymentOptions, ): Promise { const preparedTransactions: IPreparedTransaction[] = []; - - if (from) { - const approvalTx = await encodeRequestErc20ApprovalIfNeeded( - request, - signerOrProvider, - from, - options, - ); - if (approvalTx) { - preparedTransactions.push(approvalTx); - } + const approvalTx = await encodeRequestErc20Approval(request, signerOrProvider, options); + if (approvalTx) { + preparedTransactions.push(approvalTx); } preparedTransactions.push(encodeRequestPayment(request, signerOrProvider, options)); return preparedTransactions; diff --git a/packages/payment-processor/test/payment/encoder.test.ts b/packages/payment-processor/test/payment/encoder.test.ts index ea55c5faba..b0c6f9501e 100644 --- a/packages/payment-processor/test/payment/encoder.test.ts +++ b/packages/payment-processor/test/payment/encoder.test.ts @@ -225,11 +225,7 @@ beforeAll(async () => { describe('Encoder', () => { it('Should handle ERC20 Proxy request', async () => { - const encodedTransactions = await encodeRequestApprovalAndPayment( - baseValidRequest, - provider, - wallet.address, - ); + const encodedTransactions = await encodeRequestApprovalAndPayment(baseValidRequest, provider); let tx = await wallet.sendTransaction(encodedTransactions[0]); let confirmedTx = await tx.wait(1); @@ -246,7 +242,6 @@ describe('Encoder', () => { const encodedTransactions = await encodeRequestApprovalAndPayment( validRequestERC20FeeProxy, provider, - wallet.address, ); let tx = await wallet.sendTransaction(encodedTransactions[0]); @@ -264,7 +259,6 @@ describe('Encoder', () => { let encodedTransactions = await encodeRequestApprovalAndPayment( validRequestERC20ConversionProxy, provider, - wallet.address, { conversion: alphaConversionSettings, }, @@ -285,7 +279,6 @@ describe('Encoder', () => { let encodedTransactions = await encodeRequestApprovalAndPayment( validRequestERC20FeeProxy, provider, - wallet.address, { swap: alphaSwapSettings, }, @@ -306,7 +299,6 @@ describe('Encoder', () => { let encodedTransactions = await encodeRequestApprovalAndPayment( validRequestERC20ConversionProxy, provider, - wallet.address, { swap: alphaSwapConversionSettings, conversion: alphaConversionSettings, @@ -325,11 +317,7 @@ describe('Encoder', () => { }); it('Should handle Eth Proxy request', async () => { - let encodedTransactions = await encodeRequestApprovalAndPayment( - validRequestEthProxy, - provider, - wallet.address, - ); + let encodedTransactions = await encodeRequestApprovalAndPayment(validRequestEthProxy, provider); let tx = await wallet.sendTransaction(encodedTransactions[0]); let confirmedTx = await tx.wait(1); @@ -341,7 +329,6 @@ describe('Encoder', () => { let encodedTransactions = await encodeRequestApprovalAndPayment( validRequestEthFeeProxy, provider, - wallet.address, ); let tx = await wallet.sendTransaction(encodedTransactions[0]); @@ -354,7 +341,6 @@ describe('Encoder', () => { let encodedTransactions = await encodeRequestApprovalAndPayment( validRequestEthConversionProxy, provider, - wallet.address, { conversion: ethConversionSettings, }, From f1f877dad8cbba6bae67580c5db5f7fa9d125c32 Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:45:46 +0200 Subject: [PATCH 004/207] fix: superfluid balance error (#913) --- .../src/erc777/superfluid-retriever.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/payment-detection/src/erc777/superfluid-retriever.ts b/packages/payment-detection/src/erc777/superfluid-retriever.ts index e80a232581..487c004885 100644 --- a/packages/payment-detection/src/erc777/superfluid-retriever.ts +++ b/packages/payment-detection/src/erc777/superfluid-retriever.ts @@ -120,10 +120,12 @@ export class SuperFluidInfoRetriever { timestamp: streamEvents[index].timestamp, }); } - const newLastParameters = paymentEvents[paymentEvents.length - 1].parameters; - if (lastEventOngoing && newLastParameters) { - newLastParameters.streamEventName = PaymentTypes.STREAM_EVENT_NAMES.START_STREAM; - paymentEvents[paymentEvents.length - 1].parameters = newLastParameters; + if (paymentEvents.length > 0) { + const newLastParameters = paymentEvents[paymentEvents.length - 1].parameters; + if (lastEventOngoing && newLastParameters) { + newLastParameters.streamEventName = PaymentTypes.STREAM_EVENT_NAMES.START_STREAM; + paymentEvents[paymentEvents.length - 1].parameters = newLastParameters; + } } return paymentEvents; } From 1879f746488d072390f4852a0c477de63c9a81be Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Wed, 7 Sep 2022 19:46:39 +0200 Subject: [PATCH 005/207] fix: superfluid block type (#914) --- packages/payment-detection/src/erc777/superfluid-retriever.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/payment-detection/src/erc777/superfluid-retriever.ts b/packages/payment-detection/src/erc777/superfluid-retriever.ts index 487c004885..eac5015a62 100644 --- a/packages/payment-detection/src/erc777/superfluid-retriever.ts +++ b/packages/payment-detection/src/erc777/superfluid-retriever.ts @@ -73,7 +73,7 @@ export class SuperFluidInfoRetriever { oldFlowRate: streamEvents[streamEvents.length - 1].flowRate, flowRate: 0, timestamp: Utils.getCurrentTimestampInSecond(), - blockNumber: streamEvents[streamEvents.length - 1].blockNumber, + blockNumber: parseInt(streamEvents[streamEvents.length - 1].blockNumber.toString()), transactionHash: streamEvents[streamEvents.length - 1].transactionHash, } as FlowUpdatedEvent); } @@ -113,7 +113,7 @@ export class SuperFluidInfoRetriever { name: this.eventName, parameters: { to: this.toAddress, - block: streamEvents[index].blockNumber, + block: parseInt(streamEvents[index].blockNumber.toString()), txHash: streamEvents[index].transactionHash, streamEventName: getEventName(streamEvents[index]), }, From 20000587318107e97742688f69ba561868e39f8f Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 13 Sep 2022 18:59:39 +0200 Subject: [PATCH 006/207] feat(smart-contracts): batch conversion (#877) * batch erc20 conversion contract and local deployment batch conv tests rename files clean batch tests batch deploy cleaning uncomment batchERC20ConversionPaymentsMultiTokensEasy test test: clean logs * tests batchPayments functions * clear batch contract * add comments in smart contract * add batchEthConversionPaymentsWithReference add atchEth tested and contracts cleaned * refacto batch contracts approval functions * PR - update batch contact - function visibility and comments * keep prefix underscore usage for function args * convention naming in batch contract and more comments * batchConv - delete chainlink implementation * batchConv erc20 - delete a require - add error tests * prettier contract * refacto receive function - add comments to test functions * comments about unique token in smart contract * prettier * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * receive comment * tmp tests fail * batch calcul updated and tested - it includes fees now * delete basicFee inside the contract - update test - refacto script to deploy and local artifact * check the addresses of the contracts deployed and raise an error if needed * smart contract add variable tenThousand * contract require message - wording * smart contract delete chainlink * revert batchPayments modif * contract - remove irrelevant variable - wording uTokens * renaming requestInfo into conversionDetail and requestsInfoParent into cryptoDetails * contract - clean constructor * wording receive comments add batchEthPaymentsWithReference tests * clean packages * refacto batch conversion ERC20 tests * eth batch functions tested * rewording and cleaning * refacto tests batch conversion ERC20 * refacto tests: batch conversion Eth * clean tests and restore previous batchFee values * test refacto to simplify batchConvFunction * update batch conversion contract * update contract to make proxies public * test refacto: delete emitOneTx - path and add before - adminSigner * deploy instead of connect to batchProxy * end cleaning old test version * refacto new test structure * refacto test add eth beginning * tests batchRouter erros * abi update prettier contract comments deploy and addresses * batch conv tests delete proxy global variables * test refactored * tests: cleaning * add type for batch payment inputs * delete payment-processor batch * PR refacto * consistency naming: ETHBalance * refacto tests inluding aggregator rate and gasPrice types and payment * final modif Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- .../test/payment/eth-input-data.test.ts | 5 +- packages/smart-contracts/hardhat.config.ts | 7 +- .../scripts/test-deploy-all.ts | 2 + ...test-deploy-batch-conversion-deployment.ts | 88 ++ .../test-deploy-batch-erc-eth-deployment.ts | 7 + .../scripts/test-deploy_chainlink_contract.ts | 15 +- packages/smart-contracts/scripts/utils.ts | 21 + .../src/contracts/BatchConversionPayments.sol | 325 +++++++ .../contracts/BatchNoConversionPayments.sol | 387 ++++++++ .../interfaces/IEthConversionProxy.sol | 50 ++ .../BatchConversionPayments/0.1.0.json | 565 ++++++++++++ .../BatchConversionPayments/index.ts | 20 + .../BatchNoConversionPayments/0.1.0.json | 268 ++++++ .../BatchNoConversionPayments/index.ts | 20 + .../src/lib/artifacts/index.ts | 2 + .../contracts/BatchConversionPayments.test.ts | 845 ++++++++++++++++++ ...=> BatchNoConversionErc20Payments.test.ts} | 114 +-- ...s => BatchNoConversionEthPayments.test.ts} | 94 +- .../test/contracts/localArtifacts.ts | 15 + packages/types/src/payment-types.ts | 38 + 20 files changed, 2775 insertions(+), 113 deletions(-) create mode 100644 packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts create mode 100644 packages/smart-contracts/src/contracts/BatchConversionPayments.sol create mode 100644 packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol create mode 100644 packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/index.ts create mode 100644 packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts rename packages/smart-contracts/test/contracts/{BatchErc20Payments.test.ts => BatchNoConversionErc20Payments.test.ts} (90%) rename packages/smart-contracts/test/contracts/{BatchEthPayments.test.ts => BatchNoConversionEthPayments.test.ts} (81%) 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 3c020d3985..d0c0e17d89 100644 --- a/packages/payment-processor/test/payment/eth-input-data.test.ts +++ b/packages/payment-processor/test/payment/eth-input-data.test.ts @@ -19,6 +19,7 @@ const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); +const gasPrice = 2 * 10 ** 10; // await provider.getGasPrice() const validRequest: ClientTypes.IRequestData = { balance: { @@ -124,9 +125,7 @@ describe('payEthInputDataRequest', () => { expect(confirmedTx.status).toBe(1); // new_balance = old_balance + amount + fees expect(balanceAfter).toEqual( - balanceBefore - .sub(validRequest.expectedAmount) - .sub(confirmedTx.gasUsed.mul(2 * 10 ** 10) || 0), + balanceBefore.sub(validRequest.expectedAmount).sub(confirmedTx.gasUsed.mul(gasPrice) || 0), ); expect( balanceAfter.eq(balanceBefore.sub(validRequest.expectedAmount).sub(confirmedTx.gasUsed || 0)), diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index f12fdc0d4b..fc0dcc5919 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -14,6 +14,7 @@ import { computeCreate2DeploymentAddressesFromList } from './scripts-create2/com import { VerifyCreate2FromList } from './scripts-create2/verify'; import { deployWithCreate2FromList } from './scripts-create2/deploy'; import utils from '@requestnetwork/utils'; +import { NUMBER_ERRORS } from './scripts/utils'; config(); @@ -173,7 +174,11 @@ export default { task('deploy-local-env', 'Deploy a local environment').setAction(async (args, hre) => { args.force = true; await deployAllContracts(args, hre); - console.log('All contracts (re)deployed locally'); + if (NUMBER_ERRORS > 0) { + console.log(`Deployment failed, please check the ${NUMBER_ERRORS} errors`); + } else { + console.log('All contracts (re)deployed locally'); + } }); task( diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index b385507833..392f325257 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -5,6 +5,7 @@ import deployConversion from './test-deploy_chainlink_contract'; import { deployEscrow } from './test-deploy-escrow-deployment'; import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; +import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; // Deploys, set up the contracts export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -14,4 +15,5 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deployEscrow(hre); await deployBatchPayment(_args, hre); await deploySuperFluid(hre); + await deployBatchConversionPayment(_args, hre); } diff --git a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts new file mode 100644 index 0000000000..dc23d6b9a6 --- /dev/null +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -0,0 +1,88 @@ +import '@nomiclabs/hardhat-ethers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; + +import { + batchConversionPaymentsArtifact, + erc20ConversionProxy, + erc20FeeProxyArtifact, + ethConversionArtifact, + ethereumFeeProxyArtifact, +} from '../src/lib'; +import { chainlinkConversionPath as chainlinkConvArtifact } from '../src/lib'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { deployAddressChecking } from './utils'; +import { BigNumber } from 'ethers'; +import { PRECISION_RATE } from './test-deploy_chainlink_contract'; + +export const FAU_USD_RATE = BigNumber.from(201 * PRECISION_RATE).div(100); + +// Deploys, set up the contracts +export async function deployBatchConversionPayment( + args: any, + hre: HardhatRuntimeEnvironment, +): Promise { + try { + console.log('Deploy BatchConversionPayments'); + const _ERC20FeeProxyAddress = erc20FeeProxyArtifact.getAddress('private'); + const _EthereumFeeProxyAddress = ethereumFeeProxyArtifact.getAddress('private'); + const _paymentErc20ConversionFeeProxy = erc20ConversionProxy.getAddress('private'); + const _paymentEthConversionFeeProxy = ethConversionArtifact.getAddress('private'); + + // Deploy BatchConversionPayments contract + const { address: BatchConversionPaymentsAddress } = await deployOne( + args, + hre, + 'BatchConversionPayments', + { + constructorArguments: [ + _ERC20FeeProxyAddress, + _EthereumFeeProxyAddress, + _paymentErc20ConversionFeeProxy, + _paymentEthConversionFeeProxy, + await (await hre.ethers.getSigners())[0].getAddress(), + ], + }, + ); + + // Add a second ERC20 token and aggregator - useful for batch test + const [owner] = await hre.ethers.getSigners(); + const erc20Factory = await hre.ethers.getContractFactory('TestERC20'); + const testERC20FakeFAU = await erc20Factory.deploy('1000000000000000000000000000000'); + const { address: AggFakeFAU_USD_address } = await deployOne(args, hre, 'AggregatorMock', { + constructorArguments: [FAU_USD_RATE, 8, 60], + }); + const conversionPathInstance = chainlinkConvArtifact.connect('private', owner); + const currencyManager = CurrencyManager.getDefault(); + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + await conversionPathInstance.updateAggregatorsList( + [testERC20FakeFAU.address], + [USD_hash], + [AggFakeFAU_USD_address], + ); + + // Check the addresses of our contracts, to avoid misleading bugs in the tests + // ref to secondLocalERC20AlphaArtifact.getAddress('private'), that cannot be used in deployment + const fakeFAU_addressExpected = '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36'; + deployAddressChecking('testERC20FakeFAU', testERC20FakeFAU.address, fakeFAU_addressExpected); + deployAddressChecking( + 'batchConversionPayments', + BatchConversionPaymentsAddress, + batchConversionPaymentsArtifact.getAddress('private'), + ); + + // Initialize batch conversion fee, useful to others packages. + const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); + await batchConversion.connect(owner).setBatchFee(30); + await batchConversion.connect(owner).setBatchConversionFee(30); + + // ---------------------------------- + console.log('Contracts deployed'); + console.log(` + testERC20FakeFAU.address: ${testERC20FakeFAU.address} + BatchConversionPayments: ${BatchConversionPaymentsAddress} + `); + } catch (e) { + console.error(e); + } +} diff --git a/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts b/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts index d483cd177d..96fa279ccf 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-erc-eth-deployment.ts @@ -3,6 +3,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { deployOne } from '../scripts/deploy-one'; import { batchPaymentsArtifact } from '../src/lib'; +import { deployAddressChecking } from './utils'; // Deploys, set up the contracts export async function deployBatchPayment(args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -29,6 +30,12 @@ export async function deployBatchPayment(args: any, hre: HardhatRuntimeEnvironme console.log(` BatchPayments: ${BatchPaymentsAddress} `); + + deployAddressChecking( + 'BatchPayments', + BatchPaymentsAddress, + batchPaymentsArtifact.getAddress('private'), + ); } catch (e) { console.error(e); } diff --git a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts index 099be843a4..d7759d254c 100644 --- a/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts +++ b/packages/smart-contracts/scripts/test-deploy_chainlink_contract.ts @@ -4,6 +4,13 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { deployERC20ConversionProxy, deployEthConversionProxy } from './conversion-proxy'; import { deploySwapConversion } from './erc20-swap-to-conversion'; import { deployOne } from './deploy-one'; +import { BigNumber } from 'ethers'; + +export const PRECISION_RATE = 100_000_000; +export const EUR_USD_RATE = BigNumber.from(1.2 * PRECISION_RATE); +export const ETH_USD_RATE = BigNumber.from(500 * PRECISION_RATE); +export const DAI_USD_RATE = BigNumber.from(1.01 * PRECISION_RATE); +export const USDT_ETH_RATE = BigNumber.from(0.002 * 1_000_000_000_000_000_000); export default async function deploy( args: any, @@ -12,16 +19,16 @@ export default async function deploy( ) { const [deployer] = await hre.ethers.getSigners(); const { address: AggDAI_USD_address } = await deployOne(args, hre, 'AggregatorMock', { - constructorArguments: [101000000, 8, 60], + constructorArguments: [DAI_USD_RATE, 8, 60], }); const { address: AggETH_USD_address } = await deployOne(args, hre, 'AggregatorMock', { - constructorArguments: [50000000000, 8, 60], + constructorArguments: [ETH_USD_RATE, 8, 60], }); const { address: AggEUR_USD_address } = await deployOne(args, hre, 'AggregatorMock', { - constructorArguments: [120000000, 8, 60], + constructorArguments: [EUR_USD_RATE, 8, 60], }); const { address: AggUSDT_ETH_address } = await deployOne(args, hre, 'AggregatorMock', { - constructorArguments: [2000000000000000, 18, 60], + constructorArguments: [USDT_ETH_RATE, 18, 60], }); const { address: USDT_fake_address } = await deployOne(args, hre, 'UsdtFake'); diff --git a/packages/smart-contracts/scripts/utils.ts b/packages/smart-contracts/scripts/utils.ts index 4042308362..c712a7b813 100644 --- a/packages/smart-contracts/scripts/utils.ts +++ b/packages/smart-contracts/scripts/utils.ts @@ -45,3 +45,24 @@ export const jumpToNonce = async (args: any, hre: HardhatRuntimeEnvironment, non nextNonce = await deployer.getTransactionCount(); } }; + +/** Variable used to count the number of contracts deployed at the wrong address */ +export let NUMBER_ERRORS = 0; + +/** + * The function compare the address of the contract deployed with the existing one, usually stored in artifacts + * @param contratName name of the contract used to deployed an instance, or name of the instance if they are many implementations + * @param contractAddress address of the current deployement + * @param contractAddressExpected usually stored in artifacts + */ +export const deployAddressChecking = ( + contratName: string, + contractAddress: string, + contractAddressExpected: string, +): void => { + if (contractAddress !== contractAddressExpected) { + NUMBER_ERRORS += 1; + const msg = `${contratName} deployed at ${contractAddress} is different from the one expected: ${contractAddressExpected}, please update your code or the artifact`; + throw Error(msg); + } +}; diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol new file mode 100644 index 0000000000..28aaf019a7 --- /dev/null +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import './interfaces/IERC20ConversionProxy.sol'; +import './interfaces/IEthConversionProxy.sol'; +import './BatchNoConversionPayments.sol'; + +/** + * @title BatchConversionPayments + * @notice This contract makes multiple conversion payments with references, in one transaction: + * - on: + * - ERC20 tokens: using Erc20ConversionProxy and ERC20FeeProxy + * - Native tokens: (e.g. ETH) using EthConversionProxy and EthereumFeeProxy + * - to: multiple addresses + * - fees: conversion proxy fees and additional batch conversion fees are paid to the same address. + * batchRouter is the main function to batch all kinds of payments at once. + * If one transaction of the batch fails, all transactions are reverted. + * @dev Note that fees have 4 decimals (instead of 3 in a previous version) + * batchRouter is the main function, but other batch payment functions are "public" in order to do + * gas optimization in some cases. + */ +contract BatchConversionPayments is BatchNoConversionPayments { + using SafeERC20 for IERC20; + + IERC20ConversionProxy public paymentErc20ConversionProxy; + IEthConversionProxy public paymentEthConversionProxy; + + uint256 public batchConversionFee; + + /** + * @dev All the information of a request, except the feeAddress + * _recipient Recipient address of the payment + * _requestAmount Request amount in fiat + * _path Conversion path + * _paymentReference Unique reference of the payment + * _feeAmount The fee amount denominated in the first currency of `_path` + * _maxToSpend Maximum amount the payer wants to spend, denominated in the last currency of `_path`: + * it includes fee proxy but NOT the batchConversionFee + * _maxRateTimespan Max acceptable times span for conversion rates, ignored if zero + */ + struct ConversionDetail { + address recipient; + uint256 requestAmount; + address[] path; + bytes paymentReference; + uint256 feeAmount; + uint256 maxToSpend; + uint256 maxRateTimespan; + } + + /** + * @dev BatchNoConversionPayments contract input structure. + */ + struct CryptoDetails { + address[] tokenAddresses; + address[] recipients; + uint256[] amounts; + bytes[] paymentReferences; + uint256[] feeAmounts; + } + + /** + * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. + * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. + * - conversionDetails all the data required for conversion requests to be paid, for paymentNetworkId = 0 or 4 + * - cryptoDetails all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 3 + */ + struct MetaDetail { + uint256 paymentNetworkId; + ConversionDetail[] conversionDetails; + CryptoDetails cryptoDetails; + } + + /** + * @param _paymentErc20Proxy The ERC20 payment proxy address to use. + * @param _paymentEthProxy The ETH payment proxy address to use. + * @param _paymentErc20ConversionProxy The ERC20 Conversion payment proxy address to use. + * @param _paymentEthConversionFeeProxy The ETH Conversion payment proxy address to use. + * @param _owner Owner of the contract. + */ + constructor( + address _paymentErc20Proxy, + address _paymentEthProxy, + address _paymentErc20ConversionProxy, + address _paymentEthConversionFeeProxy, + address _owner + ) BatchNoConversionPayments(_paymentErc20Proxy, _paymentEthProxy, _owner) { + paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); + paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + batchConversionFee = 0; + } + + /** + * @notice Batch payments on different payment networks at once. + * @param metaDetails contains paymentNetworkId, conversionDetails, and cryptoDetails + * - batchMultiERC20ConversionPayments, paymentNetworkId=0 + * - batchERC20Payments, paymentNetworkId=1 + * - batchMultiERC20Payments, paymentNetworkId=2 + * - batchEthPayments, paymentNetworkId=3 + * - batchEthConversionPayments, paymentNetworkId=4 + * If metaDetails use paymentNetworkId = 4, it must be at the end of the list, or the transaction can be reverted + * @param _feeAddress The address where fees should be paid + * @dev batchRouter only reduces gas consumption when using more than a single payment network. + * For single payment network payments, it is more efficient to use the suited batch function. + */ + function batchRouter(MetaDetail[] calldata metaDetails, address _feeAddress) external payable { + require(metaDetails.length < 6, 'more than 5 metaDetails'); + for (uint256 i = 0; i < metaDetails.length; i++) { + MetaDetail calldata metaConversionDetail = metaDetails[i]; + if (metaConversionDetail.paymentNetworkId == 0) { + batchMultiERC20ConversionPayments(metaConversionDetail.conversionDetails, _feeAddress); + } else if (metaConversionDetail.paymentNetworkId == 1) { + batchERC20Payments( + metaConversionDetail.cryptoDetails.tokenAddresses[0], + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, + _feeAddress + ); + } else if (metaConversionDetail.paymentNetworkId == 2) { + batchMultiERC20Payments( + metaConversionDetail.cryptoDetails.tokenAddresses, + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, + _feeAddress + ); + } else if (metaConversionDetail.paymentNetworkId == 3) { + if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { + // Set to false only if batchEthConversionPayments is called after this function + transferBackRemainingEth = false; + } + batchEthPayments( + metaConversionDetail.cryptoDetails.recipients, + metaConversionDetail.cryptoDetails.amounts, + metaConversionDetail.cryptoDetails.paymentReferences, + metaConversionDetail.cryptoDetails.feeAmounts, + payable(_feeAddress) + ); + if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { + transferBackRemainingEth = true; + } + } else if (metaConversionDetail.paymentNetworkId == 4) { + batchEthConversionPayments(metaConversionDetail.conversionDetails, payable(_feeAddress)); + } else { + revert('wrong paymentNetworkId'); + } + } + } + + /** + * @notice Send a batch of ERC20 payments with amounts based on a request + * currency (e.g. fiat), with fees and paymentReferences to multiple accounts, with multiple tokens. + * @param conversionDetails list of requestInfo, each one containing all the information of a request + * @param _feeAddress The fee recipient + */ + function batchMultiERC20ConversionPayments( + ConversionDetail[] calldata conversionDetails, + address _feeAddress + ) public { + // a list of unique tokens, with the sum of maxToSpend by token + Token[] memory uTokens = new Token[](conversionDetails.length); + for (uint256 i = 0; i < conversionDetails.length; i++) { + for (uint256 k = 0; k < conversionDetails.length; k++) { + // If the token is already in the existing uTokens list + if ( + uTokens[k].tokenAddress == conversionDetails[i].path[conversionDetails[i].path.length - 1] + ) { + uTokens[k].amountAndFee += conversionDetails[i].maxToSpend; + break; + } + // If the token is not in the list (amountAndFee = 0) + else if (uTokens[k].amountAndFee == 0 && (conversionDetails[i].maxToSpend) > 0) { + uTokens[k].tokenAddress = conversionDetails[i].path[conversionDetails[i].path.length - 1]; + // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract + uTokens[k].amountAndFee = conversionDetails[i].maxToSpend; + break; + } + } + } + + IERC20 requestedToken; + // For each token: check allowance, transfer funds on the contract and approve the paymentProxy to spend if needed + for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { + requestedToken = IERC20(uTokens[k].tokenAddress); + uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / tenThousand; + // Check proxy's allowance from user, and user's funds to pay approximated amounts. + require( + requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, + 'Insufficient allowance for batch to pay' + ); + require( + requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, + 'not enough funds, including fees' + ); + + // Transfer the amount and fee required for the token on the batch conversion contract + require( + safeTransferFrom(uTokens[k].tokenAddress, address(this), uTokens[k].amountAndFee), + 'payment transferFrom() failed' + ); + + // Batch contract approves Erc20ConversionProxy to spend the token + if ( + requestedToken.allowance(address(this), address(paymentErc20ConversionProxy)) < + uTokens[k].amountAndFee + ) { + approvePaymentProxyToSpend(uTokens[k].tokenAddress, address(paymentErc20ConversionProxy)); + } + } + + // Batch pays the requests using Erc20ConversionFeeProxy + for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory rI = conversionDetails[i]; + paymentErc20ConversionProxy.transferFromWithReferenceAndFee( + rI.recipient, + rI.requestAmount, + rI.path, + rI.paymentReference, + rI.feeAmount, + _feeAddress, + rI.maxToSpend, + rI.maxRateTimespan + ); + } + + // Batch sends back to the payer the tokens not spent and pays the batch fee + for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { + requestedToken = IERC20(uTokens[k].tokenAddress); + + // Batch sends back to the payer the tokens not spent = excessAmount + // excessAmount = maxToSpend - reallySpent, which is equal to the remaining tokens on the contract + uint256 excessAmount = requestedToken.balanceOf(address(this)); + if (excessAmount > 0) { + requestedToken.safeTransfer(msg.sender, excessAmount); + } + + // Payer pays the exact batch fees amount + require( + safeTransferFrom( + uTokens[k].tokenAddress, + _feeAddress, + ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / tenThousand + ), + 'batch fee transferFrom() failed' + ); + } + } + + /** + * @notice Send a batch of ETH conversion payments with fees and paymentReferences to multiple accounts. + * If one payment fails, the whole batch is reverted. + * @param conversionDetails List of requestInfos, each one containing all the information of a request. + * _maxToSpend is not used in this function. + * @param _feeAddress The fee recipient. + * @dev It uses EthereumConversionProxy to pay an invoice and fees. + * Please: + * Note that if there is not enough ether attached to the function call, + * the following error is thrown: "revert paymentProxy transferExactEthWithReferenceAndFee failed" + * This choice reduces the gas significantly, by delegating the whole conversion to the payment proxy. + */ + function batchEthConversionPayments( + ConversionDetail[] calldata conversionDetails, + address payable _feeAddress + ) public payable { + uint256 contractBalance = address(this).balance; + payerAuthorized = true; + + // Batch contract pays the requests through EthConversionProxy + for (uint256 i = 0; i < conversionDetails.length; i++) { + paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( + payable(conversionDetails[i].recipient), + conversionDetails[i].requestAmount, + conversionDetails[i].path, + conversionDetails[i].paymentReference, + conversionDetails[i].feeAmount, + _feeAddress, + conversionDetails[i].maxRateTimespan + ); + } + + // Check that batch contract has enough funds to pay batch conversion fees + uint256 amountBatchFees = (((contractBalance - address(this).balance)) * batchConversionFee) / + tenThousand; + require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); + + // Batch contract pays batch fee + _feeAddress.transfer(amountBatchFees); + + // Batch contract transfers the remaining ethers to the payer + (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); + require(sendBackSuccess, 'Could not send remaining funds to the payer'); + payerAuthorized = false; + } + + /* + * Admin functions to edit the conversion proxies address and fees + */ + + /** + * @notice fees added when using Erc20/Eth conversion batch functions + * @param _batchConversionFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fees + */ + function setBatchConversionFee(uint256 _batchConversionFee) external onlyOwner { + batchConversionFee = _batchConversionFee; + } + + /** + * @param _paymentErc20ConversionProxy The address of the ERC20 Conversion payment proxy to use. + * Update cautiously, the proxy has to match the invoice proxy. + */ + function setPaymentErc20ConversionProxy(address _paymentErc20ConversionProxy) external onlyOwner { + paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); + } + + /** + * @param _paymentEthConversionProxy The address of the Ethereum Conversion payment proxy to use. + * Update cautiously, the proxy has to match the invoice proxy. + */ + function setPaymentEthConversionProxy(address _paymentEthConversionProxy) external onlyOwner { + paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionProxy); + } +} diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol new file mode 100644 index 0000000000..5516ac23f9 --- /dev/null +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import './lib/SafeERC20.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import './interfaces/ERC20FeeProxy.sol'; +import './interfaces/EthereumFeeProxy.sol'; + +/** + * @title BatchNoConversionPayments + * @notice This contract makes multiple payments with references, in one transaction: + * - on: ERC20 Payment Proxy and ETH Payment Proxy of the Request Network protocol + * - to: multiple addresses + * - fees: ERC20 and ETH proxies fees are paid to the same address. + * An additional batch fee is paid to the same address. + * If one transaction of the batch fail, every transactions are reverted. + * @dev It is a clone of BatchPayment.sol, with three main modifications: + * - function "receive" has one other condition: payerAuthorized + * - fees are now divided by 10_000 instead of 1_000 in previous version + * - batch payment functions have new names and are now public, instead of external + */ +contract BatchNoConversionPayments is Ownable { + using SafeERC20 for IERC20; + + IERC20FeeProxy public paymentErc20Proxy; + IEthereumFeeProxy public paymentEthProxy; + + uint256 public batchFee; + /** Used to to calculate batch fees */ + uint256 internal tenThousand = 10000; + + // payerAuthorized is set to true only when needed for batch Eth conversion + bool internal payerAuthorized; + + // transferBackRemainingEth is set to false only if the payer use batchRouter + // and call both batchEthPayments and batchConversionEthPaymentsWithReference + bool internal transferBackRemainingEth = true; + + struct Token { + address tokenAddress; + uint256 amountAndFee; + uint256 batchFeeAmount; + } + + /** + * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. + * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. + * @param _owner Owner of the contract. + */ + constructor( + address _paymentErc20Proxy, + address _paymentEthProxy, + address _owner + ) { + paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); + paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); + transferOwnership(_owner); + batchFee = 0; + } + + /** + * This contract is non-payable. Making an ETH payment with conversion requires the contract to accept incoming ETH. + * @dev See the end of `paymentEthConversionProxy.transferWithReferenceAndFee` where the leftover is given back. + */ + receive() external payable { + require(payerAuthorized || msg.value == 0, 'Non-payable'); + } + + /** + * @notice Send a batch of ETH (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * If one payment fails, the whole batch reverts. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List fee amounts, matching recipients[]. + * @param _feeAddress The fee recipient. + * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. + * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount + */ + function batchEthPayments( + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address payable _feeAddress + ) public payable { + require( + _recipients.length == _amounts.length && + _recipients.length == _paymentReferences.length && + _recipients.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // amount is used to get the total amount and then used as batch fee amount + uint256 amount = 0; + + // Batch contract pays the requests thourgh EthFeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + require(address(this).balance >= _amounts[i] + _feeAmounts[i], 'not enough funds'); + amount += _amounts[i]; + + paymentEthProxy.transferWithReferenceAndFee{value: _amounts[i] + _feeAmounts[i]}( + payable(_recipients[i]), + _paymentReferences[i], + _feeAmounts[i], + payable(_feeAddress) + ); + } + + // amount is updated into batch fee amount + amount = (amount * batchFee) / tenThousand; + // Check that batch contract has enough funds to pay batch fee + require(address(this).balance >= amount, 'not enough funds for batch fee'); + // Batch pays batch fee + _feeAddress.transfer(amount); + + // Batch contract transfers the remaining ethers to the payer + if (transferBackRemainingEth && address(this).balance > 0) { + (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); + require(sendBackSuccess, 'Could not send remaining funds to the payer'); + } + } + + /** + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. + * @param _tokenAddress Token used for all the payments. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List of payment fee amounts, matching recipients[]. + * @param _feeAddress The fee recipient. + * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * Make sure this contract has enough allowance to spend the payer's token. + * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. + */ + function batchERC20Payments( + address _tokenAddress, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) public { + require( + _recipients.length == _amounts.length && + _recipients.length == _paymentReferences.length && + _recipients.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // amount is used to get the total amount and fee, and then used as batch fee amount + uint256 amount = 0; + for (uint256 i = 0; i < _recipients.length; i++) { + amount += _amounts[i] + _feeAmounts[i]; + } + + // Transfer the amount and fee from the payer to the batch contract + IERC20 requestedToken = IERC20(_tokenAddress); + require( + requestedToken.allowance(msg.sender, address(this)) >= amount, + 'Insufficient allowance for batch to pay' + ); + require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); + require( + safeTransferFrom(_tokenAddress, address(this), amount), + 'payment transferFrom() failed' + ); + + // Batch contract approve Erc20FeeProxy to spend the token + if (requestedToken.allowance(address(this), address(paymentErc20Proxy)) < amount) { + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); + } + + // Batch contract pays the requests using Erc20FeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + // amount is updated to become the sum of amounts, to calculate batch fee amount + amount -= _feeAmounts[i]; + paymentErc20Proxy.transferFromWithReferenceAndFee( + _tokenAddress, + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + + // amount is updated into batch fee amount + amount = (amount * batchFee) / tenThousand; + // Check if the payer has enough funds to pay batch fee + require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); + + // Payer pays batch fee amount + require( + safeTransferFrom(_tokenAddress, _feeAddress, amount), + 'batch fee transferFrom() failed' + ); + } + + /** + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. + * @param _tokenAddresses List of tokens to transact with. + * @param _recipients List of recipient accounts. + * @param _amounts List of amounts, matching recipients[]. + * @param _paymentReferences List of paymentRefs, matching recipients[]. + * @param _feeAmounts List of amounts of the payment fee, matching recipients[]. + * @param _feeAddress The fee recipient. + * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * Make sure this contract has enough allowance to spend the payer's token. + * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. + */ + function batchMultiERC20Payments( + address[] calldata _tokenAddresses, + address[] calldata _recipients, + uint256[] calldata _amounts, + bytes[] calldata _paymentReferences, + uint256[] calldata _feeAmounts, + address _feeAddress + ) public { + require( + _tokenAddresses.length == _recipients.length && + _tokenAddresses.length == _amounts.length && + _tokenAddresses.length == _paymentReferences.length && + _tokenAddresses.length == _feeAmounts.length, + 'the input arrays must have the same length' + ); + + // Create a list of unique tokens used and the amounts associated + // Only considere tokens having: amounts + feeAmounts > 0 + // batchFeeAmount is the amount's sum, and then, batch fee rate is applied + Token[] memory uTokens = new Token[](_tokenAddresses.length); + for (uint256 i = 0; i < _tokenAddresses.length; i++) { + for (uint256 j = 0; j < _tokenAddresses.length; j++) { + // If the token is already in the existing uTokens list + if (uTokens[j].tokenAddress == _tokenAddresses[i]) { + uTokens[j].amountAndFee += _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount += _amounts[i]; + break; + } + // If the token is not in the list (amountAndFee = 0), and amount + fee > 0 + if (uTokens[j].amountAndFee == 0 && (_amounts[i] + _feeAmounts[i]) > 0) { + uTokens[j].tokenAddress = _tokenAddresses[i]; + uTokens[j].amountAndFee = _amounts[i] + _feeAmounts[i]; + uTokens[j].batchFeeAmount = _amounts[i]; + break; + } + } + } + + // The payer transfers tokens to the batch contract and pays batch fee + for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { + uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / tenThousand; + IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); + + require( + requestedToken.allowance(msg.sender, address(this)) >= + uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, + 'Insufficient allowance for batch to pay' + ); + // check if the payer can pay the amount, the fee, and the batchFee + require( + requestedToken.balanceOf(msg.sender) >= uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, + 'not enough funds' + ); + + // Transfer only the amount and fee required for the token on the batch contract + require( + safeTransferFrom(uTokens[i].tokenAddress, address(this), uTokens[i].amountAndFee), + 'payment transferFrom() failed' + ); + + // Batch contract approves Erc20FeeProxy to spend the token + if ( + requestedToken.allowance(address(this), address(paymentErc20Proxy)) < + uTokens[i].amountAndFee + ) { + approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); + } + + // Payer pays batch fee amount + require( + safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, uTokens[i].batchFeeAmount), + 'batch fee transferFrom() failed' + ); + } + + // Batch contract pays the requests using Erc20FeeProxy + for (uint256 i = 0; i < _recipients.length; i++) { + paymentErc20Proxy.transferFromWithReferenceAndFee( + _tokenAddresses[i], + _recipients[i], + _amounts[i], + _paymentReferences[i], + _feeAmounts[i], + _feeAddress + ); + } + } + + /* + * Helper functions + */ + + /** + * @notice Authorizes the proxy to spend a new request currency (ERC20). + * @param _erc20Address Address of an ERC20 used as the request currency. + * @param _paymentErc20Proxy Address of the proxy. + */ + function approvePaymentProxyToSpend(address _erc20Address, address _paymentErc20Proxy) internal { + IERC20 erc20 = IERC20(_erc20Address); + uint256 max = 2**256 - 1; + erc20.safeApprove(address(_paymentErc20Proxy), max); + } + + /** + * @notice Call transferFrom ERC20 function and validates the return data of a ERC20 contract call. + * @dev This is necessary because of non-standard ERC20 tokens that don't have a return value. + * @return result The return value of the ERC20 call, returning true for non-standard tokens + */ + function safeTransferFrom( + address _tokenAddress, + address _to, + uint256 _amount + ) internal returns (bool result) { + /* solium-disable security/no-inline-assembly */ + // check if the address is a contract + assembly { + if iszero(extcodesize(_tokenAddress)) { + revert(0, 0) + } + } + + // solium-disable-next-line security/no-low-level-calls + (bool success, ) = _tokenAddress.call( + abi.encodeWithSignature('transferFrom(address,address,uint256)', msg.sender, _to, _amount) + ); + + assembly { + switch returndatasize() + case 0 { + // Not a standard erc20 + result := 1 + } + case 32 { + // Standard erc20 + returndatacopy(0, 0, 32) + result := mload(0) + } + default { + // Anything else, should revert for safety + revert(0, 0) + } + } + + require(success, 'transferFrom() has been reverted'); + + /* solium-enable security/no-inline-assembly */ + return result; + } + + /* + * Admin functions to edit the proxies address and fees + */ + + /** + * @notice fees added when using Erc20/Eth batch functions + * @param _batchFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fee + */ + function setBatchFee(uint256 _batchFee) external onlyOwner { + batchFee = _batchFee; + } + + /** + * @param _paymentErc20Proxy The address to the Erc20 fee payment proxy to use. + */ + function setPaymentErc20Proxy(address _paymentErc20Proxy) external onlyOwner { + paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); + } + + /** + * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. + */ + function setPaymentEthProxy(address _paymentEthProxy) external onlyOwner { + paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); + } +} diff --git a/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol b/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol new file mode 100644 index 0000000000..beaee97af5 --- /dev/null +++ b/packages/smart-contracts/src/contracts/interfaces/IEthConversionProxy.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IEthConversionProxy + * @notice This contract converts from chainlink then swaps ETH (or native token) + * before paying a request thanks to a conversion payment proxy. + * The inheritance from ReentrancyGuard is required to perform + * "transferExactEthWithReferenceAndFee" on the eth-fee-proxy contract + */ +interface IEthConversionProxy { + // Event to declare a conversion with a reference + event TransferWithConversionAndReference( + uint256 amount, + address currency, + bytes indexed paymentReference, + uint256 feeAmount, + uint256 maxRateTimespan + ); + + // Event to declare a transfer with a reference + // This event is emitted by this contract from a delegate call of the payment-proxy + event TransferWithReferenceAndFee( + address to, + uint256 amount, + bytes indexed paymentReference, + uint256 feeAmount, + address feeAddress + ); + + /** + * @notice Performs an ETH transfer with a reference computing the payment amount based on the request amount + * @param _to Transfer recipient of the payement + * @param _requestAmount Request amount + * @param _path Conversion path + * @param _paymentReference Reference of the payment related + * @param _feeAmount The amount of the payment fee + * @param _feeAddress The fee recipient + * @param _maxRateTimespan Max time span with the oldestrate, ignored if zero + */ + function transferWithReferenceAndFee( + address _to, + uint256 _requestAmount, + address[] calldata _path, + bytes calldata _paymentReference, + uint256 _feeAmount, + address _feeAddress, + uint256 _maxRateTimespan + ) external payable; +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json new file mode 100644 index 0000000000..dc2dbe0461 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/0.1.0.json @@ -0,0 +1,565 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20Proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentEthProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentErc20ConversionProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentEthConversionFeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "batchConversionFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20Payments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestAmount", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchEthConversionPayments", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchEthPayments", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "batchFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestAmount", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchMultiERC20ConversionPayments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchMultiERC20Payments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "paymentNetworkId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestAmount", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxToSpend", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRateTimespan", + "type": "uint256" + } + ], + "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "address[]", + "name": "tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "feeAmounts", + "type": "uint256[]" + } + ], + "internalType": "struct BatchConversionPayments.CryptoDetails", + "name": "cryptoDetails", + "type": "tuple" + } + ], + "internalType": "struct BatchConversionPayments.MetaDetail[]", + "name": "metaDetails", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchRouter", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentErc20ConversionProxy", + "outputs": [ + { + "internalType": "contract IERC20ConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentErc20Proxy", + "outputs": [ + { + "internalType": "contract IERC20FeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentEthConversionProxy", + "outputs": [ + { + "internalType": "contract IEthConversionProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentEthProxy", + "outputs": [ + { + "internalType": "contract IEthereumFeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchConversionFee", + "type": "uint256" + } + ], + "name": "setBatchConversionFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchFee", + "type": "uint256" + } + ], + "name": "setBatchFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20ConversionProxy", + "type": "address" + } + ], + "name": "setPaymentErc20ConversionProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20Proxy", + "type": "address" + } + ], + "name": "setPaymentErc20Proxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentEthConversionProxy", + "type": "address" + } + ], + "name": "setPaymentEthConversionProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentEthProxy", + "type": "address" + } + ], + "name": "setPaymentEthProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts new file mode 100644 index 0000000000..d83849d90a --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -0,0 +1,20 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { BatchConversionPayments } from '../../../types/BatchConversionPayments'; + +export const batchConversionPaymentsArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x2e335F247E91caa168c64b63104C4475b2af3942', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.1.0', +); diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json new file mode 100644 index 0000000000..7803175769 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json @@ -0,0 +1,268 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20Proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_paymentEthProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchERC20Payments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchEthPayments", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "batchFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_tokenAddresses", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "_paymentReferences", + "type": "bytes[]" + }, + { + "internalType": "uint256[]", + "name": "_feeAmounts", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "batchMultiERC20Payments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentErc20Proxy", + "outputs": [ + { + "internalType": "contract IERC20FeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paymentEthProxy", + "outputs": [ + { + "internalType": "contract IEthereumFeeProxy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchFee", + "type": "uint256" + } + ], + "name": "setBatchFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentErc20Proxy", + "type": "address" + } + ], + "name": "setPaymentErc20Proxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_paymentEthProxy", + "type": "address" + } + ], + "name": "setPaymentEthProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/index.ts new file mode 100644 index 0000000000..6737cd2abb --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/index.ts @@ -0,0 +1,20 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { BatchNoConversionPayments } from '../../../types/BatchNoConversionPayments'; + +export const batchNoConversionPaymentsArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x1411CB266FCEd1587b0AA29E9d5a9Ef3Db64A9C5', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.1.0', +); diff --git a/packages/smart-contracts/src/lib/artifacts/index.ts b/packages/smart-contracts/src/lib/artifacts/index.ts index 72b4f2f15e..9a6987fa8d 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -12,6 +12,8 @@ export * from './EthereumFeeProxy'; export * from './EthConversionProxy'; export * from './ERC20EscrowToPay'; export * from './BatchPayments'; +export * from './BatchNoConversionPayments'; +export * from './BatchConversionPayments'; /** * Request Storage */ diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts new file mode 100644 index 0000000000..1bb70ab17e --- /dev/null +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -0,0 +1,845 @@ +import { ethers, network } from 'hardhat'; +import { + ERC20FeeProxy__factory, + Erc20ConversionProxy__factory, + EthConversionProxy__factory, + EthereumFeeProxy__factory, + ChainlinkConversionPath, + TestERC20, + TestERC20__factory, + BatchConversionPayments__factory, + BatchConversionPayments, +} from '../../src/types'; +import { PaymentTypes } from '@requestnetwork/types'; +import { BigNumber, ContractTransaction, Signer } from 'ethers'; +import { expect } from 'chai'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { chainlinkConversionPath } from '../../src/lib'; +import { FAU_USD_RATE } from '../../scripts/test-deploy-batch-conversion-deployment'; +import { localERC20AlphaArtifact, secondLocalERC20AlphaArtifact } from './localArtifacts'; +import Utils from '@requestnetwork/utils'; +import { HttpNetworkConfig } from 'hardhat/types'; +import { + DAI_USD_RATE, + EUR_USD_RATE, + PRECISION_RATE, +} from '../../scripts/test-deploy_chainlink_contract'; + +const BATCH_PAYMENT_NETWORK_ID = PaymentTypes.BATCH_PAYMENT_NETWORK_ID; + +describe('contract: BatchConversionPayments', async () => { + const networkConfig = network.config as HttpNetworkConfig; + const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + + let from: string; + let to: string; + let feeAddress: string; + let adminSigner: Signer; + let fromSigner: Signer; + let signer4: Signer; + let tx: ContractTransaction; + + // constants used to set up batch conversion proxy, and also requests payment + const BATCH_FEE = 50; // .5% + const BATCH_CONV_FEE = 100; // 1% + const BATCH_DENOMINATOR = 10000; + const daiDecimals = '1000000000000000000'; // 10^18 + const fiatDecimals = '00000000'; + const thousandWith18Decimal = '1000000000000000000000'; + const referenceExample = '0xaaaa'; + const gasPrice = 2 * 10 ** 10; // await provider.getGasPrice() + + // constants related to chainlink and conversion rate + const currencyManager = CurrencyManager.getDefault(); + + const ETH_hash = currencyManager.fromSymbol('ETH')!.hash; + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + const EUR_hash = currencyManager.fromSymbol('EUR')!.hash; + const DAI_address = localERC20AlphaArtifact.getAddress(network.name); + const FAU_address = secondLocalERC20AlphaArtifact.getAddress(network.name); + const USD_ETH_RATE = 20000000; + + // proxies and tokens + let batchConversionProxy: BatchConversionPayments; + let daiERC20: TestERC20; + let fauERC20: TestERC20; + let chainlinkPath: ChainlinkConversionPath; + + // constants inputs for batch functions, both conversion and no-conversion + const emptyCryptoDetails: PaymentTypes.CryptoDetails = { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }; + + const fauConvDetail: PaymentTypes.ConversionDetail = { + recipient: '', + requestAmount: '100000' + fiatDecimals, + path: [USD_hash, FAU_address], + paymentReference: referenceExample, + feeAmount: '100' + fiatDecimals, + maxToSpend: '20000000000000000000' + fiatDecimals, // Way enough + maxRateTimespan: '0', + }; + + const daiConvDetail: PaymentTypes.ConversionDetail = { + recipient: '', + requestAmount: '100000' + fiatDecimals, + path: [EUR_hash, USD_hash, DAI_address], + paymentReference: referenceExample, + feeAmount: '100' + fiatDecimals, + maxToSpend: '30000000000000000000' + fiatDecimals, // Way enough + maxRateTimespan: '0', + }; + + const ethConvDetail: PaymentTypes.ConversionDetail = { + recipient: '', + requestAmount: '1000', + path: [USD_hash, ETH_hash], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }; + + before(async () => { + [, from, to, feeAddress] = (await ethers.getSigners()).map((s) => s.address); + [adminSigner, fromSigner, , , signer4] = await ethers.getSigners(); + + chainlinkPath = chainlinkConversionPath.connect(network.name, fromSigner); + + const erc20FeeProxy = await new ERC20FeeProxy__factory(adminSigner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(adminSigner).deploy(); + const erc20ConversionProxy = await new Erc20ConversionProxy__factory(adminSigner).deploy( + erc20FeeProxy.address, + chainlinkPath.address, + await adminSigner.getAddress(), + ); + const ethConversionProxy = await new EthConversionProxy__factory(adminSigner).deploy( + ethFeeProxy.address, + chainlinkPath.address, + ETH_hash, + ); + + batchConversionProxy = await new BatchConversionPayments__factory(adminSigner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + erc20ConversionProxy.address, + ethConversionProxy.address, + await adminSigner.getAddress(), + ); + + fauConvDetail.recipient = to; + daiConvDetail.recipient = to; + ethConvDetail.recipient = to; + + // set batch proxy fees and connect fromSigner + await batchConversionProxy.setBatchFee(BATCH_FEE); + await batchConversionProxy.setBatchConversionFee(BATCH_CONV_FEE); + batchConversionProxy = batchConversionProxy.connect(fromSigner); + + // set ERC20 tokens and transfer token to "from" (fromSigner) + daiERC20 = new TestERC20__factory(adminSigner).attach(DAI_address); + await daiERC20.transfer(from, BigNumber.from(thousandWith18Decimal + '0000000')); + daiERC20 = daiERC20.connect(fromSigner); + + fauERC20 = new TestERC20__factory(adminSigner).attach(FAU_address); + await fauERC20.transfer(from, BigNumber.from(thousandWith18Decimal + '0000000')); + fauERC20 = fauERC20.connect(fromSigner); + + await daiERC20.approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals, { + from, + }); + await fauERC20.approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals, { + from, + }); + }); + + const getERC20Balances = async (testERC20: TestERC20) => { + const fromDAIBalance = await testERC20.balanceOf(from); + const toDAIBalance = await testERC20.balanceOf(to); + const feeDAIBalance = await testERC20.balanceOf(feeAddress); + const batchDAIBalance = await testERC20.balanceOf(batchConversionProxy.address); + return [fromDAIBalance, toDAIBalance, feeDAIBalance, batchDAIBalance]; + }; + + /** Returns BigNumber amounts with DAI decimals */ + const getExpectedConvERC20Balances = ( + amount: number, + fee: number, + nPayment: number, + path: string, + ) => { + // Temporary decimal offset to have a precise conversion with floating rates + const precision = 1_000_000; + const conversionRate = + path === 'EUR_DAI' + ? BigNumber.from(daiDecimals).mul(precision).mul(EUR_USD_RATE).div(DAI_USD_RATE) + : BigNumber.from(daiDecimals).mul(precision).mul(PRECISION_RATE).div(FAU_USD_RATE); + const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); + const expectedFeeDAIBalanceDiff = + // fee added by the batch + expectedToDAIBalanceDiff + .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + // fee within the invoice: .1% of the amount, + .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)); + fee; + const expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff + .add(expectedFeeDAIBalanceDiff) + .mul(-1); + return [ + expectedFromDAIBalanceDiff.div(precision), + expectedToDAIBalanceDiff.div(precision), + expectedFeeDAIBalanceDiff.div(precision), + ]; + }; + + /** No conversion */ + const getExpectedERC20Balances = (amount: number, fee: number, nPayment: number) => { + const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(nPayment); + const expectedFeeDAIBalanceDiff = + // fee added by the batch + expectedToDAIBalanceDiff + .mul(BATCH_FEE) + .div(BATCH_DENOMINATOR) + // fee within the invoice: .1% of the amount, + .add(BigNumber.from(fee).mul(nPayment)); + fee; + const expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff + .add(expectedFeeDAIBalanceDiff) + .mul(-1); + return [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff]; + }; + + /** Compares the expected delta-balances with the one it computes for from, to and fee addresses. */ + const expectERC20BalanceDiffs = async ( + token: 'DAI' | 'FAU', + initialFromBalance: BigNumber, + initialToBalance: BigNumber, + initialFeeBalance: BigNumber, + expectedFromBalanceDiff: BigNumber, + expectedToBalanceDiff: BigNumber, + expectedFeeBalanceDiff: BigNumber, + ) => { + const testERC20 = token === 'FAU' ? fauERC20 : daiERC20; + // Get balances + const [fromBalance, toBalance, feeBalance, batchBalance] = await getERC20Balances(testERC20); + // Compare balance changes to expected values + const fromBalanceDiff = BigNumber.from(fromBalance).sub(initialFromBalance); + const toBalanceDiff = BigNumber.from(toBalance).sub(initialToBalance); + const feeBalanceDiff = BigNumber.from(feeBalance).sub(initialFeeBalance); + + expect(toBalanceDiff).to.equals(expectedToBalanceDiff, `toBalanceDiff in ${token}`); + expect(feeBalanceDiff).to.equals(expectedFeeBalanceDiff, `feeBalanceDiff in ${token}`); + expect(fromBalanceDiff).to.equals(expectedFromBalanceDiff, `fromBalanceDiff in ${token}`); + expect(batchBalance).to.equals('0', `batchBalance in ${token}`); + }; + + /** Compares the expected delta-balances with the one it computes for from, to and fee addresses. */ + const expectETHBalanceDiffs = async ( + ethAmount: BigNumber, + ethFeeAmount: BigNumber, + feeApplied = BATCH_CONV_FEE, + initialFromETHBalance: BigNumber, + initialToETHBalance: BigNumber, + initialFeeETHBalance: BigNumber, + ) => { + const receipt = await tx.wait(); + const gasAmount = receipt.gasUsed.mul(gasPrice); + + const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const toETHBalance = await provider.getBalance(to); + const feeETHBalance = await provider.getBalance(feeAddress); + const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - initial + const fromETHBalanceDiff = initialFromETHBalance.sub(fromETHBalance); + const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); + const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + + const expectedToETHBalanceDiff = ethAmount; + const expectedFeeETHBalanceDiff = expectedToETHBalanceDiff + .add(ethFeeAmount) + .mul(feeApplied) + .div(BATCH_DENOMINATOR) + .add(ethFeeAmount); + const expectedFromETHBalanceDiff = gasAmount + .add(expectedToETHBalanceDiff) + .add(expectedFeeETHBalanceDiff); + + // Check balance changes + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + }; + + /** + * Pays 3 ERC20 conversions payments, with DAI and FAU tokens and it calculates the balances + * It also check the balances expected for FAU token. + */ + const manyPaymentsBatchConv = async (paymentBatch: () => Promise) => { + const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = + await getERC20Balances(daiERC20); + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + + await paymentBatch(); + + // check the balance daiERC20 token + const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 2, 'EUR_DAI'); + await expectERC20BalanceDiffs( + 'DAI', + initialFromDAIBalance, + initialToDAIBalance, + initialFeeDAIBalance, + expectedFromDAIBalanceDiff, + expectedToDAIBalanceDiff, + expectedFeeDAIBalanceDiff, + ); + + // check the balance fauERC20 token + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }; + + describe('batchRouter', async () => { + it(`make 1 ERC20 payment with no conversion`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [FAU_address], + recipients: [to], + amounts: ['100000'], + paymentReferences: [referenceExample], + feeAmounts: ['100'], + }, + }, + ], + feeAddress, + ); + + // check the balance fauERC20 token + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { + const batchPayment = async () => { + return await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ); + }; + await manyPaymentsBatchConv(batchPayment); + }); + + it('make 1 ETH payment without conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [], + recipients: [to], + amounts: ['1000'], + paymentReferences: [referenceExample], + feeAmounts: ['1'], + }, + }, + ], + feeAddress, + { value: 1000 + 1 + 11 }, // + 11 to pay batch fees + ); + + await expectETHBalanceDiffs( + BigNumber.from(1000), + BigNumber.from(1), + BATCH_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); + + it('make 1 ETH payment with 1-step conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + { + value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees + }, + ); + + await expectETHBalanceDiffs( + BigNumber.from(1000 * USD_ETH_RATE), + BigNumber.from(1 * USD_ETH_RATE), + BATCH_CONV_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); + + it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { + // get balances + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + // set inputs: ERC20 cryptoDetails & ethCryptoDetails + const cryptoDetails: PaymentTypes.CryptoDetails = { + tokenAddresses: [FAU_address], + recipients: [to], + amounts: ['100000'], + paymentReferences: [referenceExample], + feeAmounts: ['100'], + }; + const ethCryptoDetails: PaymentTypes.CryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: ['1000'], + paymentReferences: [referenceExample], + feeAmounts: ['1'], + }; + + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [], + cryptoDetails: cryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [], + cryptoDetails: ethCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees + ); + + // Chech FAU Balances // + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + const [ + noConvExpectedFromFAUBalanceDiff, + noConvExpectedToFAUBalanceDiff, + noConvExpectedFeeFAUBalanceDiff, + ] = getExpectedERC20Balances(100000, 100, 1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff.add(noConvExpectedFromFAUBalanceDiff), + expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), + expectedFeeFAUBalanceDiff.add(noConvExpectedFeeFAUBalanceDiff), + ); + + // Check ETH balances // + const receipt = await tx.wait(); + const gasAmount = receipt.gasUsed.mul(gasPrice); + + const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const toETHBalance = await provider.getBalance(to); + const feeETHBalance = await provider.getBalance(feeAddress); + const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - initial + const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); + const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); + const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + + // expectedFeeETHBalanceDiff includes batch conversion fees now + const expectedFeeETHBalanceDiff = + // Batch conversion + BigNumber.from(1000 * USD_ETH_RATE) + .add(1 * USD_ETH_RATE) + .mul(BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR) + // Batch no-conversion + .add(1 * USD_ETH_RATE) + .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); + + const expectedFromETHBalanceDiff = gasAmount + .add(1000 * USD_ETH_RATE + 1000) + .add(expectedFeeETHBalanceDiff) + .mul(-1); + + // Check balance changes + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(toETHBalanceDiff).to.equals( + BigNumber.from(1000 * USD_ETH_RATE + 1000), + 'toETHBalanceDiff', + ); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + }); + }); + describe('batchRouter errors', async () => { + it(`too many elements within batchRouter metaDetails input`, async () => { + await expect( + batchConversionProxy.batchRouter( + Array(6).fill({ + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }), + feeAddress, + ), + ).to.be.revertedWith('more than 5 metaDetails'); + }); + it(`wrong paymentNetworkId set in metaDetails input`, async () => { + await expect( + batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: 6, + conversionDetails: [], + cryptoDetails: emptyCryptoDetails, + }, + ], + feeAddress, + ), + ).to.be.revertedWith('wrong paymentNetworkId'); + }); + }); + describe('batchMultiERC20ConversionPayments', async () => { + it('make 1 payment with 1-step conversion', async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + + await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments([fauConvDetail], feeAddress); + + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it('make 1 payment with 2-steps conversion in DAI', async () => { + const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = + await getERC20Balances(daiERC20); + + await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments([daiConvDetail], feeAddress); + + const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); + + await expectERC20BalanceDiffs( + 'DAI', + initialFromDAIBalance, + initialToDAIBalance, + initialFeeDAIBalance, + expectedFromDAIBalanceDiff, + expectedToDAIBalanceDiff, + expectedFeeDAIBalanceDiff, + ); + }); + it('make 3 payments with different tokens and conversion length', async () => { + const batchPayment = async () => { + return await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments( + [fauConvDetail, daiConvDetail, daiConvDetail], + feeAddress, + ); + }; + await manyPaymentsBatchConv(batchPayment); + }); + }); + describe('batchMultiERC20ConversionPayments errors', async () => { + it('cannot transfer with invalid path', async () => { + const convDetail = Utils.deepCopy(fauConvDetail); + convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('revert No aggregator found'); + }); + + it('cannot transfer if max to spend too low', async () => { + const convDetail = Utils.deepCopy(fauConvDetail); + convDetail.maxToSpend = '1000000'; // not enough + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('Amount to pay is over the user limit'); + }); + + it('cannot transfer if rate is too old', async () => { + const convDetail = Utils.deepCopy(fauConvDetail); + convDetail.maxRateTimespan = '10'; + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('aggregator rate is outdated'); + }); + + it('Not enough allowance', async () => { + const convDetail = Utils.deepCopy(fauConvDetail); + // reduce fromSigner± allowance + await fauERC20.approve( + batchConversionProxy.address, + BigNumber.from(convDetail.maxToSpend).sub(2), + { + from, + }, + ); + await expect( + batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + ).to.be.revertedWith('Insufficient allowance for batch to pay'); + }); + + it('Not enough funds even if partially enough funds', async () => { + const convDetail = Utils.deepCopy(fauConvDetail); + // fromSigner transfer enough token to pay just 1 invoice to signer4 + await fauERC20 + .connect(fromSigner) + .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); + // increase signer4 allowance + await fauERC20 + .connect(signer4) + .approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals); + + // 3 invoices to pay + await expect( + batchConversionProxy + .connect(signer4) + .batchMultiERC20ConversionPayments([convDetail, convDetail, convDetail], feeAddress), + ).to.be.revertedWith('not enough funds, including fees'); + + // signer4 transfer token to fromSigner + await fauERC20 + .connect(signer4) + .transfer(from, await fauERC20.balanceOf(await signer4.getAddress())); + }); + }); + describe(`batchEthConversionPayments`, () => { + it('make 1 payment with 1-step conversion', async function () { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], feeAddress, { + value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees + }); + await expectETHBalanceDiffs( + BigNumber.from(1000 * USD_ETH_RATE), + BigNumber.from(1 * USD_ETH_RATE), + BATCH_CONV_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); + + it('make 3 payments with different conversion lengths', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const EurConvDetail = Utils.deepCopy(ethConvDetail); + EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; + + tx = await batchConversionProxy.batchEthConversionPayments( + [ethConvDetail, EurConvDetail, ethConvDetail], + feeAddress, + { + value: BigNumber.from('100000000000000000'), + }, + ); + await expectETHBalanceDiffs( + BigNumber.from(1000 * USD_ETH_RATE) + .mul(2) + .add(1000 * 24000000), // 24000000 is EUR_ETH_RATE + BigNumber.from(USD_ETH_RATE).mul(2).add(24000000), + BATCH_CONV_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); + }); + describe('batchEthConversionPayments errors', () => { + it('cannot transfer with invalid path', async () => { + const wrongConvDetail = Utils.deepCopy(ethConvDetail); + wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; + await expect( + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees + }), + ).to.be.revertedWith('No aggregator found'); + }); + it('not enough funds even if partially enough funds', async () => { + await expect( + batchConversionProxy.batchEthConversionPayments( + [ethConvDetail, ethConvDetail], + feeAddress, + { + value: (2000 + 1) * USD_ETH_RATE, // no enough to pay the amount AND the fees + }, + ), + ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); + }); + + it('cannot transfer if rate is too old', async () => { + const wrongConvDetail = Utils.deepCopy(ethConvDetail); + wrongConvDetail.maxRateTimespan = '1'; + await expect( + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + value: 1000 + 1 + 11, // + 11 to pay batch fees + }), + ).to.be.revertedWith('aggregator rate is outdated'); + }); + }); + describe('Functions inherited from contract BatchNoConversionPayments ', () => { + it(`make 1 ERC20 payment without conversion, using batchERC20Payments`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchERC20Payments( + FAU_address, + [to], + ['100000'], + [referenceExample], + ['100'], + feeAddress, + ); + + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + + it(`make 1 ERC20 payment without conversion, using batchMultiERC20Payments`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchMultiERC20Payments( + [FAU_address], + [to], + ['100000'], + [referenceExample], + ['100'], + feeAddress, + ); + + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances(100000, 100, 1); + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + + it('make 1 ETH payment without conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchEthPayments( + [to], + ['1000'], + [referenceExample], + ['1'], + feeAddress, + { value: 1000 + 1 + 11 }, // + 11 to pay batch fees + ); + await expectETHBalanceDiffs( + BigNumber.from(1000), + BigNumber.from(1), + BATCH_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); + }); +}); diff --git a/packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts similarity index 90% rename from packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts rename to packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 03daf16761..220704ff2b 100644 --- a/packages/smart-contracts/test/contracts/BatchErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -1,12 +1,19 @@ -import { ethers, network } from 'hardhat'; +import { ethers } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; -import { TestERC20__factory, TestERC20, BatchPayments, ERC20FeeProxy } from '../../src/types'; -import { batchPaymentsArtifact, erc20FeeProxyArtifact } from '../../src/lib'; +import { + TestERC20__factory, + TestERC20, + ERC20FeeProxy, + EthereumFeeProxy__factory, + BatchNoConversionPayments, + ERC20FeeProxy__factory, + BatchNoConversionPayments__factory, +} from '../../src/types'; const logGasInfos = false; -describe('contract: BatchPayments: ERC20', () => { +describe('contract: batchNoConversionPayments: ERC20', () => { let payee1: string; let payee2: string; let payee3: string; @@ -20,7 +27,7 @@ describe('contract: BatchPayments: ERC20', () => { let token1: TestERC20; let token2: TestERC20; let token3: TestERC20; - let batch: BatchPayments; + let batch: BatchNoConversionPayments; let erc20FeeProxy: ERC20FeeProxy; let token1Address: string; @@ -50,8 +57,13 @@ describe('contract: BatchPayments: ERC20', () => { [, payee1, payee2, payee3, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [owner, spender1, spender2, spender3] = await ethers.getSigners(); - erc20FeeProxy = erc20FeeProxyArtifact.connect(network.name, owner); - batch = batchPaymentsArtifact.connect(network.name, owner); + erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); + const ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + batch = await new BatchNoConversionPayments__factory(owner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + await owner.getAddress(), + ); token1 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); token2 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); token3 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); @@ -65,7 +77,7 @@ describe('contract: BatchPayments: ERC20', () => { token3Address = token3.address; batchAddress = batch.address; - await batch.connect(owner).setBatchFee(100); + await batch.connect(owner).setBatchFee(1000); }); beforeEach(async () => { @@ -86,12 +98,8 @@ describe('contract: BatchPayments: ERC20', () => { await token3.connect(spender3).approve(batchAddress, 0); }); - after(async () => { - await batch.connect(owner).setBatchFee(10); - }); - describe('Batch working well: right args, and approvals', () => { - it('Should pay 3 ERC20 payments with paymentRef and pay batch fee', async function () { + it('Should pay 3 ERC20 payments with paymentRef and pay batch fee', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 1000); @@ -102,7 +110,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee2], [200, 30, 40], @@ -160,7 +168,7 @@ describe('contract: BatchPayments: ERC20', () => { ); }); - it('Should pay 3 ERC20 payments Multi tokens with paymentRef and pay batch fee', async function () { + it('Should pay 3 ERC20 payments Multi tokens with paymentRef and pay batch fee', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); await token3.connect(owner).transfer(spender3Address, 1000); @@ -181,7 +189,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token2Address, token3Address], [payee1, payee2, payee2], [500, 300, 400], @@ -252,7 +260,7 @@ describe('contract: BatchPayments: ERC20', () => { ); }); - it('Should pay 3 ERC20 payments Multi tokens, with one payment of 0 token', async function () { + it('Should pay 3 ERC20 payments Multi tokens, with one payment of 0 token', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); await token3.connect(owner).transfer(spender3Address, 1000); @@ -272,7 +280,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token2Address, token3Address], [payee1, payee2, payee2], [500, 0, 400], @@ -297,7 +305,7 @@ describe('contract: BatchPayments: ERC20', () => { ); }); - it('Should pay 4 ERC20 payments on 2 tokens', async function () { + it('Should pay 4 ERC20 payments on 2 tokens', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); @@ -320,7 +328,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( tokenAddresses, recipients, amounts, @@ -343,7 +351,7 @@ describe('contract: BatchPayments: ERC20', () => { expect(beforeERC20Balance3Token2).to.be.equal(afterERC20Balance3Token2.add((20 + 1 + 2) * 2)); }); - it('Should pay 10 ERC20 payments', async function () { + it('Should pay 10 ERC20 payments', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 1000); @@ -359,7 +367,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Addresses[0], recipients, amounts, @@ -382,7 +390,7 @@ describe('contract: BatchPayments: ERC20', () => { ); }); - it('Should pay 10 ERC20 payments on multiple tokens', async function () { + it('Should pay 10 ERC20 payments on multiple tokens', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); @@ -405,7 +413,7 @@ describe('contract: BatchPayments: ERC20', () => { const tx = await batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( tokenAddresses, recipients, amounts, @@ -427,14 +435,14 @@ describe('contract: BatchPayments: ERC20', () => { }); describe('Batch revert, issues with: args, or funds, or approval', () => { - it('Should revert batch if not enough funds to pay the request', async function () { + it('Should revert batch if not enough funds to pay the request', async () => { await token1.connect(owner).transfer(spender3Address, 100); await token1.connect(spender3).approve(batchAddress, 1000); await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 400], @@ -442,17 +450,17 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); - it('Should revert batch if not enough funds to pay the batch fee', async function () { + it('Should revert batch if not enough funds to pay the batch fee', async () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 1000); await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2], [100, 200], @@ -463,13 +471,13 @@ describe('contract: BatchPayments: ERC20', () => { ).revertedWith('not enough funds for the batch fee'); }); - it('Should revert batch without approval', async function () { + it('Should revert batch without approval', async () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 10); await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [20, 30, 40], @@ -477,17 +485,17 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert Not sufficient allowance for batch to pay'); + ).revertedWith('Insufficient allowance for batch to pay'); }); - it('Should revert batch multi tokens if not enough funds', async function () { + it('Should revert batch multi tokens if not enough funds', async () => { await token1.connect(owner).transfer(spender3Address, 400); await token1.connect(spender3).approve(batchAddress, 1000); await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 400], @@ -495,17 +503,17 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); - it('Should revert batch multi tokens if not enough funds to pay the batch fee', async function () { + it('Should revert batch multi tokens if not enough funds to pay the batch fee', async () => { await token1.connect(owner).transfer(spender3Address, 607); await token1.connect(spender3).approve(batchAddress, 1000); await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee2], [100, 200, 300], @@ -513,17 +521,17 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert not enough funds'); + ).revertedWith('not enough funds'); }); - it('Should revert batch multi tokens without approval', async function () { + it('Should revert batch multi tokens without approval', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 10); await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [100, 200, 300], @@ -531,14 +539,14 @@ describe('contract: BatchPayments: ERC20', () => { [1, 2, 3], feeAddress, ), - ).revertedWith('revert Not sufficient allowance for batch to pay'); + ).revertedWith('Insufficient allowance for batch to pay'); }); - it('Should revert batch multi tokens if input s arrays do not have same size', async function () { + it('Should revert batch multi tokens if input s arrays do not have same size', async () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -551,7 +559,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2], [5, 30, 40], @@ -564,7 +572,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30], @@ -577,7 +585,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -590,7 +598,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsMultiTokensWithReference( + .batchMultiERC20Payments( [token1Address, token1Address, token1Address], [payee1, payee2, payee3], [5, 30, 40], @@ -601,11 +609,11 @@ describe('contract: BatchPayments: ERC20', () => { ).revertedWith('the input arrays must have the same length'); }); - it('Should revert batch if input s arrays do not have same size', async function () { + it('Should revert batch if input s arrays do not have same size', async () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 40], @@ -618,7 +626,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2], [5, 30, 40], @@ -631,7 +639,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30], @@ -644,7 +652,7 @@ describe('contract: BatchPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20PaymentsWithReference( + .batchERC20Payments( token1Address, [payee1, payee2, payee3], [5, 30, 40], @@ -657,7 +665,7 @@ describe('contract: BatchPayments: ERC20', () => { }); }); -// Allow to create easly BatchPayments input, especially for gas optimization +// Allow to create easly batchNoConversionPayments input, especially for gas optimization const getBatchPaymentsInputs = function ( nbTxs: number, tokenAddress: string, diff --git a/packages/smart-contracts/test/contracts/BatchEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts similarity index 81% rename from packages/smart-contracts/test/contracts/BatchEthPayments.test.ts rename to packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 1f2fc2d026..dd550678af 100644 --- a/packages/smart-contracts/test/contracts/BatchEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -1,15 +1,17 @@ import { ethers, network } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; -import { EthereumFeeProxy, BatchPayments } from '../../src/types'; -import { batchPaymentsArtifact } from '../../src/lib'; - -import { ethereumFeeProxyArtifact } from '../../src/lib'; +import { + EthereumFeeProxy__factory, + BatchNoConversionPayments__factory, + ERC20FeeProxy__factory, +} from '../../src/types'; +import { EthereumFeeProxy, BatchNoConversionPayments } from '../../src/types'; import { HttpNetworkConfig } from 'hardhat/types'; const logGasInfos = false; -describe('contract: BatchPayments: Ethereum', () => { +describe('contract: batchNoConversionPayments: Ethereum', () => { let payee1: string; let payee2: string; let feeAddress: string; @@ -27,7 +29,7 @@ describe('contract: BatchPayments: Ethereum', () => { const referenceExample2 = '0xbbbb'; let ethFeeProxy: EthereumFeeProxy; - let batch: BatchPayments; + let batch: BatchNoConversionPayments; const networkConfig = network.config as HttpNetworkConfig; const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); @@ -35,18 +37,19 @@ describe('contract: BatchPayments: Ethereum', () => { [, payee1, payee2, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [owner, payee1Sig] = await ethers.getSigners(); - ethFeeProxy = ethereumFeeProxyArtifact.connect(network.name, owner); - batch = batchPaymentsArtifact.connect(network.name, owner); + const erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); + ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + batch = await new BatchNoConversionPayments__factory(owner).deploy( + erc20FeeProxy.address, + ethFeeProxy.address, + await owner.getAddress(), + ); batchAddress = batch.address; - await batch.connect(owner).setBatchFee(10); - }); - - after(async () => { - await batch.connect(owner).setBatchFee(10); + await batch.connect(owner).setBatchFee(100); }); describe('Batch Eth normal flow', () => { - it('Should pay 2 payments and contract do not keep funds of ethers', async function () { + it('Should pay 2 payments and contract do not keep funds of ethers', async () => { const beforeEthBalanceFee = await provider.getBalance(feeAddress); beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); @@ -54,7 +57,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [2000, 3000], [referenceExample1, referenceExample2], @@ -82,7 +85,7 @@ describe('contract: BatchPayments: Ethereum', () => { expect(await provider.getBalance(batchAddress)).to.be.equal(0); }); - it('Should pay 2 payments with the exact amount', async function () { + it('Should pay 2 payments with the exact amount', async () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); @@ -90,7 +93,7 @@ describe('contract: BatchPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -111,7 +114,7 @@ describe('contract: BatchPayments: Ethereum', () => { expect(await provider.getBalance(batchAddress)).to.be.equal(0); }); - it('Should pay 10 Ethereum payments', async function () { + it('Should pay 10 Ethereum payments', async () => { beforeEthBalance2 = await provider.getBalance(payee2); const amount = 2; @@ -129,16 +132,9 @@ describe('contract: BatchPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( - recipients, - amounts, - paymentReferences, - feeAmounts, - feeAddress, - { - value: totalAmount, - }, - ); + .batchEthPayments(recipients, amounts, paymentReferences, feeAmounts, feeAddress, { + value: totalAmount, + }); const receipt = await tx.wait(); if (logGasInfos) { @@ -153,7 +149,7 @@ describe('contract: BatchPayments: Ethereum', () => { }); describe('Batch revert, issues with: args, or funds', () => { - it('Should revert batch if not enough funds', async function () { + it('Should revert batch if not enough funds', async () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); @@ -162,7 +158,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -183,7 +179,7 @@ describe('contract: BatchPayments: Ethereum', () => { expect(await provider.getBalance(batchAddress)).to.be.equal(0); }); - it('Should revert batch if not enough funds for the batch fee', async function () { + it('Should revert batch if not enough funds for the batch fee', async () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); @@ -192,7 +188,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -213,11 +209,11 @@ describe('contract: BatchPayments: Ethereum', () => { expect(await provider.getBalance(batchAddress)).to.be.equal(0); }); - it('Should revert batch if input s arrays do not have same size', async function () { + it('Should revert batch if input s arrays do not have same size', async () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [5, 30], [referenceExample1, referenceExample2], @@ -229,7 +225,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1], [5, 30], [referenceExample1, referenceExample2], @@ -241,7 +237,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [5], [referenceExample1, referenceExample2], @@ -253,13 +249,7 @@ describe('contract: BatchPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPaymentsWithReference( - [payee1, payee2], - [5, 30], - [referenceExample1], - [1, 2], - feeAddress, - ), + .batchEthPayments([payee1, payee2], [5, 30], [referenceExample1], [1, 2], feeAddress), ).revertedWith('the input arrays must have the same length'); expect(await provider.getBalance(batchAddress)).to.be.equal(0); @@ -267,21 +257,21 @@ describe('contract: BatchPayments: Ethereum', () => { }); describe('Function allowed only to the owner', () => { - it('Should allow the owner to update batchFee', async function () { + it('Should allow the owner to update batchFee', async () => { const beforeBatchFee = await batch.batchFee.call({ from: owner }); - let tx = await batch.connect(owner).setBatchFee(beforeBatchFee.add(10)); + let tx = await batch.connect(owner).setBatchFee(beforeBatchFee.add(100)); await tx.wait(); const afterBatchFee = await batch.batchFee.call({ from: owner }); - expect(afterBatchFee).to.be.equal(beforeBatchFee.add(10)); + expect(afterBatchFee).to.be.equal(beforeBatchFee.add(100)); }); - it('Should applied the new batchFee', async function () { + it('Should applied the new batchFee', async () => { // check if batch fee applied are the one updated const beforeFeeAddress = await provider.getBalance(feeAddress); const tx = await batch .connect(owner) - .batchEthPaymentsWithReference( + .batchEthPayments( [payee1, payee2], [200, 300], [referenceExample1, referenceExample2], @@ -297,15 +287,15 @@ describe('contract: BatchPayments: Ethereum', () => { expect(afterFeeAddress).to.be.equal(beforeFeeAddress.add(10 + 20 + (4 + 6))); // fee: (10+20), batch fee: (4+6) }); - it('Should revert if it is not the owner that try to update batchFee', async function () { - await expect(batch.connect(payee1Sig).setBatchFee(30)).revertedWith( - 'revert Ownable: caller is not the owner', + it('Should revert if it is not the owner that try to update batchFee', async () => { + await expect(batch.connect(payee1Sig).setBatchFee(300)).revertedWith( + 'Ownable: caller is not the owner', ); }); }); }); -// Allow to create easly BatchPayments input, especially for gas optimization. +// Allow to create easly batchNoConversionPayments input, especially for gas optimization. const getBatchPaymentsInputs = function ( nbTxs: number, tokenAddress: string, diff --git a/packages/smart-contracts/test/contracts/localArtifacts.ts b/packages/smart-contracts/test/contracts/localArtifacts.ts index 45114d99a6..f87e518d27 100644 --- a/packages/smart-contracts/test/contracts/localArtifacts.ts +++ b/packages/smart-contracts/test/contracts/localArtifacts.ts @@ -16,6 +16,21 @@ export const localERC20AlphaArtifact = new ContractArtifact( '0.0.1', ); +export const secondLocalERC20AlphaArtifact = new ContractArtifact( + { + '0.0.1': { + abi: [], + deployment: { + private: { + address: '0xe4e47451AAd6C89a6D9E4aD104A7b77FfE1D3b36', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.0.1', +); + export const localUSDTArtifact = new ContractArtifact( { '0.0.1': { diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index a0496013ac..87b1544f39 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -320,3 +320,41 @@ export type AllNetworkRetrieverEvents = { paymentEvents: TPaymentNetworkEventType[]; escrowEvents?: EscrowNetworkEvent[]; }; + +// Types used by batch conversion smart contract +/** Input type used by batch conversion proxy to make an ERC20/ETH conversion payment */ +export interface ConversionDetail { + recipient: string; + requestAmount: string; + path: string[]; + paymentReference: string; + feeAmount: string; + maxToSpend: string; + maxRateTimespan: string; +} + +/** Input type used by batch conversion proxy to make an ERC20/ETH no-conversion payment */ +export interface CryptoDetails { + tokenAddresses: Array; + recipients: Array; + amounts: Array; + paymentReferences: Array; + feeAmounts: Array; +} + +/** Each paymentNetworkId is linked with a batch function */ +export enum BATCH_PAYMENT_NETWORK_ID { + BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + BATCH_ERC20_PAYMENTS, + BATCH_MULTI_ERC20_PAYMENTS, + BATCH_ETH_PAYMENTS, + BATCH_ETH_CONVERSION_PAYMENTS, +} + +/** Input type used by batch conversion proxy to make an ERC20 & ETH, + * and conversion & no-conversion payment through batchRouter */ +export interface MetaDetail { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID; + conversionDetails: ConversionDetail[]; + cryptoDetails: CryptoDetails; +} From 715bfd605fa002c47b9769493727a98fd5413cd3 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:07:08 +0200 Subject: [PATCH 007/207] feat(payment-processor): batch conversion - erc20 (#903) * batch erc20 conversion contract and local deployment batch conv tests rename files clean batch tests batch deploy cleaning uncomment batchERC20ConversionPaymentsMultiTokensEasy test test: clean logs * tests batchPayments functions * clear batch contract * add comments in smart contract * add batchEthConversionPaymentsWithReference add atchEth tested and contracts cleaned * refacto batch contracts approval functions * PR - update batch contact - function visibility and comments * keep prefix underscore usage for function args * convention naming in batch contract and more comments * batchConv - delete chainlink implementation * batchConv erc20 - delete a require - add error tests * prettier contract * doc: modify command to create request (#880) * refactor: command to create request * fix: escrow audit fix 2 (#878) * fix(smart-contracts): update batch fees (#873) update batch fees from 1% to .3% * feat: add cancel stream function (#884) * refactor: contract setup compression fix (#888) * fix: escrow audit fix 2 (#878) * fix(smart-contracts): update batch fees (#873) update batch fees from 1% to .3% * feat: add cancel stream function (#884) * refactor: contract setup compression fix (#888) * feat: goerli storage (#890) feat: squash commits goerli storage * fix: delete ETHConversionProxy Co-authored-by: Darko Kolev Co-authored-by: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Co-authored-by: Bertrand Juglas * chore: update escrow addresses (#886) * feat: goerli payment (#892) * refactor: factorize goerli tests (#893) * refactor: factorize goerli tests * refactor: testSuite to testProvider * fix: delete goerli aggregator * feat: add void goerli chainlinkAggregator * feat: add goerli support (advanced-logic) (#895) feat: add goerli support * tmp * any-to-erc20-proxy payment - correct documention on proxy used * wip * refacto receive function - add comments to test functions * comments about unique token in smart contract * prettier * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchPaymentsPublic.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * Update packages/smart-contracts/src/contracts/BatchConversionPayments.sol Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> * receive comment * tmp tests fail * batch calcul updated and tested - it includes fees now * delete basicFee inside the contract - update test - refacto script to deploy and local artifact * check the addresses of the contracts deployed and raise an error if needed * smart contract add variable tenThousand * contract require message - wording * smart contract delete chainlink * revert batchPayments modif * contract - remove irrelevant variable - wording uTokens * renaming requestInfo into conversionDetail and requestsInfoParent into cryptoDetails * contract - clean constructor * wording receive comments add batchEthPaymentsWithReference tests * clean packages * wip * refacto batch conversion ERC20 tests * eth batch functions tested * rewording and cleaning * refacto tests batch conversion ERC20 * refacto tests: batch conversion Eth * test erc20 can be run multiple time - revoke these approvals * test swap-erc-20-fee-proxy revoke approval * refacto any-to-erc20-proxy to create the function checkRequestAndGetPathAndCurrency * prettier * prettier test swap * batch conversion proxy functions and approvals * test batch erc20 * tests batch conversion payment done * prettier * clean tests and restore previous batchFee values * test refacto to simplify batchConvFunction * update batch conversion contract * clean escrow * version with errors on 2 proxies * make proxies public * batch conversion deploy on rinkeby * batch conversion deployed on goerli * clean test * update contract to make proxies public * fix import getPaymentNetworkExtension and its paymentReference undefined possibility * fix error message * rename batch conversion file * test refacto: delete emitOneTx - path and add before - adminSigner * deploy instead of connect to batchProxy * end cleaning old test version * refacto new test structure * refacto test add eth beginning * tests batchRouter erros * abi update prettier contract comments deploy and addresses * batch conv tests delete proxy global variables * test refactored * tests: cleaning * update batchConversionPayments proxies on rinkeby, goerli, and matic * add type for batch payment inputs * update processor with type * refacto feeAddress * refacto batch payment * clean history * refacto batch-conversion-proxy * refacto tests * cleaning * tests cleaning * refacto from PR comments * use type guards for ERC20 and ISO4217 currencies * PR comments B * add comments * convention if * Add every params description to functions * add batch proxy addresses Co-authored-by: Romain <45540622+rom1trt@users.noreply.github.com> Co-authored-by: Darko Kolev Co-authored-by: Bertrand Juglas Co-authored-by: Yo <56731761+yomarion@users.noreply.github.com> --- packages/payment-processor/src/index.ts | 1 + .../src/payment/any-to-erc20-proxy.ts | 116 ++- .../src/payment/batch-conversion-proxy.ts | 375 +++++++++ .../src/payment/batch-proxy.ts | 44 +- .../payment-processor/src/payment/index.ts | 20 +- .../payment-processor/src/payment/utils.ts | 54 +- .../payment/any-to-erc20-batch-proxy.test.ts | 711 ++++++++++++++++++ .../test/payment/swap-erc20-fee-proxy.test.ts | 8 + .../BatchConversionPayments/index.ts | 36 + 9 files changed, 1303 insertions(+), 62 deletions(-) create mode 100644 packages/payment-processor/src/payment/batch-conversion-proxy.ts create mode 100644 packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index fc48075b2c..0bbf676fbc 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -9,6 +9,7 @@ export * from './payment/near-input-data'; export * from './payment/eth-proxy'; export * from './payment/eth-fee-proxy'; export * from './payment/batch-proxy'; +export * from './payment/batch-conversion-proxy'; export * from './payment/swap-conversion-erc20'; export * from './payment/swap-any-to-erc20'; export * from './payment/swap-erc20'; 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 a557f826d7..8f81445381 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-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 { AnyToERC20PaymentDetector } from '@requestnetwork/payment-detection'; import { Erc20ConversionProxy__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes, RequestLogicTypes } from '@requestnetwork/types'; @@ -20,13 +24,13 @@ import { IConversionPaymentSettings } from './index'; /** * Processes a transaction to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). - * The payment is made by the ERC20 fee proxy contract. - * @param request the request to pay - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings payment settings - * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. - * @param feeAmount optionally, the fee amount to pay. Defaults to the fee amount. - * @param overrides optionally, override default transaction values, like gas. + * The payment is made by the ERC20 Conversion fee proxy contract. + * @param request The request to pay + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings + * @param amount Optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmount Optionally, the fee amount to pay. Defaults to the fee amount. + * @param overrides Optionally, override default transaction values, like gas. */ export async function payAnyToErc20ProxyRequest( request: ClientTypes.IRequestData, @@ -47,12 +51,12 @@ export async function payAnyToErc20ProxyRequest( } /** - * Encodes the call to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). The payment is made by the ERC20 fee proxy contract. - * @param request request to pay - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings payment settings - * @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. + * Encodes the call to pay a request with an ERC20 currency that is different from the request currency (eg. fiat). + * The payment is made by the ERC20 Conversion fee proxy contract. + * @param request The request to pay + * @param paymentSettings The payment settings + * @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 encodePayAnyToErc20ProxyRequest( request: ClientTypes.IRequestData, @@ -60,6 +64,41 @@ export function encodePayAnyToErc20ProxyRequest( amount?: BigNumberish, feeAmountOverride?: BigNumberish, ): string { + const { + path, + paymentReference, + paymentAddress, + feeAddress, + maxRateTimespan, + amountToPay, + feeToPay, + } = prepareAnyToErc20Arguments(request, paymentSettings, amount, feeAmountOverride); + const proxyContract = Erc20ConversionProxy__factory.createInterface(); + return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ + paymentAddress, + amountToPay, + path, + `0x${paymentReference}`, + feeToPay, + feeAddress || constants.AddressZero, + BigNumber.from(paymentSettings.maxToSpend), + maxRateTimespan || 0, + ]); +} + +/** + * It checks paymentSettings values, it get request's path and requestCurrency + * @param request The request to pay + * @param paymentSettings The payment settings + * @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( + 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'); } @@ -94,24 +133,53 @@ export function encodePayAnyToErc20ProxyRequest( // Check request validateConversionFeeProxyRequest(request, path, amount, feeAmountOverride); + return { path, requestCurrency }; +} + +/** + * Prepares all necessaries arguments required to encode an any-to-erc20 request + * @param request The request to pay + * @param paymentSettings The payment settings + * @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. + */ +function prepareAnyToErc20Arguments( + request: ClientTypes.IRequestData, + paymentSettings: IConversionPaymentSettings, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): { + path: string[]; + paymentReference: string; + paymentAddress: string; + feeAddress: string | undefined; + maxRateTimespan: string | undefined; + amountToPay: BigNumber; + feeToPay: BigNumber; +} { + const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( + request, + paymentSettings, + amount, + feeAmountOverride, + ); const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues(request); - + if (!paymentReference) { + throw new Error('paymentReference is missing'); + } const amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); - - const proxyContract = Erc20ConversionProxy__factory.createInterface(); - return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ + return { + path, + paymentReference, paymentAddress, + feeAddress, + maxRateTimespan, amountToPay, - path, - `0x${paymentReference}`, feeToPay, - feeAddress || constants.AddressZero, - BigNumber.from(paymentSettings.maxToSpend), - maxRateTimespan || 0, - ]); + }; } export function prepareAnyToErc20ProxyPaymentTransaction( diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts new file mode 100644 index 0000000000..60715f6357 --- /dev/null +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -0,0 +1,375 @@ +import { ContractTransaction, Signer, providers, BigNumber, constants } from 'ethers'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { BatchConversionPayments__factory } from '@requestnetwork/smart-contracts/types'; +import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; +import { ITransactionOverrides } from './transaction-overrides'; +import { + comparePnTypeAndVersion, + getPnAndNetwork, + getProvider, + getProxyAddress, + getRequestPaymentValues, + getSigner, +} from './utils'; +import { + padAmountForChainlink, + getPaymentNetworkExtension, +} from '@requestnetwork/payment-detection'; +import { IPreparedTransaction } from './prepared-transaction'; +import { EnrichedRequest, IConversionPaymentSettings } from './index'; +import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; +import { getBatchArgs } from './batch-proxy'; +import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; +import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; +import { IState } from 'types/dist/extension-types'; +import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwork/currency/dist'; + +/** + * Processes a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) + * The payment is made through ERC20 or ERC20Conversion proxies + * It can be used with a Multisig contract + * @param enrichedRequests List of EnrichedRequests to pay + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides Optionally, override default transaction values, like gas. + * @dev We only implement batchRouter using two ERC20 functions: + * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. + */ +export async function payBatchConversionProxyRequest( + enrichedRequests: EnrichedRequest[], + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, version); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Prepares a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) + * it can be used with a Multisig contract. + * @param enrichedRequests List of EnrichedRequests to pay + * @param version The version of the batch conversion proxy + */ +export function prepareBatchConversionPaymentTransaction( + enrichedRequests: EnrichedRequest[], + version: string, +): IPreparedTransaction { + const encodedTx = encodePayBatchConversionRequest(enrichedRequests); + const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); + return { + data: encodedTx, + to: proxyAddress, + value: 0, + }; +} + +/** + * Encodes a transaction to pay a batch of requests with an ERC20 currency + * that is different from the request currency (eg. fiat) + * It can be used with a Multisig contract. + * @param enrichedRequests List of EnrichedRequests to pay + */ +export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { + const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); + + const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1]; + let firstConversionRequestExtension: IState | undefined; + let firstNoConversionRequestExtension: IState | undefined; + const requestsWithoutConversion: ClientTypes.IRequestData[] = []; + const conversionDetails: PaymentTypes.ConversionDetail[] = []; + + // fill conversionDetails and requestsWithoutConversion lists + for (const enrichedRequest of enrichedRequests) { + if ( + enrichedRequest.paymentNetworkId === + BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS + ) { + firstConversionRequestExtension = + firstConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); + + comparePnTypeAndVersion(firstConversionRequestExtension, enrichedRequest.request); + if ( + !( + isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) || + isISO4217Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) + ) + ) { + throw new Error(`wrong request currencyInfo type`); + } + conversionDetails.push(getInputConversionDetail(enrichedRequest)); + } else if ( + enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS + ) { + firstNoConversionRequestExtension = + firstNoConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); + + // isERC20Currency is checked within getBatchArgs function + comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request); + requestsWithoutConversion.push(enrichedRequest.request); + } + if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1]) + throw new Error('All the requests must have the same network'); + } + + const metaDetails: PaymentTypes.MetaDetail[] = []; + // Add conversionDetails to metaDetails + if (conversionDetails.length > 0) { + metaDetails.push({ + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: conversionDetails, + cryptoDetails: { + tokenAddresses: [], + recipients: [], + amounts: [], + paymentReferences: [], + feeAmounts: [], + }, // cryptoDetails is not used with paymentNetworkId 0 + }); + } + + // Get values and add cryptoDetails to metaDetails + if (requestsWithoutConversion.length > 0) { + const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = + getBatchArgs(requestsWithoutConversion, 'ERC20'); + + // add ERC20 no-conversion payments + metaDetails.push({ + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: tokenAddresses, + recipients: paymentAddresses, + amounts: amountsToPay.map((x) => x.toString()), + paymentReferences: paymentReferences, + feeAmounts: feesToPay.map((x) => x.toString()), + }, + }); + } + + const proxyContract = BatchConversionPayments__factory.createInterface(); + return proxyContract.encodeFunctionData('batchRouter', [ + metaDetails, + feeAddress || constants.AddressZero, + ]); +} + +/** + * Get the conversion detail values from one enriched request + * @param enrichedRequest The enrichedRequest to pay + */ +function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.ConversionDetail { + const paymentSettings = enrichedRequest.paymentSettings; + if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); + + const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( + enrichedRequest.request, + paymentSettings, + ); + + const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( + enrichedRequest.request, + ); + + const requestAmount = BigNumber.from(enrichedRequest.request.expectedAmount).sub( + enrichedRequest.request.balance?.balance || 0, + ); + + const padRequestAmount = padAmountForChainlink(requestAmount, requestCurrency); + const padFeeAmount = padAmountForChainlink(feeAmount || 0, requestCurrency); + return { + recipient: paymentAddress, + requestAmount: padRequestAmount.toString(), + path: path, + paymentReference: `0x${paymentReference}`, + feeAmount: padFeeAmount.toString(), + maxToSpend: paymentSettings.maxToSpend.toString(), + maxRateTimespan: maxRateTimespan || '0', + }; +} + +/** + * + * @param network The network targeted + * @param version The version of the batch conversion proxy + * @returns + */ +function getBatchDeploymentInformation( + network: string, + version: string, +): { address: string } | null { + return { address: batchConversionPaymentsArtifact.getAddress(network, version) }; +} + +/** + * Gets batch conversion contract Address + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy + */ +export function getBatchConversionProxyAddress( + request: ClientTypes.IRequestData, + version: string, +): string { + return getProxyAddress(request, getBatchDeploymentInformation, version); +} + +/** + * ERC20 Batch conversion proxy approvals methods + */ + +/** + * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. + * @param request The request for an ERC20 payment with/out conversion + * @param account The account that will be used to pay the request + * @param version The version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param overrides Optionally, override default transaction values, like gas. + */ +export async function approveErc20BatchConversionIfNeeded( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): Promise { + if ( + !(await hasErc20BatchConversionApproval( + request, + account, + version, + signerOrProvider, + paymentSettings, + )) + ) { + return approveErc20BatchConversion( + request, + version, + getSigner(signerOrProvider), + paymentSettings, + overrides, + ); + } +} + +/** + * Checks if the batch conversion proxy has the necessary allowance from a given account + * to pay a given request with ERC20 batch conversion proxy + * @param request The request for an ERC20 payment with/out conversion + * @param account The account that will be used to pay the request + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval + */ +export async function hasErc20BatchConversionApproval( + request: ClientTypes.IRequestData, + account: string, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, +): Promise { + return checkErc20Allowance( + account, + getBatchConversionProxyAddress(request, version), + signerOrProvider, + getTokenAddress(request, paymentSettings), + request.expectedAmount, + ); +} + +/** + * Processes the transaction to approve the batch conversion proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy, which can be different from request pn version + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param overrides Optionally, override default transaction values, like gas. + */ +export async function approveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): Promise { + const preparedTx = prepareApproveErc20BatchConversion( + request, + version, + signerOrProvider, + paymentSettings, + overrides, + ); + const signer = getSigner(signerOrProvider); + const tx = await signer.sendTransaction(preparedTx); + return tx; +} + +/** + * Prepare the transaction to approve the proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param overrides Optionally, override default transaction values, like gas. + */ +export function prepareApproveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, + overrides?: ITransactionOverrides, +): IPreparedTransaction { + const encodedTx = encodeApproveErc20BatchConversion( + request, + version, + signerOrProvider, + paymentSettings, + ); + return { + data: encodedTx, + to: getTokenAddress(request, paymentSettings), + value: 0, + ...overrides, + }; +} + +/** + * Encodes the transaction to approve the batch conversion proxy to spend signer's tokens to pay + * the request in its payment currency. Can be used with a Multisig contract. + * @param request The request for an ERC20 payment with/out conversion + * @param version The version of the batch conversion proxy + * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. + * @param paymentSettings The payment settings are necessary for conversion payment approval + */ +export function encodeApproveErc20BatchConversion( + request: ClientTypes.IRequestData, + version: string, + signerOrProvider: providers.Provider | Signer = getProvider(), + paymentSettings?: IConversionPaymentSettings, +): string { + const proxyAddress = getBatchConversionProxyAddress(request, version); + return encodeApproveAnyErc20( + getTokenAddress(request, paymentSettings), + proxyAddress, + getSigner(signerOrProvider), + ); +} + +/** + * Get the address of the token to interact with, + * if it is a conversion payment, the info is inside paymentSettings + * @param request The request for an ERC20 payment with/out conversion + * @param paymentSettings The payment settings are necessary for conversion payment + * */ +function getTokenAddress( + request: ClientTypes.IRequestData, + paymentSettings?: IConversionPaymentSettings, +): string { + return paymentSettings ? paymentSettings.currency!.value : request.currencyInfo.value; +} diff --git a/packages/payment-processor/src/payment/batch-proxy.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 8872c8eea1..531daac2be 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -34,7 +34,7 @@ import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; * Processes a transaction to pay a batch of ETH Requests with fees. * Requests paymentType must be "ETH" or "ERC20" * @param requests List of requests - * @param version version of the batch proxy, which can be different from request pn version + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param batchFee Only for batch ETH: additional fee applied to a batch, between 0 and 1000, default value = 10 * @param overrides optionally, override default transaction values, like gas. @@ -55,7 +55,7 @@ export async function payBatchProxyRequest( * Prepate the transaction to pay a batch of requests through the batch proxy contract, can be used with a Multisig contract. * Requests paymentType must be "ETH" or "ERC20" * @param requests list of ETH requests to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param version The version version of the batch proxy, which can be different from request pn version * @param batchFee additional fee applied to a batch */ export function prepareBatchPaymentTransaction( @@ -116,9 +116,7 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str const pn = getPaymentNetworkExtension(requests[0]); for (let i = 0; i < requests.length; i++) { validateErc20FeeProxyRequest(requests[i]); - if (!comparePnTypeAndVersion(pn, requests[i])) { - throw new Error(`Every payment network type and version must be identical`); - } + comparePnTypeAndVersion(pn, requests[i]); } if (isMultiTokens) { @@ -155,10 +153,14 @@ export function encodePayBatchRequest(requests: ClientTypes.IRequestData[]): str /** * Get batch arguments * @param requests List of requests + * @param forcedPaymentType It force to considere the request as an ETH or an ERC20 payment * @returns List with the args required by batch Eth and Erc20 functions, * @dev tokenAddresses returned is for batch Erc20 functions */ -function getBatchArgs(requests: ClientTypes.IRequestData[]): { +export function getBatchArgs( + requests: ClientTypes.IRequestData[], + forcedPaymentType?: 'ETH' | 'ERC20', +): { tokenAddresses: Array; paymentAddresses: Array; amountsToPay: Array; @@ -173,7 +175,7 @@ function getBatchArgs(requests: ClientTypes.IRequestData[]): { const feesToPay: Array = []; let feeAddressUsed = constants.AddressZero; - const paymentType = requests[0].currencyInfo.type; + const paymentType = forcedPaymentType ?? requests[0].currencyInfo.type; for (let i = 0; i < requests.length; i++) { if (paymentType === 'ETH') { validateEthFeeProxyRequest(requests[i]); @@ -208,8 +210,8 @@ function getBatchArgs(requests: ClientTypes.IRequestData[]): { /** * Get Batch contract Address - * @param request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version */ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: string): string { const pn = getPaymentNetworkExtension(request); @@ -232,9 +234,9 @@ export function getBatchProxyAddress(request: ClientTypes.IRequestData, version: /** * Processes the approval transaction of the targeted ERC20 with batch proxy. - * @param request request to pay - * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param account The account that will be used to pay the request + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -253,9 +255,9 @@ export async function approveErc20BatchIfNeeded( /** * Checks if the batch proxy has the necessary allowance from a given account * to pay a given request with ERC20 batch - * @param request request to pay - * @param account account that will be used to pay the request - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param account The account that will be used to pay the request + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ export async function hasErc20BatchApproval( @@ -276,8 +278,8 @@ export async function hasErc20BatchApproval( /** * Processes the transaction to approve the batch proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -296,8 +298,8 @@ export async function approveErc20Batch( /** * Prepare the transaction to approve the proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ @@ -320,8 +322,8 @@ export function prepareApproveErc20Batch( /** * Encodes the transaction to approve the batch proxy to spend signer's tokens to pay * the request in its payment currency. Can be used with a Multisig contract. - * @param request request to pay - * @param version version of the batch proxy, which can be different from request pn version + * @param request The request to pay + * @param version The version version of the batch proxy, which can be different from request pn version * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. */ export function encodeApproveErc20Batch( diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index d5bc316573..ee1f039db6 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -1,6 +1,6 @@ import { ContractTransaction, Signer, BigNumber, BigNumberish, providers } from 'ethers'; -import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; +import { ClientTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types'; import { getBtcPaymentUrl } from './btc-address-based'; import { _getErc20PaymentUrl, getAnyErc20Balance } from './erc20'; @@ -326,3 +326,21 @@ 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 + * Also used in Invoicing repository. + * @dev next step: paymentNetworkId could get more values options, see the "ref" + * in batchConversionPayment.sol + */ +export interface EnrichedRequest { + paymentNetworkId: + | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS + | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS; + request: ClientTypes.IRequestData; + paymentSettings?: IConversionPaymentSettings; + amount?: BigNumberish; + feeAmount?: BigNumberish; +} diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index cd570bbafc..f107162cf1 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -119,7 +119,11 @@ export function getPaymentExtensionVersion(request: ClientTypes.IRequestData): s return extension.version; } -const getProxyNetwork = ( +/** + * @param pn It contains the payment network extension + * @param currency It contains the currency information + */ +export const getProxyNetwork = ( pn: ExtensionTypes.IState, currency: RequestLogicTypes.ICurrency, ): string => { @@ -132,18 +136,34 @@ const getProxyNetwork = ( throw new Error('Payment currency must have a network'); }; -export const getProxyAddress = ( +/** + * @param request The request to pay + * @return A list that contains the payment network extension and the currency information + */ +export function getPnAndNetwork( request: ClientTypes.IRequestData, - getDeploymentInformation: (network: string, version: string) => { address: string } | null, -): string => { +): [ExtensionTypes.IState, string] { const pn = getPaymentNetworkExtension(request); if (!pn) { throw new Error('PaymentNetwork not found'); } - const network = getProxyNetwork(pn, request.currencyInfo); - const deploymentInfo = getDeploymentInformation(network, pn.version); + return [pn, getProxyNetwork(pn, request.currencyInfo)]; +} + +/** + * @param request The request to pay + * @param getDeploymentInformation The function to get the proxy address + * @param version The version has to be set to get batch conversion proxy + */ +export const getProxyAddress = ( + request: ClientTypes.IRequestData, + getDeploymentInformation: (network: string, version: string) => { address: string } | null, + version?: string, +): string => { + const [pn, network] = getPnAndNetwork(request); + const deploymentInfo = getDeploymentInformation(network, version || pn.version); if (!deploymentInfo) { - throw new Error(`No deployment found for network ${network}, version ${pn.version}`); + throw new Error(`No deployment found for network ${network}, version ${version || pn.version}`); } return deploymentInfo.address; }; @@ -273,7 +293,6 @@ export function validateConversionFeeProxyRequest( PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, ); const { tokensAccepted } = getRequestPaymentValues(request); - const requestCurrencyHash = path[0]; if (requestCurrencyHash.toLowerCase() !== getCurrencyHash(request.currencyInfo).toLowerCase()) { throw new Error(`The first entry of the path does not match the request currency`); @@ -316,18 +335,21 @@ export function getAmountToPay( /** * Compare 2 payment networks type and version in request's extension - * @param pn payment network - * @param request - * @returns true if type and version are identique else false + * and throw an exception if they are different + * @param pn The payment network extension + * @param request The request to pay */ export function comparePnTypeAndVersion( pn: ExtensionTypes.IState | undefined, request: ClientTypes.IRequestData, -): boolean { - return ( - pn?.type === getPaymentNetworkExtension(request)?.type && - pn?.version === getPaymentNetworkExtension(request)?.version - ); +): void { + const extension = getPaymentNetworkExtension(request); + if (!extension) { + throw new Error('no payment network found'); + } + if (!(pn?.type === extension.type && pn?.version === extension.version)) { + throw new Error(`Every payment network type and version must be identical`); + } } /** diff --git a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts new file mode 100644 index 0000000000..4b94551b61 --- /dev/null +++ b/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts @@ -0,0 +1,711 @@ +import { Wallet, providers, BigNumber } from 'ethers'; + +import { + ClientTypes, + ExtensionTypes, + IdentityTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { getErc20Balance } from '../../src/payment/erc20'; +import Utils from '@requestnetwork/utils'; +import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; +import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; +import { currencyManager } from './shared'; +import { + approveErc20BatchConversionIfNeeded, + getBatchConversionProxyAddress, + payBatchConversionProxyRequest, + prepareBatchConversionPaymentTransaction, +} from '../../src/payment/batch-conversion-proxy'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; + +/* eslint-disable no-magic-numbers */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +/** Used to to calculate batch fees */ +const BATCH_DENOMINATOR = 10000; +const BATCH_FEE = 30; +const BATCH_CONV_FEE = 30; + +const batchConvVersion = '0.1.0'; +const DAITokenAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; +const FAUTokenAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); + +// Cf. ERC20Alpha in TestERC20.sol +const alphaPaymentSettings: IConversionPaymentSettings = { + currency: { + type: RequestLogicTypes.CURRENCY.ERC20, + value: DAITokenAddress, + network: 'private', + }, + maxToSpend: '10000000000000000000000000000', + currencyManager, +}; + +// requests setting + +const EURExpectedAmount = 100; +const EURFeeAmount = 2; +// amounts used for DAI and FAU requests +const expectedAmount = 100000; +const feeAmount = 100; + +const EURValidRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'EUR', + currencyInfo: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [], + expectedAmount: EURExpectedAmount, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: EURFeeAmount, + paymentAddress, + salt: 'salt', + network: 'private', + tokensAccepted: [DAITokenAddress], + }, + version: '0.1.0', + }, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; + +const DAIValidRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'DAI', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20 as any, + value: DAITokenAddress, + }, + events: [], + expectedAmount: expectedAmount, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; + +const FAUValidRequest = Utils.deepCopy(DAIValidRequest) as ClientTypes.IRequestData; +FAUValidRequest.currencyInfo = { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20 as any, + value: FAUTokenAddress, +}; + +let enrichedRequests: EnrichedRequest[] = []; +// EUR and FAU requests modified within tests to throw errors +let EURRequest: ClientTypes.IRequestData; +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 => { + // 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); +}; + +describe('erc20-batch-conversion-proxy', () => { + beforeAll(async () => { + // Revoke DAI and FAU approvals + await revokeErc20Approval( + getBatchConversionProxyAddress(DAIValidRequest, batchConvVersion), + DAITokenAddress, + wallet, + ); + await revokeErc20Approval( + getBatchConversionProxyAddress(FAUValidRequest, batchConvVersion), + FAUTokenAddress, + wallet, + ); + }); + + describe(`Conversion:`, () => { + beforeEach(() => { + jest.restoreAllMocks(); + EURRequest = Utils.deepCopy(EURValidRequest); + enrichedRequests = [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURRequest, + paymentSettings: alphaPaymentSettings, + }, + ]; + }); + + describe('Throw an error', () => { + it('should throw an error if the token is not accepted', async () => { + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: { + ...alphaPaymentSettings, + currency: { + ...alphaPaymentSettings.currency, + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + }, + } as IConversionPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError( + new UnsupportedCurrencyError({ + value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', + network: 'private', + }), + ); + }); + it('should throw an error if request has no paymentSettings', async () => { + await expect( + payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURRequest, + paymentSettings: undefined, + }, + ], + batchConvVersion, + wallet, + ), + ).rejects.toThrowError('the enrichedRequest has no paymentSettings'); + }); + it('should throw an error if the request is ETH', async () => { + EURRequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).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 + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + network: 'fakePrivate', + }, + version: '0.1.0', + }, + }; + + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('All the requests must have the same network'); + }); + it('should throw an error if the request has a wrong payment network id', async () => { + EURRequest.extensions = { + // ERC20_FEE_PROXY_CONTRACT instead of ANY_TO_ERC20_PROXY + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: feeAmount, + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }; + + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', + ); + }); + it("should throw an error if one request's currencyInfo has no value", async () => { + EURRequest.currencyInfo.value = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError("The currency '' is unknown or not supported"); + }); + it('should throw an error if request has no extension', async () => { + EURRequest.extensions = [] as any; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('no payment network found'); + }); + it('should throw an error if there is a wrong version mapping', async () => { + EURRequest.extensions = { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: { + ...EURRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY], + version: '0.3.0', + }, + }; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Every payment network type and version must be identical'); + }); + }); + + describe('payment', () => { + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + { gasPrice: '20000000000' }, + ); + expect(spy).toHaveBeenCalledWith({ + data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + gasPrice: '20000000000', + to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'), + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + it('should convert and pay a request in EUR with ERC20', async () => { + // Approve the contract + const approvalTx = await approveErc20BatchConversionIfNeeded( + EURValidRequest, + wallet.address, + batchConvVersion, + wallet.provider, + alphaPaymentSettings, + ); + expect(approvalTx).toBeDefined(); + if (approvalTx) { + await approvalTx.wait(1); + } + + // Get the balances to compare after payment + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, + provider, + ); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toEqual(1); + expect(tx.hash).toBeDefined(); + + // Get the new balances + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); + + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount); + const feeToPay = expectedConversionAmount(EURFeeAmount); + const expectedAmountToPay = amountToPay + .add(feeToPay) + .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR); + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + expect( + BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)), + // Calculation of expectedAmountToPay + // expectedAmount: 1.00 + // feeAmount: + .02 + // = 1.02 + // 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 two requests in EUR with ERC20', async () => { + // Get initial balances + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, + provider, + ); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + Array(2).fill({ + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }), + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + 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 + expect( + BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), + ).toBeGreaterThan(0); + + // Checks DAI balances + const amountToPay = expectedConversionAmount(EURExpectedAmount).mul(2); // multiply by the number of requests: 2 + const feeToPay = expectedConversionAmount(EURFeeAmount).mul(2); // multiply by the number of requests: 2 + const expectedAmoutToPay = amountToPay + .add(feeToPay) + .mul(BATCH_DENOMINATOR + BATCH_CONV_FEE) + .div(BATCH_DENOMINATOR); + expect(BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance))).toEqual( + 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, + ); + + // Convert the two first requests and pay the three requests + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: DAIValidRequest, + }, + ], + batchConvVersion, + wallet, + ); + const confirmedTx = await tx.wait(1); + 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 + 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), + ); + }); + }); + }); + + describe('No conversion:', () => { + beforeEach(() => { + FAURequest = Utils.deepCopy(FAUValidRequest); + enrichedRequests = [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: DAIValidRequest, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: FAURequest, + }, + ]; + }); + + describe('Throw an error', () => { + it('should throw an error if the request is not erc20', async () => { + FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no value", async () => { + FAURequest.currencyInfo.value = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + }); + + it("should throw an error if one request's currencyInfo has no network", async () => { + FAURequest.currencyInfo.network = ''; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Payment currency must have a network'); + }); + + it('should throw an error if request has no extension', async () => { + FAURequest.extensions = [] as any; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('no payment network found'); + }); + + it('should throw an error if there is a wrong version mapping', async () => { + FAURequest.extensions = { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...DAIValidRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + version: '0.3.0', + }, + }; + await expect( + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + ).rejects.toThrowError('Every payment network type and version must be identical'); + }); + }); + + describe('payBatchConversionProxyRequest', () => { + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, { + gasPrice: '20000000000', + }); + expect(spy).toHaveBeenCalledWith({ + data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064', + gasPrice: '20000000000', + to: getBatchConversionProxyAddress(DAIValidRequest, '0.1.0'), + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + it(`should pay 2 differents ERC20 requests with fees`, async () => { + // Approve the contract for DAI and FAU tokens + const FAUApprovalTx = await approveErc20BatchConversionIfNeeded( + FAUValidRequest, + wallet.address, + batchConvVersion, + wallet, + ); + if (FAUApprovalTx) await FAUApprovalTx.wait(1); + + const DAIApprovalTx = await approveErc20BatchConversionIfNeeded( + DAIValidRequest, + wallet.address, + batchConvVersion, + wallet, + ); + if (DAIApprovalTx) await DAIApprovalTx.wait(1); + + // Get initial balances + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, + provider, + ); + const initialDAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider); + + const initialFAUFromBalance = await getErc20Balance( + FAUValidRequest, + wallet.address, + provider, + ); + const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); + + // Batch payment + const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); + const confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // Get balances + const ETHFromBalance = await wallet.getBalance(); + const DAIFromBalance = await getErc20Balance(DAIValidRequest, wallet.address, provider); + const DAIFeeBalance = await getErc20Balance(DAIValidRequest, feeAddress, provider); + const FAUFromBalance = await getErc20Balance(FAUValidRequest, wallet.address, provider); + const FAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); + + // Checks ETH balances + expect(ETHFromBalance.lte(initialETHFromBalance)).toBeTruthy(); // 'ETH balance should be lower' + + // Check FAU balances + const expectedFAUFeeAmountToPay = + feeAmount + ((FAUValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + + expect(BigNumber.from(FAUFromBalance)).toEqual( + BigNumber.from(initialFAUFromBalance).sub( + (FAUValidRequest.expectedAmount as number) + expectedFAUFeeAmountToPay, + ), + ); + expect(BigNumber.from(FAUFeeBalance)).toEqual( + BigNumber.from(initialFAUFeeBalance).add(expectedFAUFeeAmountToPay), + ); + // Check DAI balances + const expectedDAIFeeAmountToPay = + feeAmount + ((DAIValidRequest.expectedAmount as number) * BATCH_FEE) / BATCH_DENOMINATOR; + + expect(BigNumber.from(DAIFromBalance)).toEqual( + BigNumber.from(initialDAIFromBalance) + .sub(DAIValidRequest.expectedAmount as number) + .sub(expectedDAIFeeAmountToPay), + ); + expect(BigNumber.from(DAIFeeBalance)).toEqual( + BigNumber.from(initialDAIFeeBalance).add(expectedDAIFeeAmountToPay), + ); + }); + }); + + describe('prepareBatchPaymentTransaction', () => { + it('should consider the version mapping', () => { + expect( + prepareBatchConversionPaymentTransaction( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: { + ...DAIValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...DAIValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, + }, + } as any, + } as EnrichedRequest, + { + request: { + ...FAUValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...FAUValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, + }, + } as any, + } as EnrichedRequest, + ], + batchConvVersion, + ).to, + ).toBe(batchConversionPaymentsArtifact.getAddress('private', '0.1.0')); + }); + }); + }); +}); 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 4d67d8d63c..d41dfcae9c 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 @@ -79,6 +79,14 @@ const validSwapSettings: ISwapSettings = { }; describe('swap-erc20-fee-proxy', () => { + beforeAll(async () => { + // revoke erc20SwapToPay approval + await revokeErc20Approval( + erc20SwapToPayArtifact.getAddress(validRequest.currencyInfo.network!), + alphaErc20Address, + wallet.provider, + ); + }); describe('encodeSwapErc20FeeRequest', () => { beforeAll(async () => { // revoke erc20SwapToPay approval diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index d83849d90a..1f09adc858 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -13,6 +13,42 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Mon, 19 Sep 2022 16:22:56 +0200 Subject: [PATCH 008/207] fix: updating URL to RF (#915) --- packages/docs/docs/guides/1-first-request.md | 2 +- .../docs/guides/3-Portal-API/1-create-and-share-request.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docs/docs/guides/1-first-request.md b/packages/docs/docs/guides/1-first-request.md index bf2db6a3ed..7aa38820ed 100644 --- a/packages/docs/docs/guides/1-first-request.md +++ b/packages/docs/docs/guides/1-first-request.md @@ -45,7 +45,7 @@ try { if (request.data?.requestId) { console.log(request.data); - console.log(`https://pay.request.network/${request.data.requestId}`); + console.log(`https://app.request.finance/${request.data.requestId}`); } else { console.log(`Error, something went wrong fetching the requestId.`); } diff --git a/packages/docs/docs/guides/3-Portal-API/1-create-and-share-request.md b/packages/docs/docs/guides/3-Portal-API/1-create-and-share-request.md index 12e118b256..7aa1b3b500 100644 --- a/packages/docs/docs/guides/3-Portal-API/1-create-and-share-request.md +++ b/packages/docs/docs/guides/3-Portal-API/1-create-and-share-request.md @@ -64,7 +64,7 @@ function CreateDAIRequest() { // Once the query returns, we know the request ID // The payment page can only query and show the request once it is broadcasted over Ethereum. if (result.data.requestId) { - setPaymentLink(`https://pay.request.network/${result.data.requestId}`); + setPaymentLink(`https://app.request.finance/${result.data.requestId}`); } else { console.warn('Something went wrong, could not create the request or retrieve requestId.'); } @@ -94,7 +94,7 @@ The payment page throws a "Your request was not found" error, because the Portal Once a user has created a request, you need to support him alerting the payer. -The first way is to let the user share a payment URL with the payer. From a UX point of view, it forces him to switch context, but mobile apps often propose this solution. Keep in mind that for the recipient, it looks more secure to click on a link directly sent by a known contact. The best payment page so far is the one we have made, check it out! You can find the link in front of each request on your dashboard. The URL is `https://pay.request.network/{requestId}` +The first way is to let the user share a payment URL with the payer. From a UX point of view, it forces him to switch context, but mobile apps often propose this solution. Keep in mind that for the recipient, it looks more secure to click on a link directly sent by a known contact. The best payment page so far is the one we have made, check it out! You can find the link in front of each request on your dashboard. The URL is `https://app.request.finance/{requestId}` Another way is to handle the notification in your backend, either within your app (if the payer also uses it) or with an e-mail for example. For app-embedded payment requests, it is **strongly advised** to provide the payer with a white-list feature, and to prevent him from clicking on requests sent by strangers. From 001fb7a00e6138f43f5b58debff4b9717bc0f6f4 Mon Sep 17 00:00:00 2001 From: Bertrand Juglas Date: Tue, 20 Sep 2022 19:17:15 +0200 Subject: [PATCH 009/207] feat: add GitHub Action to deploy docs to prod (#916) --- .github/workflows/docs-deploy-prod.yml | 37 +++++++++++++++++++++++ .github/workflows/docs-deploy-staging.yml | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs-deploy-prod.yml diff --git a/.github/workflows/docs-deploy-prod.yml b/.github/workflows/docs-deploy-prod.yml new file mode 100644 index 0000000000..03f1436fb8 --- /dev/null +++ b/.github/workflows/docs-deploy-prod.yml @@ -0,0 +1,37 @@ +name: Deploy docs to prod + +on: + workflow_call: + inputs: + AWS_S3_BUCKET: + type: string + required: true + default: docs.request.network + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + AWS_REGION: + required: true + +jobs: + build-deploy-staging: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Use Node.js 16.x + uses: actions/setup-node@v1 + with: + node-version: 16 + - name: yarn install & build + run: | + yarn + yarn build + - uses: benjlevesque/s3-sync-action@master + env: + SOURCE_DIR: './packages/docs/build' + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_S3_BUCKET: ${{ inputs.AWS_S3_BUCKET }} diff --git a/.github/workflows/docs-deploy-staging.yml b/.github/workflows/docs-deploy-staging.yml index d5b37b2938..44f029de15 100644 --- a/.github/workflows/docs-deploy-staging.yml +++ b/.github/workflows/docs-deploy-staging.yml @@ -24,4 +24,4 @@ jobs: AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_S3_BUCKET: ${{ secrets. AWS_S3_BUCKET_DOCS_STAGING }} + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_DOCS_STAGING }} From a9b91c9a40ef6f96c7f554eab833ef34790c1311 Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:01:00 +0200 Subject: [PATCH 010/207] feat: erc777 balance computation (#918) --- .../src/payment/erc777-stream.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/payment-processor/src/payment/erc777-stream.ts b/packages/payment-processor/src/payment/erc777-stream.ts index e010764e92..9d6eaddaad 100644 --- a/packages/payment-processor/src/payment/erc777-stream.ts +++ b/packages/payment-processor/src/payment/erc777-stream.ts @@ -1,9 +1,9 @@ -import { ContractTransaction, Signer, Overrides, providers } from 'ethers'; +import { ContractTransaction, Signer, Overrides, providers, BigNumberish } from 'ethers'; import { ClientTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; -import { getProvider, getRequestPaymentValues, validateRequest } from './utils'; +import { getNetworkProvider, getProvider, getRequestPaymentValues, validateRequest } from './utils'; import { Framework } from '@superfluid-finance/sdk-core'; import { IPreparedTransaction } from './prepared-transaction'; @@ -160,3 +160,31 @@ export async function prepareErc777StreamPaymentTransaction( value: 0, }; } + +/** + * Gets the future ERC777 balance of an address, based on the request currency information + * @param request the request that contains currency information + * @param address the address to check + * @param timestamp the time to calculate the balance at + * @param provider the web3 provider. Defaults to Etherscan + */ +export async function getErc777BalanceAt( + request: ClientTypes.IRequestData, + address: string, + timestamp: number, + provider: providers.Provider = getNetworkProvider(request), +): Promise { + const id = getPaymentNetworkExtension(request)?.id; + if (id !== ExtensionTypes.ID.PAYMENT_NETWORK_ERC777_STREAM) { + throw new Error('Not a supported ERC777 payment network request'); + } + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC777_STREAM); + const sf = await getSuperFluidFramework(request, provider); + const superToken = await sf.loadSuperToken(request.currencyInfo.value); + const realtimeBalance = await superToken.realtimeBalanceOf({ + providerOrSigner: provider, + account: address, + timestamp, + }); + return realtimeBalance.availableBalance; +} From 97ee4354aac4b9f9583deec27505581b97e0799e Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:47:29 +0200 Subject: [PATCH 011/207] fix(smart-contract): batch conversion - add xdai and fuse proxy addresses (#919) * add xdai and fuse network * comment correction --- .../src/lib/artifacts/BatchConversionPayments/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 1f09adc858..eb1bae233e 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -49,6 +49,16 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Thu, 22 Sep 2022 14:38:49 +0200 Subject: [PATCH 012/207] feat: any to near advanced logic (#842) --- packages/advanced-logic/src/advanced-logic.ts | 43 +- .../payment-network/address-based.ts | 11 +- .../payment-network/any-to-erc20-proxy.ts | 8 +- .../payment-network/any-to-eth-proxy.ts | 8 +- .../payment-network/any-to-native.ts | 52 ++ .../extensions/payment-network/any-to-near.ts | 211 ++++++ .../extensions/payment-network/near-native.ts | 4 +- .../any-to-erc20-proxy.test.ts | 12 +- .../payment-network/any-to-eth-proxy.test.ts | 12 +- .../payment-network/any-to-near.test.ts | 659 ++++++++++++++++++ .../payment-network/native-token.test.ts | 30 +- .../any/generator-data-create.ts | 219 ++++++ .../src/aggregators/near-testnet.json | 8 + packages/currency/src/aggregators/near.json | 8 + .../src/chainlink-path-aggregators.ts | 4 + packages/types/src/extension-types.ts | 1 + 16 files changed, 1237 insertions(+), 53 deletions(-) create mode 100644 packages/advanced-logic/src/extensions/payment-network/any-to-native.ts create mode 100644 packages/advanced-logic/src/extensions/payment-network/any-to-near.ts create mode 100644 packages/advanced-logic/test/extensions/payment-network/any-to-near.test.ts create mode 100644 packages/currency/src/aggregators/near-testnet.json create mode 100644 packages/currency/src/aggregators/near.json diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index 87e70c107f..c6923e7928 100644 --- a/packages/advanced-logic/src/advanced-logic.ts +++ b/packages/advanced-logic/src/advanced-logic.ts @@ -20,6 +20,8 @@ import NearNative from './extensions/payment-network/near-native'; import AnyToErc20Proxy from './extensions/payment-network/any-to-erc20-proxy'; import AnyToEthProxy from './extensions/payment-network/any-to-eth-proxy'; import NativeTokenPaymentNetwork from './extensions/payment-network/native-token'; +import AnyToNear from './extensions/payment-network/any-to-near'; +import AnyToNativeTokenPaymentNetwork from './extensions/payment-network/any-to-native'; /** * Module to manage Advanced logic extensions @@ -41,6 +43,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic erc777Stream: Erc777Stream; feeProxyContractEth: FeeProxyContractEth; anyToEthProxy: AnyToEthProxy; + anyToNativeToken: AnyToNativeTokenPaymentNetwork[]; }; constructor(currencyManager?: ICurrencyManager) { @@ -61,6 +64,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic feeProxyContractEth: new FeeProxyContractEth(), anyToEthProxy: new AnyToEthProxy(currencyManager), nativeToken: [new NearNative()], + anyToNativeToken: [new AnyToNear(currencyManager)], }; } /** @@ -115,14 +119,20 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic [ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT]: this.extensions.feeProxyContractEth, [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ETH_PROXY]: this.extensions.anyToEthProxy, + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN]: + this.getAnyToNativeTokenExtensionForActionAndState(extensionAction, requestState), }[id]; if (!extension) { - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN) { - throw Error( - `extension with id: ${id} not found for network: ${requestState.currency.network}`, - ); + if ( + id === ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN || + id === ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN + ) { + const network = + this.getNetwork(extensionAction, requestState) || requestState.currency.network; + throw Error(`extension with id: ${id} not found for network: ${network}`); } + throw Error(`extension not recognized, id: ${id}`); } return extension; @@ -148,4 +158,29 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic ) : undefined; } + + protected getAnyToNativeTokenExtensionForActionAndState( + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + ): ExtensionTypes.IExtension | undefined { + const network = this.getNetwork(extensionAction, requestState); + + return network + ? this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) => + anyToNativeTokenExtension.supportedNetworks.includes(network), + ) + : undefined; + } + + protected getNetwork( + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + ): string | undefined { + const network = + extensionAction.action === 'create' + ? extensionAction.parameters.network + : requestState.extensions[ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN]?.values + ?.network; + return network; + } } diff --git a/packages/advanced-logic/src/extensions/payment-network/address-based.ts b/packages/advanced-logic/src/extensions/payment-network/address-based.ts index 0c57b37115..6f546a621b 100644 --- a/packages/advanced-logic/src/extensions/payment-network/address-based.ts +++ b/packages/advanced-logic/src/extensions/payment-network/address-based.ts @@ -283,8 +283,15 @@ export default abstract class AddressBasedPaymentNetwork< if (request.currency.type !== this.supportedCurrencyType) { throw Error(`This extension can be used only on ${this.supportedCurrencyType} requests`); } - if (request.currency.network && !this.supportedNetworks.includes(request.currency.network)) { - throw new UnsupportedNetworkError(request.currency.network, this.supportedNetworks); + this.throwIfInvalidNetwork(request.currency.network); + } + + protected throwIfInvalidNetwork(network?: string): asserts network is string { + if (!network) { + throw Error('network is required'); + } + if (network && this.supportedNetworks && !this.supportedNetworks.includes(network)) { + throw new UnsupportedNetworkError(network, this.supportedNetworks); } } } diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts index 29275fe927..02038f59ec 100644 --- a/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts @@ -38,14 +38,8 @@ export default class AnyToErc20ProxyPaymentNetwork extends Erc20FeeProxyPaymentN if (creationParameters.acceptedTokens.some((address) => !this.isValidAddress(address))) { throw Error('acceptedTokens must contains only valid ethereum addresses'); } - const network = creationParameters.network; - if (!network) { - throw Error('network is required'); - } - if (!conversionSupportedNetworks.includes(network)) { - throw Error(`network ${network} not supported`); - } + this.throwIfInvalidNetwork(network); for (const address of creationParameters.acceptedTokens) { const acceptedCurrency = this.currencyManager.fromAddress(address, network); diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-eth-proxy.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-eth-proxy.ts index 35b84f9013..94c4dd0d20 100644 --- a/packages/advanced-logic/src/extensions/payment-network/any-to-eth-proxy.ts +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-eth-proxy.ts @@ -27,13 +27,7 @@ export default class AnyToEthProxyPaymentNetwork extends EthereumFeeProxyPayment public createCreationAction( creationParameters: ExtensionTypes.PnAnyToEth.ICreationParameters, ): ExtensionTypes.IAction { - const network = creationParameters.network; - if (!network) { - throw Error('network is required'); - } - if (!conversionSupportedNetworks.includes(network)) { - throw Error(`network ${network} not supported`); - } + this.throwIfInvalidNetwork(creationParameters.network); return super.createCreationAction(creationParameters); } diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-native.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-native.ts new file mode 100644 index 0000000000..a06e0cdb5b --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-native.ts @@ -0,0 +1,52 @@ +import { FeeReferenceBasedPaymentNetwork } from './fee-reference-based'; +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { InvalidPaymentAddressError } from './address-based'; + +export default abstract class AnyToNativeTokenPaymentNetwork extends FeeReferenceBasedPaymentNetwork { + public constructor( + extensionId: ExtensionTypes.ID, + currentVersion: string, + supportedNetworks: string[], + ) { + super(extensionId, currentVersion, supportedNetworks, RequestLogicTypes.CURRENCY.ETH); + } + + public createCreationAction( + creationParameters: ExtensionTypes.PnAnyToAnyConversion.ICreationParameters, + ): ExtensionTypes.IAction { + const network = creationParameters.network; + this.throwIfInvalidNetwork(network); + + if ( + creationParameters.paymentAddress && + !this.isValidAddress(creationParameters.paymentAddress, network) + ) { + throw new InvalidPaymentAddressError(creationParameters.paymentAddress); + } + if ( + creationParameters.refundAddress && + !this.isValidAddress(creationParameters.refundAddress, network) + ) { + throw new InvalidPaymentAddressError(creationParameters.refundAddress, 'refundAddress'); + } + if ( + creationParameters.feeAddress && + !this.isValidAddress(creationParameters.feeAddress, network) + ) { + throw new InvalidPaymentAddressError(creationParameters.feeAddress, 'feeAddress'); + } + if (creationParameters.maxRateTimespan && creationParameters.maxRateTimespan < 0) { + throw new InvalidMaxRateTimespanError(creationParameters.maxRateTimespan); + } + return super.createCreationAction(creationParameters); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected abstract isValidAddress(_address: string, _networkName?: string): boolean; +} + +export class InvalidMaxRateTimespanError extends Error { + constructor(maxRateTimespan: number) { + super(`${maxRateTimespan} is not a valid maxRateTimespan`); + } +} diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts new file mode 100644 index 0000000000..2078602e3c --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts @@ -0,0 +1,211 @@ +import { ICurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { UnsupportedNetworkError } from './address-based'; +import AnyToNativeTokenPaymentNetwork from './any-to-native'; + +const CURRENT_VERSION = '0.2.0'; +const supportedNetworks = ['aurora', 'aurora-testnet']; + +export default class AnyToNearPaymentNetwork extends AnyToNativeTokenPaymentNetwork { + public constructor( + private currencyManager: ICurrencyManager, + extensionId: ExtensionTypes.ID = ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + currentVersion: string = CURRENT_VERSION, + ) { + super(extensionId, currentVersion, supportedNetworks); + } + + /** + * Check if a near address is valid + * + * @param {string} address address to check + * @returns {boolean} true if address is valid + */ + protected isValidAddress(address: string, networkName?: string): boolean { + switch (networkName) { + case 'aurora': + return this.isValidMainnetAddress(address); + case 'aurora-testnet': + return this.isValidTestnetAddress(address); + case undefined: + return this.isValidMainnetAddress(address) || this.isValidTestnetAddress(address); + default: + throw new UnsupportedNetworkError(networkName, this.supportedNetworks); + } + } + + private isValidMainnetAddress(address: string): boolean { + return this.isValidAddressForSymbolAndNetwork(address, 'NEAR', 'aurora'); + } + + private isValidTestnetAddress(address: string): boolean { + return this.isValidAddressForSymbolAndNetwork(address, 'NEAR-testnet', 'aurora-testnet'); + } + + /** + * Applies a creation extension action + * + * @param extensionAction action to apply + * @param timestamp action timestamp + * + * @returns state of the extension created + */ + protected applyCreation( + extensionAction: ExtensionTypes.IAction, + timestamp: number, + ): ExtensionTypes.IState { + if (!extensionAction.parameters.network || extensionAction.parameters.network.length === 0) { + throw Error('network is required'); + } + + if ( + extensionAction.parameters.paymentAddress && + !this.isValidAddress( + extensionAction.parameters.paymentAddress, + extensionAction.parameters.network, + ) + ) { + throw Error( + `paymentAddress ${extensionAction.parameters.paymentAddress} is not a valid address`, + ); + } + + if ( + extensionAction.parameters.feeAddress && + !this.isValidAddress( + extensionAction.parameters.feeAddress, + extensionAction.parameters.network, + ) + ) { + throw Error(`feeAddress ${extensionAction.parameters.feeAddress} is not a valid address`); + } + + if ( + extensionAction.parameters.refundAddress && + !this.isValidAddress( + extensionAction.parameters.refundAddress, + extensionAction.parameters.network, + ) + ) { + throw Error( + `refundAddress ${extensionAction.parameters.refundAddress} is not a valid address`, + ); + } + + const feePNCreationAction = super.applyCreation(extensionAction, timestamp); + + return { + ...feePNCreationAction, + events: [ + { + name: 'create', + parameters: { + feeAddress: extensionAction.parameters.feeAddress, + feeAmount: extensionAction.parameters.feeAmount, + paymentAddress: extensionAction.parameters.paymentAddress, + refundAddress: extensionAction.parameters.refundAddress, + salt: extensionAction.parameters.salt, + network: extensionAction.parameters.network, + maxRateTimespan: extensionAction.parameters.maxRateTimespan, + }, + timestamp, + }, + ], + values: { + ...feePNCreationAction.values, + network: extensionAction.parameters.network, + maxRateTimespan: extensionAction.parameters.maxRateTimespan, + }, + }; + } + + /** + * Applies add payment address + * + * @param extensionState previous state of the extension + * @param extensionAction action to apply + * @param requestState request state read-only + * @param actionSigner identity of the signer + * + * @returns state of the extension updated + */ + protected applyAddPaymentAddress( + extensionState: ExtensionTypes.IState, + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + actionSigner: IdentityTypes.IIdentity, + timestamp: number, + ): ExtensionTypes.IState { + const paymentAddress = extensionAction.parameters.paymentAddress; + if (!this.isValidAddress(paymentAddress, extensionState.values.network)) { + throw new Error(`paymentAddress '${paymentAddress}' is not a valid address`); + } + return super.applyAddPaymentAddress( + extensionState, + extensionAction, + requestState, + actionSigner, + timestamp, + ); + } + + protected applyAddFee( + extensionState: ExtensionTypes.IState, + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + actionSigner: IdentityTypes.IIdentity, + timestamp: number, + ): ExtensionTypes.IState { + const network = + requestState.extensions[ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN].values.network; + if ( + extensionAction.parameters.feeAddress && + !this.isValidAddress(extensionAction.parameters.feeAddress, network) + ) { + throw Error('feeAddress is not a valid address'); + } + return super.applyAddFee( + extensionState, + extensionAction, + requestState, + actionSigner, + timestamp, + ); + } + + /** + * Validate that the network and currency coming from the extension and/or action are valid and supported. + * It must throw in case of error. + */ + protected validate( + request: RequestLogicTypes.IRequest, + extensionAction: ExtensionTypes.IAction, + ): void { + const network = + extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE + ? extensionAction.parameters.network + : request.extensions[this.extensionId]?.values.network; + + if (!network) { + throw new Error( + `The network must be provided by the creation action or by the extension state`, + ); + } + + if (!supportedNetworks.includes(network)) { + throw new Error(`The network (${network}) is not supported for this payment network.`); + } + + const currency = this.currencyManager.fromStorageCurrency(request.currency); + if (!currency) { + throw new UnsupportedCurrencyError(request.currency); + } + console.log(network); + console.log(currency); + if (!this.currencyManager.supportsConversion(currency, network)) { + throw new Error( + `The currency (${request.currency.value}) of the request is not supported for this payment network.`, + ); + } + } +} diff --git a/packages/advanced-logic/src/extensions/payment-network/near-native.ts b/packages/advanced-logic/src/extensions/payment-network/near-native.ts index 27017d2b27..fabc5f4755 100644 --- a/packages/advanced-logic/src/extensions/payment-network/near-native.ts +++ b/packages/advanced-logic/src/extensions/payment-network/near-native.ts @@ -6,7 +6,7 @@ const CURRENT_VERSION = '0.2.0'; const supportedNetworks = ['aurora', 'aurora-testnet']; /** - * Implementation of the payment network to pay in ETH based on input data. + * Implementation of the payment network to pay in Near based on input data. */ export default class NearNativePaymentNetwork extends NativeTokenPaymentNetwork { public constructor( @@ -17,7 +17,7 @@ export default class NearNativePaymentNetwork extends NativeTokenPaymentNetwork } /** - * Check if an ethereum address is valid + * Check if a near address is valid * * @param {string} address address to check * @returns {boolean} true if address is valid diff --git a/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts b/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts index 9157590e29..804cceb224 100644 --- a/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts @@ -1,6 +1,10 @@ import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import Utils from '@requestnetwork/utils'; -import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { + conversionSupportedNetworks, + CurrencyManager, + UnsupportedCurrencyError, +} from '@requestnetwork/currency'; import AnyToErc20Proxy from '../../../src/extensions/payment-network/any-to-erc20-proxy'; import * as DataConversionERC20FeeAddData from '../../utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator'; @@ -148,7 +152,11 @@ describe('extensions/payment-network/erc20/any-to-erc20-fee-proxy-contract', () network: 'kovan', acceptedTokens: ['0x0000000000000000000000000000000000000003'], }); - }).toThrowError('network kovan not supported'); + }).toThrowError( + `Payment network 'kovan' is not supported by this extension (only ${conversionSupportedNetworks.join( + ', ', + )})`, + ); }); it('cannot createCreationAction with tokens accepted not supported', () => { diff --git a/packages/advanced-logic/test/extensions/payment-network/any-to-eth-proxy.test.ts b/packages/advanced-logic/test/extensions/payment-network/any-to-eth-proxy.test.ts index ccb924f161..7dcc958214 100644 --- a/packages/advanced-logic/test/extensions/payment-network/any-to-eth-proxy.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/any-to-eth-proxy.test.ts @@ -1,6 +1,10 @@ import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import Utils from '@requestnetwork/utils'; -import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { + conversionSupportedNetworks, + CurrencyManager, + UnsupportedCurrencyError, +} from '@requestnetwork/currency'; import AnyToEthProxy from '../../../src/extensions/payment-network/any-to-eth-proxy'; import * as DataConversionETHFeeAddData from '../../utils/payment-network/ethereum/any-to-eth-proxy-add-data-generator'; @@ -119,7 +123,11 @@ describe('extensions/payment-network/ethereum/any-to-eth-fee-proxy-contract', () salt: 'ea3bc7caf64110ca', network: 'kovan', }); - }).toThrowError('network kovan not supported'); + }).toThrowError( + `Payment network 'kovan' is not supported by this extension (only ${conversionSupportedNetworks.join( + ', ', + )})`, + ); }); it('cannot applyActionToExtensions of creation with an invalid network', () => { diff --git a/packages/advanced-logic/test/extensions/payment-network/any-to-near.test.ts b/packages/advanced-logic/test/extensions/payment-network/any-to-near.test.ts new file mode 100644 index 0000000000..066011e136 --- /dev/null +++ b/packages/advanced-logic/test/extensions/payment-network/any-to-near.test.ts @@ -0,0 +1,659 @@ +import { + requestStateNoExtensions, + arbitrarySalt, + actionCreationWithAnyToNativeTokenPayment, + extensionStateWithAnyToNativeTokenPaymentAndRefund, + extensionStateAnyToNativeWithPaymentAddressAdded, + extensionStateAnyToNativeWithFeeAdded, +} from '../../utils/payment-network/any/generator-data-create'; +import { AdvancedLogic } from '../../../src'; +import { arbitraryTimestamp, payeeRaw, payerRaw } from '../../utils/test-data-generator'; +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import AnyToNearPaymentNetwork from '../../../src/extensions/payment-network/any-to-near'; +import AnyToNativeTokenPaymentNetwork from '../../../src/extensions/payment-network/any-to-native'; +import { CurrencyManager } from '@requestnetwork/currency'; +import utils from '@requestnetwork/utils'; + +const salt = arbitrarySalt; +const currencyManager = CurrencyManager.getDefault(); + +describe('extensions/payment-network/any-to-native-token', () => { + const validCurrency = { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'USD', + }; + const wrongCurrency = { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }; + const anyToNativeTokenTestCases = [ + { + name: 'Near', + paymentNetwork: new AnyToNearPaymentNetwork( + currencyManager, + ) as AnyToNativeTokenPaymentNetwork, + suffix: 'near', + wrongSuffix: 'testnet', + network: 'aurora', + wrongNetwork: 'aurora-testnet', + maxRateTimespan: 100000, + feeAmount: '100', + }, + { + name: 'Near testnet', + paymentNetwork: new AnyToNearPaymentNetwork( + currencyManager, + ) as AnyToNativeTokenPaymentNetwork, + suffix: 'testnet', + wrongSuffix: 'near', + network: 'aurora-testnet', + wrongNetwork: 'aurora', + maxRateTimespan: 100000, + feeAmount: '100', + }, + ]; + + anyToNativeTokenTestCases.forEach((testCase) => { + describe(`action creations for ${testCase.name}`, () => { + describe('createCreationAction', () => { + it('works with valid parameters', () => { + expect( + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.suffix}`, + feeAddress: `fee.${testCase.suffix}`, + feeAmount: testCase.feeAmount, + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }), + ).toBeDefined(); + }); + it('works with minimum parameters', () => { + expect( + testCase.paymentNetwork.createCreationAction({ + salt, + network: testCase.network, + }), + ).toBeTruthy(); + }); + it('throws when payment address is invalid', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: 'not a near address', + refundAddress: `refund.${testCase.suffix}`, + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError("paymentAddress 'not a near address' is not a valid address"); + }); + it('throws when payment address is on the wrong network', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.wrongSuffix}`, + refundAddress: `refund.${testCase.suffix}`, + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError(`paymentAddress 'pay.${testCase.wrongSuffix}' is not a valid address`); + }); + it('throws when refund address is invalid', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: 'not a near address', + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError("refundAddress 'not a near address' is not a valid address"); + }); + it('throws when refund address is on the wrong network', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.wrongSuffix}`, + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError(`refundAddress 'refund.${testCase.wrongSuffix}' is not a valid address`); + }); + it('throws when fee address is invalid', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.suffix}`, + feeAddress: 'not a near address', + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError("feeAddress 'not a near address' is not a valid address"); + }); + it('throws when fee address is on the wrong network', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.suffix}`, + feeAddress: `fee.${testCase.wrongSuffix}`, + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError(`feeAddress 'fee.${testCase.wrongSuffix}' is not a valid address`); + }); + it('throws when fee amount is invalid', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.suffix}`, + feeAddress: `fee.${testCase.suffix}`, + feeAmount: '-2000', + salt, + network: testCase.network, + maxRateTimespan: testCase.maxRateTimespan, + }); + }).toThrowError(`feeAmount is not a valid amount`); + }); + it('throws when maxRateTimespan is invalid', () => { + expect(() => { + testCase.paymentNetwork.createCreationAction({ + paymentAddress: `pay.${testCase.suffix}`, + refundAddress: `refund.${testCase.suffix}`, + feeAddress: `fee.${testCase.suffix}`, + feeAmount: '2000', + salt, + network: testCase.network, + maxRateTimespan: -2000, + }); + }).toThrowError(`-2000 is not a valid maxRateTimespan`); + }); + describe('edge cases', () => { + const partialCreationParams: ExtensionTypes.PnAnyToAnyConversion.ICreationParameters = { + salt, + refundAddress: 'refund.near', + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + }; + it('throws when payment network is not supported', () => { + expect(() => { + new AnyToNearPaymentNetwork(currencyManager).createCreationAction({ + ...partialCreationParams, + network: 'another-chain', + }); + }).toThrowError( + `Payment network 'another-chain' is not supported by this extension (only aurora, aurora-testnet)`, + ); + }); + it('throws when payment network is missing', () => { + expect(() => { + new AnyToNearPaymentNetwork(currencyManager).createCreationAction( + partialCreationParams, + ); + }).toThrowError(`network is required`); + }); + }); + }); + describe('createAddPaymentAddressAction', () => { + it('works with valid payment address', () => { + expect( + testCase.paymentNetwork.createAddPaymentAddressAction({ + paymentAddress: `pay.${testCase.suffix}`, + }), + ).toBeTruthy(); + }); + it('throws when payment address is invalid', () => { + expect(() => { + testCase.paymentNetwork.createAddPaymentAddressAction({ + paymentAddress: 'not a near address', + }); + }).toThrowError("paymentAddress 'not a near address' is not a valid address"); + }); + }); + describe('createAddRefundAddressAction', () => { + it('works with valid payment address', () => { + expect( + testCase.paymentNetwork.createAddRefundAddressAction({ + refundAddress: `refund.${testCase.suffix}`, + }), + ).toBeTruthy(); + }); + it('throws when payment address is invalid', () => { + expect(() => { + testCase.paymentNetwork.createAddRefundAddressAction({ + refundAddress: `not a near address`, + }); + }).toThrowError("refundAddress 'not a near address' is not a valid address"); + }); + }); + describe('createAddFeeAction', () => { + it('can createAddFeeAction', () => { + expect( + testCase.paymentNetwork.createAddFeeAction({ + feeAddress: `fee.${testCase.suffix}`, + feeAmount: '2000', + }), + ).toEqual({ + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + feeAddress: `fee.${testCase.suffix}`, + feeAmount: '2000', + }, + }); + }); + it('cannot createAddFeeAction with amount non positive integer', () => { + expect(() => { + testCase.paymentNetwork.createAddFeeAction({ + feeAddress: `fee.${testCase.suffix}`, + feeAmount: '-30000', + }); + }).toThrowError('feeAmount is not a valid amount'); + }); + }); + }); + }); + + describe('AdvancedLogic.applyActionToExtension', () => { + const mainnetTestCase = anyToNativeTokenTestCases[0]; + let advancedLogic: AdvancedLogic; + let validRequestState: typeof requestStateNoExtensions; + let creationAction: ExtensionTypes.IAction; + let anyToNearPn: AnyToNearPaymentNetwork; + beforeEach(() => { + advancedLogic = new AdvancedLogic(); + anyToNearPn = new AnyToNearPaymentNetwork(currencyManager); + validRequestState = { + ...requestStateNoExtensions, + currency: validCurrency, + }; + creationAction = utils.deepCopy(actionCreationWithAnyToNativeTokenPayment); + }); + describe('applyActionToExtension/create action', () => { + it('works with valid parameters', () => { + const newExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + expect(newExtensionState).toEqual(extensionStateWithAnyToNativeTokenPaymentAndRefund); + }); + it('throws when currency is not supported', () => { + const invalidRequestState: typeof requestStateNoExtensions = { + ...requestStateNoExtensions, + currency: wrongCurrency, + }; + expect(() => + advancedLogic.applyActionToExtensions( + invalidRequestState.extensions, + creationAction, + invalidRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError( + `The currency (${wrongCurrency.value}) of the request is not supported for this payment network`, + ); + }); + it('throws when network is undefined', () => { + creationAction.parameters.network = undefined; + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError( + 'extension with id: pn-any-to-native-token not found for network: undefined', + ); + }); + it('throws when the network is wrong', () => { + const wrongNetwork = `wrong network`; + creationAction.parameters.network = wrongNetwork; + + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError( + 'extension with id: pn-any-to-native-token not found for network: wrong network', + ); + }); + it('throws when payment address is not valid', () => { + const invalidAddress = 'pay.testnet'; + creationAction.parameters.paymentAddress = invalidAddress; + + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError(`paymentAddress ${invalidAddress} is not a valid address`); + }); + it('throws when refund address is not valid', () => { + const invalidAddress = 'refund.testnet'; + creationAction.parameters.refundAddress = invalidAddress; + + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError(`refundAddress ${invalidAddress} is not a valid address`); + }); + it('throws when fee address is not valid', () => { + const invalidAddress = 'fee.testnet'; + creationAction.parameters.feeAddress = invalidAddress; + + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError(`feeAddress ${invalidAddress} is not a valid address`); + }); + it('throws when fee amount is not valid', () => { + const invalidFeeAmount = '-100'; + creationAction.parameters.feeAmount = invalidFeeAmount; + + expect(() => + advancedLogic.applyActionToExtensions( + validRequestState.extensions, + creationAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ), + ).toThrowError(`feeAmount is not a valid amount`); + }); + it('throws when version is missing', () => { + expect(() => { + advancedLogic.applyActionToExtensions( + {}, + { ...actionCreationWithAnyToNativeTokenPayment, version: '' }, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError('version is required at creation'); + }); + }); + describe('applyActionToExtension/addPaymentAddress action', () => { + it('works when adding a payment address to a created state', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addPaymentAddressAction = anyToNearPn.createAddPaymentAddressAction({ + paymentAddress: 'pay.near', + }); + + const newExtensionState = advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addPaymentAddressAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + expect(newExtensionState).toEqual(extensionStateAnyToNativeWithPaymentAddressAdded); + }); + it('throws when payment address is invalid', () => { + const invalidAddress = 'pay.testnet'; + + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addPaymentAddressAction = anyToNearPn.createAddPaymentAddressAction({ + paymentAddress: invalidAddress, + }); + + expect(() => { + advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addPaymentAddressAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError(`paymentAddress '${invalidAddress}' is not a valid address`); + }); + }); + describe('applyActionToExtension/addFeeAddress action', () => { + it('works when adding a fee parameters to a created state', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + paymentAddress: 'pay.near', + network: 'aurora', + refundAddress: 'refund.near', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addFeeAction = anyToNearPn.createAddFeeAction({ + feeAddress: 'fee.near', + feeAmount: '100', + }); + + const newExtensionState = advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addFeeAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + expect(newExtensionState).toEqual(extensionStateAnyToNativeWithFeeAdded); + }); + it('throws when fee amount is invalid', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addFeeAction = { + action: 'addFee', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + feeAddress: 'fee.near', + feeAmount: '-200', + }, + }; + + expect(() => { + advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addFeeAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError(`feeAmount is not a valid amount`); + }); + it('throws when fee address is invalid', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addFeeAction = { + action: 'addFee', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + feeAddress: 'fee.testnet', + feeAmount: '100', + }, + }; + + expect(() => { + advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addFeeAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError(`feeAddress is not a valid address`); + }); + it('throws when fee parameters is already given', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addFeeAction = { + action: 'addFee', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + feeAddress: 'newfee.near', + feeAmount: '100', + }, + }; + + expect(() => { + advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addFeeAction, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError(`Fee address already given`); + }); + it('throws when addFee action is signed by someone else', () => { + const intermediateExtensionState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + anyToNearPn.createCreationAction({ + salt, + network: 'aurora', + refundAddress: 'refund.near', + maxRateTimespan: 1000000, + }), + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + + validRequestState.extensions = intermediateExtensionState; + + const addFeeAction = { + action: 'addFee', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + feeAddress: 'fee.near', + feeAmount: '100', + }, + }; + + expect(() => { + advancedLogic.applyActionToExtensions( + intermediateExtensionState, + addFeeAction, + validRequestState, + payerRaw.identity, + arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payee`); + }); + }); + + it('keeps the version used at creation', () => { + const newState = advancedLogic.applyActionToExtensions( + validRequestState.extensions, + { ...actionCreationWithAnyToNativeTokenPayment, version: 'ABCD' }, + validRequestState, + payeeRaw.identity, + arbitraryTimestamp, + ); + expect(newState[mainnetTestCase.paymentNetwork.extensionId].version).toBe('ABCD'); + }); + }); +}); diff --git a/packages/advanced-logic/test/extensions/payment-network/native-token.test.ts b/packages/advanced-logic/test/extensions/payment-network/native-token.test.ts index fc71fa2083..de4c3aff46 100644 --- a/packages/advanced-logic/test/extensions/payment-network/native-token.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/native-token.test.ts @@ -135,7 +135,9 @@ describe('extensions/payment-network/native-token', () => { ...partialCreationParams, paymentNetworkName: 'another-chain', }); - }).toThrowError(`Payment network 'another-chain' is not supported by this extension (only`); + }).toThrowError( + `Payment network 'another-chain' is not supported by this extension (only aurora, aurora-testnet)`, + ); }); it('createCreationAction() throws without payment network', () => { expect(() => { @@ -174,32 +176,6 @@ describe('extensions/payment-network/native-token', () => { expect(newExtensionState).toEqual(extensionStateWithNativeTokenPaymentAndRefund); }); - it('works on a state with no currency network', () => { - const advancedLogic = new AdvancedLogic(); - - const requestState: typeof requestStateNoExtensions = { - ...requestStateNoExtensions, - currency: { ...mainnetTestCase.currency, network: undefined }, - }; - - const creationAction = { - ...actionCreationWithNativeTokenPayment, - parameters: { - ...actionCreationWithNativeTokenPayment.parameters, - paymentNetworkName: mainnetTestCase.currency.network, - }, - }; - - const newExtensionState = advancedLogic.applyActionToExtensions( - requestState.extensions, - creationAction, - requestState, - payeeRaw.identity, - arbitraryTimestamp, - ); - - expect(newExtensionState).toEqual(extensionStateWithNativeTokenPaymentAndRefund); - }); it('works with an action without payment network', () => { const advancedLogic = new AdvancedLogic(); diff --git a/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts b/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts index 507a1e86eb..4385d7cc29 100644 --- a/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts +++ b/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts @@ -84,6 +84,33 @@ export const actionCreationPayeeDelegate = { version: '0.1.0', }; +export const actionCreationWithNativeTokenPayment: ExtensionTypes.IAction = + { + action: ExtensionTypes.PnAnyDeclarative.ACTION.CREATE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN, + parameters: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + salt: arbitrarySalt, + }, + version: '0.2.0', + }; +export const actionCreationWithAnyToNativeTokenPayment: ExtensionTypes.IAction = + { + action: ExtensionTypes.PnAnyDeclarative.ACTION.CREATE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + parameters: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + feeAmount: '100', + salt: arbitrarySalt, + network: 'aurora', + maxRateTimespan: 1000000, + }, + version: '0.2.0', + }; + export const actionAddDelegate = { action: ExtensionTypes.PnAnyDeclarative.ACTION.ADD_DELEGATE, id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_DECLARATIVE, @@ -184,6 +211,198 @@ const extensionStateWithPayeeDelegate: RequestLogicTypes.IExtensionStates = { version: '0.1.0', }, }; +export const extensionStateWithNativeTokenPaymentAndRefund: RequestLogicTypes.IExtensionStates = { + [ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN as string]: { + events: [ + { + name: 'create', + parameters: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + salt: arbitrarySalt, + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + salt: arbitrarySalt, + payeeDelegate: undefined, + payerDelegate: undefined, + paymentInfo: undefined, + receivedPaymentAmount: '0', + receivedRefundAmount: '0', + refundInfo: undefined, + sentPaymentAmount: '0', + sentRefundAmount: '0', + }, + version: '0.2.0', + }, +}; +export const extensionStateWithAnyToNativeTokenPaymentAndRefund: RequestLogicTypes.IExtensionStates = + { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN as string]: { + events: [ + { + name: 'create', + parameters: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + salt: arbitrarySalt, + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + network: 'aurora', + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + salt: arbitrarySalt, + payeeDelegate: undefined, + payerDelegate: undefined, + paymentInfo: undefined, + receivedPaymentAmount: '0', + receivedRefundAmount: '0', + refundInfo: undefined, + sentPaymentAmount: '0', + sentRefundAmount: '0', + network: 'aurora', + maxRateTimespan: 1000000, + feeAmount: '100', + }, + version: '0.2.0', + }, + }; +export const extensionStateAnyToNativeWithPaymentAddressAdded: RequestLogicTypes.IExtensionStates = + { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN as string]: { + events: [ + { + name: 'create', + parameters: { + refundAddress: 'refund.near', + salt: arbitrarySalt, + feeAddress: 'fee.near', + feeAmount: '100', + maxRateTimespan: 1000000, + network: 'aurora', + }, + timestamp: arbitraryTimestamp, + }, + { + name: 'addPaymentAddress', + parameters: { + paymentAddress: 'pay.near', + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + salt: arbitrarySalt, + payeeDelegate: undefined, + payerDelegate: undefined, + paymentInfo: undefined, + receivedPaymentAmount: '0', + receivedRefundAmount: '0', + refundInfo: undefined, + sentPaymentAmount: '0', + sentRefundAmount: '0', + network: 'aurora', + maxRateTimespan: 1000000, + feeAmount: '100', + }, + version: '0.2.0', + }, + }; + +export const extensionStateAnyToNativeWithFeeAdded: RequestLogicTypes.IExtensionStates = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN as string]: { + events: [ + { + name: 'create', + parameters: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + salt: arbitrarySalt, + maxRateTimespan: 1000000, + network: 'aurora', + }, + timestamp: arbitraryTimestamp, + }, + { + name: 'addFee', + parameters: { + feeAddress: 'fee.near', + feeAmount: '100', + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress: 'pay.near', + refundAddress: 'refund.near', + feeAddress: 'fee.near', + salt: arbitrarySalt, + payeeDelegate: undefined, + payerDelegate: undefined, + paymentInfo: undefined, + receivedPaymentAmount: '0', + receivedRefundAmount: '0', + refundInfo: undefined, + sentPaymentAmount: '0', + sentRefundAmount: '0', + network: 'aurora', + maxRateTimespan: 1000000, + feeAmount: '100', + }, + version: '0.2.0', + }, +}; + +export const extensionStateWithPaymentAddressAdded: RequestLogicTypes.IExtensionStates = { + [ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN as string]: { + events: [ + { + name: 'create', + parameters: { + salt: arbitrarySalt, + }, + timestamp: arbitraryTimestamp, + }, + { + name: 'addPaymentAddress', + parameters: { + paymentAddress: 'pay.near', + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + ...zeroAmounts, + payerDelegate, + }, + version: '0.1.0', + }, +}; + const extensionStateWithPayerDelegate: RequestLogicTypes.IExtensionStates = { [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_DECLARATIVE as string]: { events: [createEvent], diff --git a/packages/currency/src/aggregators/near-testnet.json b/packages/currency/src/aggregators/near-testnet.json new file mode 100644 index 0000000000..31edaf5cb3 --- /dev/null +++ b/packages/currency/src/aggregators/near-testnet.json @@ -0,0 +1,8 @@ +{ + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { + "0xed9cbb2837912278b47d422f36df40a1da36c4e0": 1 + }, + "0xed9cbb2837912278b47d422f36df40a1da36c4e0": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 + } +} diff --git a/packages/currency/src/aggregators/near.json b/packages/currency/src/aggregators/near.json new file mode 100644 index 0000000000..1719821d97 --- /dev/null +++ b/packages/currency/src/aggregators/near.json @@ -0,0 +1,8 @@ +{ + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { + "0x86c47ea0ea1129e55e6165d934e71698f0f49c01": 1 + }, + "0x86c47ea0ea1129e55e6165d934e71698f0f49c01": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 + } +} diff --git a/packages/currency/src/chainlink-path-aggregators.ts b/packages/currency/src/chainlink-path-aggregators.ts index e59e35e5c9..6fff0701d6 100644 --- a/packages/currency/src/chainlink-path-aggregators.ts +++ b/packages/currency/src/chainlink-path-aggregators.ts @@ -6,6 +6,8 @@ import mainnetAggregator from './aggregators/mainnet.json'; import rinkebyAggregator from './aggregators/rinkeby.json'; import maticAggregator from './aggregators/matic.json'; import fantomAggregator from './aggregators/fantom.json'; +import nearAggregator from './aggregators/near.json'; +import nearTestnetAggregator from './aggregators/near-testnet.json'; export type CurrencyPairs = Record>; // List of currencies supported by network (can be generated from requestNetwork/toolbox/src/chainlinkConversionPathTools.ts) @@ -25,6 +27,8 @@ export const chainlinkCurrencyPairs: Record = { xdai: {}, avalanche: {}, bsc: {}, + aurora: nearAggregator, + 'aurora-testnet': nearTestnetAggregator, }; export const chainlinkSupportedNetworks = Object.keys(chainlinkCurrencyPairs); diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index 490f9bf4ba..a233e88fd9 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -78,6 +78,7 @@ export enum ID { PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT = 'pn-eth-fee-proxy-contract', PAYMENT_NETWORK_ETH_INPUT_DATA = 'pn-eth-input-data', PAYMENT_NETWORK_NATIVE_TOKEN = 'pn-native-token', + PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN = 'pn-any-to-native-token', PAYMENT_NETWORK_ANY_DECLARATIVE = 'pn-any-declarative', PAYMENT_NETWORK_ANY_TO_ERC20_PROXY = 'pn-any-to-erc20-proxy', PAYMENT_NETWORK_ANY_TO_ETH_PROXY = 'pn-any-to-eth-proxy', From 59d222fb1b1e9613ee7809fb6ac5c1543b4464c3 Mon Sep 17 00:00:00 2001 From: Bertrand Juglas Date: Thu, 22 Sep 2022 15:19:09 +0200 Subject: [PATCH 013/207] fix: use workflow_dispatch (#917) to be able to trigger manually the GitHub Action --- .github/workflows/docs-deploy-prod.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/docs-deploy-prod.yml b/.github/workflows/docs-deploy-prod.yml index 03f1436fb8..7e9fbfbd35 100644 --- a/.github/workflows/docs-deploy-prod.yml +++ b/.github/workflows/docs-deploy-prod.yml @@ -1,19 +1,12 @@ name: Deploy docs to prod on: - workflow_call: + workflow_dispatch: inputs: AWS_S3_BUCKET: type: string required: true default: docs.request.network - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true - AWS_REGION: - required: true jobs: build-deploy-staging: From 8b851e7fd634ad9f92bc6e83280d68da3e306bfe Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:57:36 +0200 Subject: [PATCH 014/207] feat: near conversion payment detector (#920) --- .../extensions/payment-network/any-to-near.ts | 2 - packages/payment-detection/src/index.ts | 3 +- packages/payment-detection/src/near/index.ts | 4 + .../src/near/near-conversion-detector.ts | 122 +++++++++++ .../src/{ => near}/near-detector.ts | 4 +- .../near-conversion-info-retriever.ts | 62 ++++++ .../retrievers}/near-info-retriever.ts | 25 +-- .../src/payment-network-factory.ts | 3 +- .../near/GetConversionPayments.graphql | 31 +++ .../thegraph/queries/near/GetPayments.graphql | 8 +- .../test/near/near-native-conversion.test.ts | 203 ++++++++++++++++++ .../test/{ => near}/near-native.test.ts | 7 +- packages/types/src/payment-types.ts | 1 + 13 files changed, 447 insertions(+), 28 deletions(-) create mode 100644 packages/payment-detection/src/near/index.ts create mode 100644 packages/payment-detection/src/near/near-conversion-detector.ts rename packages/payment-detection/src/{ => near}/near-detector.ts (95%) create mode 100644 packages/payment-detection/src/near/retrievers/near-conversion-info-retriever.ts rename packages/payment-detection/src/{ => near/retrievers}/near-info-retriever.ts (68%) create mode 100644 packages/payment-detection/src/thegraph/queries/near/GetConversionPayments.graphql create mode 100644 packages/payment-detection/test/near/near-native-conversion.test.ts rename packages/payment-detection/test/{ => near}/near-native.test.ts (94%) diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts index 2078602e3c..95799cf16d 100644 --- a/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts @@ -200,8 +200,6 @@ export default class AnyToNearPaymentNetwork extends AnyToNativeTokenPaymentNetw if (!currency) { throw new UnsupportedCurrencyError(request.currency); } - console.log(network); - console.log(currency); if (!this.currencyManager.supportsConversion(currency, network)) { throw new Error( `The currency (${request.currency.value}) of the request is not supported for this payment network.`, diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index e80042f493..34a075b852 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -16,7 +16,7 @@ import { getPaymentNetworkExtension, getPaymentReference, } from './utils'; -import { NearNativeTokenPaymentDetector } from './near-detector'; +import { NearNativeTokenPaymentDetector, NearConversionNativeTokenPaymentDetector } from './near'; import { FeeReferenceBasedDetector } from './fee-reference-based-detector'; import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever'; @@ -37,6 +37,7 @@ export { FeeReferenceBasedDetector, SuperFluidPaymentDetector, NearNativeTokenPaymentDetector, + NearConversionNativeTokenPaymentDetector, EscrowERC20InfoRetriever, SuperFluidInfoRetriever, setProviderFactory, diff --git a/packages/payment-detection/src/near/index.ts b/packages/payment-detection/src/near/index.ts new file mode 100644 index 0000000000..408868b950 --- /dev/null +++ b/packages/payment-detection/src/near/index.ts @@ -0,0 +1,4 @@ +export { NearNativeTokenPaymentDetector } from './near-detector'; +export { NearConversionNativeTokenPaymentDetector } from './near-conversion-detector'; +export { NearConversionInfoRetriever } from './retrievers/near-conversion-info-retriever'; +export { NearInfoRetriever } from './retrievers/near-info-retriever'; diff --git a/packages/payment-detection/src/near/near-conversion-detector.ts b/packages/payment-detection/src/near/near-conversion-detector.ts new file mode 100644 index 0000000000..be000a1dca --- /dev/null +++ b/packages/payment-detection/src/near/near-conversion-detector.ts @@ -0,0 +1,122 @@ +import { + AdvancedLogicTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; + +import { AnyToAnyDetector } from '../any-to-any-detector'; +import { ICurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { NearConversionInfoRetriever } from './retrievers/near-conversion-info-retriever'; + +// interface of the object indexing the proxy contract version +interface IProxyContractVersion { + [version: string]: string; +} + +// the versions 0.1.0 and 0.2.0 have the same contracts +const CONTRACT_ADDRESS_MAP: IProxyContractVersion = { + ['0.1.0']: '0.1.0', +}; + +/** + * Handle payment detection for NEAR native token payment with conversion + */ +export class NearConversionNativeTokenPaymentDetector extends AnyToAnyDetector< + ExtensionTypes.PnAnyToEth.IAnyToEth, + PaymentTypes.IETHPaymentEventParameters +> { + /** + * @param extension The advanced logic payment network extension + */ + public constructor({ + advancedLogic, + currencyManager, + }: { + advancedLogic: AdvancedLogicTypes.IAdvancedLogic; + currencyManager: ICurrencyManager; + }) { + super( + PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE, + advancedLogic.extensions.anyToNativeToken[0], + currencyManager, + ); + } + + public static getContractName = (chainName: string, paymentNetworkVersion = '0.1.0'): string => { + const version = + NearConversionNativeTokenPaymentDetector.getVersionOrThrow(paymentNetworkVersion); + const versionMap: Record> = { + aurora: { '0.1.0': 'requestnetwork.conversion.near' }, + 'aurora-testnet': { + '0.1.0': 'dev-xxxxxxx', + }, + }; + if (versionMap[chainName]?.[version]) { + return versionMap[chainName][version]; + } + throw Error(`Unconfigured chain '${chainName}' and version '${version}'.`); + }; + + /** + * Extracts the events for an address and a payment reference + * + * @param address Address to check + * @param eventName Indicate if it is an address for payment or refund + * @param requestCurrency The request currency + * @param paymentReference The reference to identify the payment + * @param paymentNetwork the payment network state + * @returns The balance with events + */ + protected async extractEvents( + eventName: PaymentTypes.EVENTS_NAMES, + address: string | undefined, + paymentReference: string, + requestCurrency: RequestLogicTypes.ICurrency, + paymentChain: string, + paymentNetwork: ExtensionTypes.IState, + ): Promise> { + if (!address) { + return { + paymentEvents: [], + }; + } + + const currency = this.currencyManager.fromStorageCurrency(requestCurrency); + if (!currency) { + throw new UnsupportedCurrencyError(requestCurrency.value); + } + + const infoRetriever = new NearConversionInfoRetriever( + currency, + paymentReference, + address, + NearConversionNativeTokenPaymentDetector.getContractName( + paymentChain, + paymentNetwork.version, + ), + eventName, + paymentChain, + paymentNetwork.values.maxRateTimespan, + ); + const paymentEvents = await infoRetriever.getTransferEvents(); + return { + paymentEvents, + }; + } + + protected getPaymentChain(request: RequestLogicTypes.IRequest): string { + const network = this.getPaymentExtension(request).values.network; + if (!network) { + throw Error(`request.extensions[${this.paymentNetworkId}].values.network must be defined`); + } + return network; + } + + protected static getVersionOrThrow = (paymentNetworkVersion: string): string => { + if (!CONTRACT_ADDRESS_MAP[paymentNetworkVersion]) { + throw Error(`Near payment detection not implemented for version ${paymentNetworkVersion}`); + } + return CONTRACT_ADDRESS_MAP[paymentNetworkVersion]; + }; +} diff --git a/packages/payment-detection/src/near-detector.ts b/packages/payment-detection/src/near/near-detector.ts similarity index 95% rename from packages/payment-detection/src/near-detector.ts rename to packages/payment-detection/src/near/near-detector.ts index 0a48b00ef6..590db1748b 100644 --- a/packages/payment-detection/src/near-detector.ts +++ b/packages/payment-detection/src/near/near-detector.ts @@ -5,8 +5,8 @@ import { RequestLogicTypes, } from '@requestnetwork/types'; -import { ReferenceBasedDetector } from './reference-based-detector'; -import { NearInfoRetriever } from './near-info-retriever'; +import { ReferenceBasedDetector } from '../reference-based-detector'; +import { NearInfoRetriever } from './retrievers/near-info-retriever'; // interface of the object indexing the proxy contract version interface IProxyContractVersion { diff --git a/packages/payment-detection/src/near/retrievers/near-conversion-info-retriever.ts b/packages/payment-detection/src/near/retrievers/near-conversion-info-retriever.ts new file mode 100644 index 0000000000..3059f0c4f2 --- /dev/null +++ b/packages/payment-detection/src/near/retrievers/near-conversion-info-retriever.ts @@ -0,0 +1,62 @@ +import { PaymentTypes } from '@requestnetwork/types'; +import { CurrencyDefinition } from 'currency/dist'; +import { NearInfoRetriever } from './near-info-retriever'; + +// FIXME#1: when Near subgraphes can retrieve a txHash, replace the custom IPaymentNetworkEvent with PaymentTypes.ETHPaymentNetworkEvent +interface NearSubGraphPaymentEvent extends PaymentTypes.IETHPaymentEventParameters { + receiptId: string; +} + +/** + * Gets a list of transfer events for a set of Near payment details + */ +export class NearConversionInfoRetriever extends NearInfoRetriever { + /** + * @param paymentReference The reference to identify the payment + * @param toAddress Address to check + * @param eventName Indicate if it is an address for payment or refund + * @param network The id of network we want to check + */ + constructor( + protected requestCurrency: CurrencyDefinition, + protected paymentReference: string, + protected toAddress: string, + protected proxyContractName: string, + protected eventName: PaymentTypes.EVENTS_NAMES, + protected network: string, + protected maxRateTimespan: number = 0, + ) { + super(paymentReference, toAddress, proxyContractName, eventName, network); + } + + public async getTransferEvents(): Promise< + PaymentTypes.IPaymentNetworkEvent[] + > { + const payments = await this.client.GetNearConversionPayments({ + reference: this.paymentReference, + to: this.toAddress, + currency: this.requestCurrency.symbol, + maxRateTimespan: this.maxRateTimespan, + contractAddress: this.proxyContractName, + }); + return payments.payments.map((p: any) => ({ + amount: p.amount, + name: this.eventName, + parameters: { + block: p.block, + feeAddress: p.feeAddress || undefined, + feeAmount: p.feeAmount, + feeAmountInCrypto: p.feeAmountInCrypto || undefined, + amountInCrypto: p.amountInCrypto, + to: this.toAddress, + maxRateTimespan: p.maxRateTimespan?.toString(), + from: p.from, + gasUsed: p.gasUsed, + gasPrice: p.gasPrice, + receiptId: p.receiptId, + currency: p.currency, + }, + timestamp: Number(p.timestamp), + })); + } +} diff --git a/packages/payment-detection/src/near-info-retriever.ts b/packages/payment-detection/src/near/retrievers/near-info-retriever.ts similarity index 68% rename from packages/payment-detection/src/near-info-retriever.ts rename to packages/payment-detection/src/near/retrievers/near-info-retriever.ts index 2bd957932a..bc79d01b97 100644 --- a/packages/payment-detection/src/near-info-retriever.ts +++ b/packages/payment-detection/src/near/retrievers/near-info-retriever.ts @@ -1,5 +1,5 @@ import { PaymentTypes } from '@requestnetwork/types'; -import { getTheGraphNearClient, TheGraphClient } from '.'; +import { getTheGraphNearClient, TheGraphClient } from '../../thegraph'; // FIXME#1: when Near subgraphes can retrieve a txHash, replace the custom IPaymentNetworkEvent with PaymentTypes.ETHPaymentNetworkEvent interface NearSubGraphPaymentEvent extends PaymentTypes.IETHPaymentEventParameters { @@ -10,33 +10,25 @@ interface NearSubGraphPaymentEvent extends PaymentTypes.IETHPaymentEventParamete * Gets a list of transfer events for a set of Near payment details */ export class NearInfoRetriever { - private client: TheGraphClient<'near'>; + protected client: TheGraphClient<'near'>; /** * @param paymentReference The reference to identify the payment * @param toAddress Address to check * @param eventName Indicate if it is an address for payment or refund * @param network The id of network we want to check + * */ constructor( - private paymentReference: string, - private toAddress: string, - private proxyContractName: string, - private eventName: PaymentTypes.EVENTS_NAMES, - private network: string, + protected paymentReference: string, + protected toAddress: string, + protected proxyContractName: string, + protected eventName: PaymentTypes.EVENTS_NAMES, + protected network: string, ) { if (this.network !== 'aurora' && this.network !== 'aurora-testnet') { throw new Error('Near input data info-retriever only works with Near mainnet and testnet'); } - if (this.network !== 'aurora') { - // FIXME: remove this check and implement testnet detection once aurora-testnet subgraphes are available - throw new Error('FIXME: getTransactionsFromNearSubGraph() only implemented for Near mainnet'); - } this.network = this.network.replace('aurora', 'near'); - if (this.proxyContractName !== 'requestnetwork.near') { - throw new Error( - `Proxy contract "${proxyContractName}" not supported by Near subgraph retriever`, - ); - } this.client = getTheGraphNearClient(this.network as 'near' | 'near-testnet'); } @@ -46,6 +38,7 @@ export class NearInfoRetriever { const payments = await this.client.GetNearPayments({ reference: this.paymentReference, to: this.toAddress, + contractAddress: this.proxyContractName, }); return payments.payments.map((p) => ({ amount: p.amount, diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 6362ef7b9b..0c9fa2728e 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -16,7 +16,7 @@ import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EthInputDataPaymentDetector } from './eth/input-data'; import { EthFeeProxyPaymentDetector } from './eth/fee-proxy-detector'; import { AnyToERC20PaymentDetector } from './any/any-to-erc20-proxy'; -import { NearNativeTokenPaymentDetector } from './near-detector'; +import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector } from './near'; import { AnyToEthFeeProxyPaymentDetector } from './any/any-to-eth-proxy'; const PN_ID = PaymentTypes.PAYMENT_NETWORK_ID; @@ -59,6 +59,7 @@ const anyCurrencyPaymentNetwork: IPaymentNetworkModuleByType = { [PN_ID.ANY_TO_ERC20_PROXY]: AnyToERC20PaymentDetector, [PN_ID.DECLARATIVE]: DeclarativePaymentDetector, [PN_ID.ANY_TO_ETH_PROXY]: AnyToEthFeeProxyPaymentDetector, + [PN_ID.ANY_TO_NATIVE]: NearConversionNativeTokenPaymentDetector, }; /** Factory to create the payment network according to the currency and payment network type */ diff --git a/packages/payment-detection/src/thegraph/queries/near/GetConversionPayments.graphql b/packages/payment-detection/src/thegraph/queries/near/GetConversionPayments.graphql new file mode 100644 index 0000000000..120b02dc19 --- /dev/null +++ b/packages/payment-detection/src/thegraph/queries/near/GetConversionPayments.graphql @@ -0,0 +1,31 @@ +query GetNearConversionPayments( + $reference: String! + $to: String! + $currency: String! + $maxRateTimespan: Int! + $contractAddress: String! +) { + payments( + where: { + reference: $reference + to: $to + currency: $currency + maxRateTimespan_gte: $maxRateTimespan + contractAddress: $contractAddress + } + orderBy: timestamp + orderDirection: asc + ) { + amount + block + receiptId + feeAmount + feeAddress + from + timestamp + currency + maxRateTimespan + amountInCrypto + feeAmountInCrypto + } +} diff --git a/packages/payment-detection/src/thegraph/queries/near/GetPayments.graphql b/packages/payment-detection/src/thegraph/queries/near/GetPayments.graphql index a8c9afacc4..f8af0845c6 100644 --- a/packages/payment-detection/src/thegraph/queries/near/GetPayments.graphql +++ b/packages/payment-detection/src/thegraph/queries/near/GetPayments.graphql @@ -1,5 +1,9 @@ -query GetNearPayments($reference: String!, $to: String!) { - payments(where: { reference: $reference, to: $to }, orderBy: timestamp, orderDirection: asc) { +query GetNearPayments($reference: String!, $to: String!, $contractAddress: String!) { + payments( + where: { reference: $reference, to: $to, contractAddress: $contractAddress } + orderBy: timestamp + orderDirection: asc + ) { amount block receiptId diff --git a/packages/payment-detection/test/near/near-native-conversion.test.ts b/packages/payment-detection/test/near/near-native-conversion.test.ts new file mode 100644 index 0000000000..17656dcbee --- /dev/null +++ b/packages/payment-detection/test/near/near-native-conversion.test.ts @@ -0,0 +1,203 @@ +import { + AdvancedLogicTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { CurrencyDefinition, CurrencyManager } from '@requestnetwork/currency'; +import PaymentNetworkFactory from '../../src/payment-network-factory'; +import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; +import { + NearConversionNativeTokenPaymentDetector, + NearConversionInfoRetriever, +} from '../../src/near'; +import { deepCopy } from 'ethers/lib/utils'; +import { GraphQLClient } from 'graphql-request'; +import { mocked } from 'ts-jest/utils'; + +jest.mock('graphql-request'); +const graphql = mocked(GraphQLClient.prototype); +const mockNearPaymentNetwork = { + supportedNetworks: ['aurora', 'aurora-testnet'], +}; +const currencyManager = CurrencyManager.getDefault(); + +const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { + applyActionToExtensions(): any { + return; + }, + extensions: { anyToNativeToken: [mockNearPaymentNetwork] }, +}; +const salt = 'a6475e4c3d45feb6'; +const paymentAddress = 'gus.near'; +const feeAddress = 'fee.near'; +const network = 'aurora'; +const feeAmount = '5'; +const receiptId = 'FYVnCvJFoNtK7LE2uAdTFfReFMGiCUHMczLsvEni1Cpf'; +const requestCurrency = currencyManager.from('USD') as CurrencyDefinition; +const request: any = { + requestId: '01c9190b6d015b3a0b2bbd0e492b9474b0734ca19a16f2fda8f7adec10d0fa3e7a', + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'USD', + }, + extensions: { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN as string]: { + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress, + salt, + feeAddress, + feeAmount, + network, + }, + version: '0.1.0', + }, + }, +}; +const graphPaymentEvent = { + amount: '5000000000', + amountInCrypto: '10000000000', + block: 10088347, + currency: 'USD', + feeAddress, + feeAmount: '5000000', + feeAmountInCrypto: '10000000', + from: 'payer.near', + maxRateTimespan: 0, + timestamp: 1643647285, + receiptId, + gasUsed: '144262', + gasPrice: '2425000017', +}; +const expectedRetrieverEvent = { + amount: graphPaymentEvent.amount, + name: 'payment', + parameters: { + ...graphPaymentEvent, + amount: undefined, + timestamp: undefined, + to: paymentAddress, + maxRateTimespan: graphPaymentEvent.maxRateTimespan.toString(), + }, + timestamp: graphPaymentEvent.timestamp, +}; + +describe('Near payments detection', () => { + beforeAll(() => { + graphql.request.mockResolvedValue({ + payments: [graphPaymentEvent], + }); + }); + + it('NearConversionInfoRetriever can retrieve a NEAR payment', async () => { + const paymentReference = PaymentReferenceCalculator.calculate( + request.requestId, + salt, + paymentAddress, + ); + + const infoRetriever = new NearConversionInfoRetriever( + requestCurrency, + paymentReference, + paymentAddress, + 'requestnetwork.conversion.near', + PaymentTypes.EVENTS_NAMES.PAYMENT, + 'aurora', + ); + const events = await infoRetriever.getTransferEvents(); + expect(events).toHaveLength(1); + expect(events[0]).toEqual(expectedRetrieverEvent); + }); + + it('PaymentNetworkFactory can get the detector (testnet)', async () => { + expect( + PaymentNetworkFactory.getPaymentNetworkFromRequest({ + advancedLogic: mockAdvancedLogic, + request, + currencyManager, + }), + ).toBeInstanceOf(NearConversionNativeTokenPaymentDetector); + }); + + it('PaymentNetworkFactory can get the detector (mainnet)', async () => { + expect( + PaymentNetworkFactory.getPaymentNetworkFromRequest({ + advancedLogic: mockAdvancedLogic, + request: { ...request, currency: { ...request.currency, network: 'aurora' } }, + currencyManager, + }), + ).toBeInstanceOf(NearConversionNativeTokenPaymentDetector); + }); + + it('NearConversionNativeTokenPaymentDetector can detect a payment on Near', async () => { + const paymentDetector = new NearConversionNativeTokenPaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + }); + const balance = await paymentDetector.getBalance(request); + + expect(balance.events).toHaveLength(1); + expect(balance.balance).toBe(graphPaymentEvent.amount); + }); + + describe('Edge cases for NearConversionNativeTokenPaymentDetector', () => { + it('throws with a wrong version', async () => { + let requestWithWrongVersion = deepCopy(request); + requestWithWrongVersion = { + ...requestWithWrongVersion, + extensions: { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN]: { + ...requestWithWrongVersion.extensions[ + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN + ], + version: '3.14', + }, + }, + }; + const paymentDetector = new NearConversionNativeTokenPaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + }); + expect(await paymentDetector.getBalance(requestWithWrongVersion)).toMatchObject({ + balance: null, + error: { code: 0, message: 'Near payment detection not implemented for version 3.14' }, + events: [], + }); + }); + + it('throws with a wrong network', async () => { + let requestWithWrongNetwork = deepCopy(request); + requestWithWrongNetwork = { + ...requestWithWrongNetwork, + extensions: { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN as string]: { + ...requestWithWrongNetwork.extensions[ + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN + ], + values: { + ...requestWithWrongNetwork.extensions[ + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN + ].values, + network: 'unknown-network', + }, + }, + }, + }; + const paymentDetector = new NearConversionNativeTokenPaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + }); + expect(await paymentDetector.getBalance(requestWithWrongNetwork)).toMatchObject({ + balance: null, + error: { + code: 2, + message: + 'Payment network unknown-network not supported by pn-any-to-native-token payment detection. Supported networks: aurora, aurora-testnet', + }, + events: [], + }); + }); + }); +}); diff --git a/packages/payment-detection/test/near-native.test.ts b/packages/payment-detection/test/near/near-native.test.ts similarity index 94% rename from packages/payment-detection/test/near-native.test.ts rename to packages/payment-detection/test/near/near-native.test.ts index 0d9192f7b1..8bd3e56e4a 100644 --- a/packages/payment-detection/test/near-native.test.ts +++ b/packages/payment-detection/test/near/near-native.test.ts @@ -5,10 +5,9 @@ import { RequestLogicTypes, } from '@requestnetwork/types'; import { CurrencyManager } from '@requestnetwork/currency'; -import PaymentNetworkFactory from '../src/payment-network-factory'; -import PaymentReferenceCalculator from '../src/payment-reference-calculator'; -import { NearNativeTokenPaymentDetector } from '../src/near-detector'; -import { NearInfoRetriever } from '../src/near-info-retriever'; +import PaymentNetworkFactory from '../../src/payment-network-factory'; +import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; +import { NearNativeTokenPaymentDetector, NearInfoRetriever } from '../../src/near'; import { deepCopy } from 'ethers/lib/utils'; const mockNearPaymentNetwork = { diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 87b1544f39..3e87eead05 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -14,6 +14,7 @@ export enum PAYMENT_NETWORK_ID { ETH_INPUT_DATA = Extension.ID.PAYMENT_NETWORK_ETH_INPUT_DATA, ETH_FEE_PROXY_CONTRACT = Extension.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT, NATIVE_TOKEN = Extension.ID.PAYMENT_NETWORK_NATIVE_TOKEN, + ANY_TO_NATIVE = Extension.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, DECLARATIVE = Extension.ID.PAYMENT_NETWORK_ANY_DECLARATIVE, ANY_TO_ERC20_PROXY = Extension.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, ANY_TO_ETH_PROXY = Extension.ID.PAYMENT_NETWORK_ANY_TO_ETH_PROXY, From 2386f1b68c3ac9c2d7e33688dcaa871eff17850a Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:58:56 +0200 Subject: [PATCH 015/207] fix: NEAR contracts addresses for conversion-native (#922) --- .../payment-detection/src/near/near-conversion-detector.ts | 4 ++-- .../test/near/near-native-conversion.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/payment-detection/src/near/near-conversion-detector.ts b/packages/payment-detection/src/near/near-conversion-detector.ts index be000a1dca..e6a88336bc 100644 --- a/packages/payment-detection/src/near/near-conversion-detector.ts +++ b/packages/payment-detection/src/near/near-conversion-detector.ts @@ -47,9 +47,9 @@ export class NearConversionNativeTokenPaymentDetector extends AnyToAnyDetector< const version = NearConversionNativeTokenPaymentDetector.getVersionOrThrow(paymentNetworkVersion); const versionMap: Record> = { - aurora: { '0.1.0': 'requestnetwork.conversion.near' }, + aurora: { '0.1.0': 'native.conversion.reqnetwork.near' }, 'aurora-testnet': { - '0.1.0': 'dev-xxxxxxx', + '0.1.0': 'native.conversion.reqnetwork.testnet', }, }; if (versionMap[chainName]?.[version]) { diff --git a/packages/payment-detection/test/near/near-native-conversion.test.ts b/packages/payment-detection/test/near/near-native-conversion.test.ts index 17656dcbee..1478fff60d 100644 --- a/packages/payment-detection/test/near/near-native-conversion.test.ts +++ b/packages/payment-detection/test/near/near-native-conversion.test.ts @@ -102,7 +102,7 @@ describe('Near payments detection', () => { requestCurrency, paymentReference, paymentAddress, - 'requestnetwork.conversion.near', + 'native.conversion.mock', PaymentTypes.EVENTS_NAMES.PAYMENT, 'aurora', ); From 78671d691c8c7f124fa9de9a3dc3e567d6b934ac Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:54:49 +0200 Subject: [PATCH 016/207] chore: replaced references to chainlink with aggregators (#923) --- ...gregators.ts => conversion-aggregators.ts} | 48 ++++++++++++++----- packages/currency/src/currencyManager.ts | 11 +++-- packages/currency/src/index.ts | 5 +- .../test/chainlink-path-aggregators.test.ts | 2 +- .../src/chainlinkConversionPathTools.ts | 2 +- .../src/commands/chainlink/addAggregators.ts | 4 +- 6 files changed, 48 insertions(+), 24 deletions(-) rename packages/currency/src/{chainlink-path-aggregators.ts => conversion-aggregators.ts} (58%) diff --git a/packages/currency/src/chainlink-path-aggregators.ts b/packages/currency/src/conversion-aggregators.ts similarity index 58% rename from packages/currency/src/chainlink-path-aggregators.ts rename to packages/currency/src/conversion-aggregators.ts index 6fff0701d6..3795627a1b 100644 --- a/packages/currency/src/chainlink-path-aggregators.ts +++ b/packages/currency/src/conversion-aggregators.ts @@ -9,29 +9,55 @@ import fantomAggregator from './aggregators/fantom.json'; import nearAggregator from './aggregators/near.json'; import nearTestnetAggregator from './aggregators/near-testnet.json'; +/** + * currencyFrom => currencyTo => cost + */ export type CurrencyPairs = Record>; -// List of currencies supported by network (can be generated from requestNetwork/toolbox/src/chainlinkConversionPathTools.ts) -// Network => currencyFrom => currencyTo => cost -// Must be updated every time an aggregator is added -export const chainlinkCurrencyPairs: Record = { + +/** + * Aggregators maps define pairs of currencies for which an onchain oracle exists, by network. + * + * Network => currencyFrom => currencyTo => cost + */ +export type AggregatorsMap = Record; + +// Pairs supported by Chainlink (can be generated from requestNetwork/toolbox/src/chainlinkConversionPathTools.ts) +const chainlinkCurrencyPairs: AggregatorsMap = { private: privateAggregator, rinkeby: rinkebyAggregator, - goerli: {}, mainnet: mainnetAggregator, matic: maticAggregator, fantom: fantomAggregator, - // FIX ME: This fix enables to get these networks registered in chainlinkSupportedNetworks. - // Could be improved by removing the supported network check from the protocol +}; + +// Pairs supported by Flux Protocol +const fluxCurrencyPairs: AggregatorsMap = { + aurora: nearAggregator, + 'aurora-testnet': nearTestnetAggregator, +}; + +// FIX ME: This fix enables to get these networks registered in conversionSupportedNetworks. +// Could be improved by removing the supported network check from the protocol +const noConversionNetworks: AggregatorsMap = { + goerli: {}, 'arbitrum-rinkeby': {}, 'arbitrum-one': {}, xdai: {}, avalanche: {}, bsc: {}, - aurora: nearAggregator, - 'aurora-testnet': nearTestnetAggregator, }; -export const chainlinkSupportedNetworks = Object.keys(chainlinkCurrencyPairs); +/** + * Conversion paths per network used by default if no other path given to the Currency Manager. + * Must be updated every time an aggregator is added to one network. + */ +export const defaultConversionPairs: AggregatorsMap = { + ...chainlinkCurrencyPairs, + ...fluxCurrencyPairs, + ...noConversionNetworks, +}; + +export const conversionSupportedNetworks = Object.keys(defaultConversionPairs); /** * Gets the on-chain conversion path between two currencies. @@ -47,7 +73,7 @@ export function getPath( currencyFrom: Pick, currencyTo: Pick, network = 'mainnet', - pairs = chainlinkCurrencyPairs, + pairs = defaultConversionPairs, ): string[] | null { if (!pairs[network]) { throw Error(`network ${network} not supported`); diff --git a/packages/currency/src/currencyManager.ts b/packages/currency/src/currencyManager.ts index 86d78b0f09..3e52a4438f 100644 --- a/packages/currency/src/currencyManager.ts +++ b/packages/currency/src/currencyManager.ts @@ -15,7 +15,7 @@ import { LegacyTokenMap, NativeCurrencyType, } from './types'; -import { chainlinkCurrencyPairs, CurrencyPairs, getPath } from './chainlink-path-aggregators'; +import { defaultConversionPairs, AggregatorsMap, getPath } from './conversion-aggregators'; import { isValidNearAddress } from './currency-utils'; const { BTC, ERC20, ERC777, ETH, ISO4217 } = RequestLogicTypes.CURRENCY; @@ -26,17 +26,18 @@ const { BTC, ERC20, ERC777, ETH, ISO4217 } = RequestLogicTypes.CURRENCY; export class CurrencyManager implements ICurrencyManager { private readonly knownCurrencies: CurrencyDefinition[]; private readonly legacyTokens: LegacyTokenMap; - private readonly conversionPairs: Record; + private readonly conversionPairs: AggregatorsMap; /** * * @param inputCurrencies The list of currencies known by the Manager. * @param legacyTokens A mapping of legacy currency name or network name, in the format { "chainName": {"TOKEN": ["NEW_TOKEN","NEW_CHAIN"]}} + * @param conversionPairs A mapping of possible conversions by network (network => currencyFrom => currencyTo => cost) */ constructor( inputCurrencies: (CurrencyInput & { id?: string; meta?: TMeta })[], legacyTokens?: LegacyTokenMap, - conversionPairs?: Record, + conversionPairs?: AggregatorsMap, ) { this.knownCurrencies = []; for (const input of inputCurrencies) { @@ -294,8 +295,8 @@ export class CurrencyManager implements ICurrencyManager }; } - static getDefaultConversionPairs(): Record { - return chainlinkCurrencyPairs; + static getDefaultConversionPairs(): AggregatorsMap { + return defaultConversionPairs; } /** diff --git a/packages/currency/src/index.ts b/packages/currency/src/index.ts index 17ff3c07dd..165f27f611 100644 --- a/packages/currency/src/index.ts +++ b/packages/currency/src/index.ts @@ -1,9 +1,6 @@ export { getSupportedERC20Tokens } from './erc20'; export { getSupportedERC777Tokens } from './erc777'; -export { - chainlinkSupportedNetworks as conversionSupportedNetworks, - CurrencyPairs, -} from './chainlink-path-aggregators'; +export { conversionSupportedNetworks, CurrencyPairs } from './conversion-aggregators'; export { getHash as getCurrencyHash } from './getHash'; export { CurrencyManager } from './currencyManager'; export * from './types'; diff --git a/packages/currency/test/chainlink-path-aggregators.test.ts b/packages/currency/test/chainlink-path-aggregators.test.ts index cd106ddaec..6427a2addd 100644 --- a/packages/currency/test/chainlink-path-aggregators.test.ts +++ b/packages/currency/test/chainlink-path-aggregators.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { CurrencyPairs, getPath } from '../src/chainlink-path-aggregators'; +import { CurrencyPairs, getPath } from '../src/conversion-aggregators'; import { CurrencyManager } from '../src'; const currencyManager = CurrencyManager.getDefault(); const USD = currencyManager.from('USD')!; diff --git a/packages/toolbox/src/chainlinkConversionPathTools.ts b/packages/toolbox/src/chainlinkConversionPathTools.ts index cb44717a80..585f9a0f69 100644 --- a/packages/toolbox/src/chainlinkConversionPathTools.ts +++ b/packages/toolbox/src/chainlinkConversionPathTools.ts @@ -178,7 +178,7 @@ export const listAggregators = async (options?: IOptions): Promise => { // enables this usage: yarn -s chainlinkPath mainnet | clip console.error('#####################################################################'); console.error('All aggregators nodes (currency) :'); - console.error('../currency/src/chainlink-path-aggregators.ts'); + console.error('../currency/src/conversion-aggregators.ts'); console.log(JSON.stringify(aggregatorsNodesForDijkstra, null, 2)); }; diff --git a/packages/toolbox/src/commands/chainlink/addAggregators.ts b/packages/toolbox/src/commands/chainlink/addAggregators.ts index 5f35b8b7e8..93eba581e6 100644 --- a/packages/toolbox/src/commands/chainlink/addAggregators.ts +++ b/packages/toolbox/src/commands/chainlink/addAggregators.ts @@ -75,8 +75,8 @@ export const handler = async (args: Options): Promise => { if (!conversionSupportedNetworks.includes(network)) { console.warn( - `WARNING: ${network} is missing in chainlinkSupportedNetworks from the Currency package.`, - `Add '${network}: {}' to chainlinkCurrencyPairs, in currency/src/chainlink-path-aggregators.ts.`, + `WARNING: ${network} is missing in conversionSupportedNetworks from the Currency package.`, + `Add '${network}: {}' to chainlinkCurrencyPairs, in currency/src/conversion-aggregators.ts.`, ); } From c2e44c5ffd77b6b754bfbd8a1295b9a3801e9a69 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Mon, 3 Oct 2022 18:43:18 +0200 Subject: [PATCH 017/207] feat: goerli aggregators (#924) --- packages/currency/src/aggregators/goerli.json | 12 ++++++++++++ packages/currency/src/conversion-aggregators.ts | 3 ++- packages/currency/src/erc20/networks/goerli.ts | 6 +++--- packages/currency/src/erc20/networks/index.ts | 3 +++ .../src/commands/chainlink/aggregatorsUtils.ts | 13 +++++++------ 5 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 packages/currency/src/aggregators/goerli.json diff --git a/packages/currency/src/aggregators/goerli.json b/packages/currency/src/aggregators/goerli.json new file mode 100644 index 0000000000..1b82e896f5 --- /dev/null +++ b/packages/currency/src/aggregators/goerli.json @@ -0,0 +1,12 @@ +{ + "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 + }, + "0x39e19aa5b69466dfdc313c7cda37cb2a599015cd": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 + }, + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": { + "0xba62bcfcaafc6622853cca2be6ac7d845bc0f2dc": 1, + "0x39e19aa5b69466dfdc313c7cda37cb2a599015cd": 1 + } +} diff --git a/packages/currency/src/conversion-aggregators.ts b/packages/currency/src/conversion-aggregators.ts index 3795627a1b..9a29874ef1 100644 --- a/packages/currency/src/conversion-aggregators.ts +++ b/packages/currency/src/conversion-aggregators.ts @@ -3,6 +3,7 @@ import { CurrencyDefinition } from './types'; import privateAggregator from './aggregators/private.json'; import mainnetAggregator from './aggregators/mainnet.json'; +import goerliAggregator from './aggregators/goerli.json'; import rinkebyAggregator from './aggregators/rinkeby.json'; import maticAggregator from './aggregators/matic.json'; import fantomAggregator from './aggregators/fantom.json'; @@ -24,6 +25,7 @@ export type AggregatorsMap = Record; // Pairs supported by Chainlink (can be generated from requestNetwork/toolbox/src/chainlinkConversionPathTools.ts) const chainlinkCurrencyPairs: AggregatorsMap = { private: privateAggregator, + goerli: goerliAggregator, rinkeby: rinkebyAggregator, mainnet: mainnetAggregator, matic: maticAggregator, @@ -39,7 +41,6 @@ const fluxCurrencyPairs: AggregatorsMap = { // FIX ME: This fix enables to get these networks registered in conversionSupportedNetworks. // Could be improved by removing the supported network check from the protocol const noConversionNetworks: AggregatorsMap = { - goerli: {}, 'arbitrum-rinkeby': {}, 'arbitrum-one': {}, xdai: {}, diff --git a/packages/currency/src/erc20/networks/goerli.ts b/packages/currency/src/erc20/networks/goerli.ts index badb77bd56..b8529468ec 100644 --- a/packages/currency/src/erc20/networks/goerli.ts +++ b/packages/currency/src/erc20/networks/goerli.ts @@ -2,10 +2,10 @@ import { TokenMap } from './types'; // List of the supported goerli ERC20 tokens export const supportedGoerliERC20: TokenMap = { - // Faucet Token on goerli network. Easy to use on tests. + // Faucet Token on goerli network. '0xBA62BCfcAaFc6622853cca2BE6Ac7d845BC0f2Dc': { decimals: 18, - name: 'Faucet Token', - symbol: 'FAU-goerli', + name: 'FaucetToken', + symbol: 'FAU', }, }; diff --git a/packages/currency/src/erc20/networks/index.ts b/packages/currency/src/erc20/networks/index.ts index d64c907abc..ee43dbdebe 100644 --- a/packages/currency/src/erc20/networks/index.ts +++ b/packages/currency/src/erc20/networks/index.ts @@ -7,10 +7,13 @@ import { supportedFantomTokens } from './fantom'; import { supportedBSCTestERC20 } from './bsctest'; import { supportedBSCERC20 } from './bsc'; import { supportedXDAIERC20 } from './xdai'; +import { supportedGoerliERC20 } from './goerli'; export const supportedNetworks: Record = { celo: supportedCeloERC20, + // FIXME: Rinkeby is deprecated rinkeby: supportedRinkebyERC20, + goerli: supportedGoerliERC20, mainnet: supportedMainnetERC20, matic: supportedMaticERC20, fantom: supportedFantomTokens, diff --git a/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts b/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts index 4b65a7c06b..0fcb23941a 100644 --- a/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts +++ b/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts @@ -27,12 +27,13 @@ export type Aggregator = { }; const feedMap: Record = { - mainnet: ['ethereum-addresses', 'Ethereum Mainnet'], - rinkeby: ['ethereum-addresses', 'Rinkeby Testnet'], - fantom: ['fantom-price-feeds', 'Fantom Mainnet'], - matic: ['matic-addresses', 'Polygon Mainnet'], - xdai: ['data-feeds-gnosis-chain', 'Gnosis Chain Mainnet'], - bsc: ['bnb-chain-addresses-price', 'BNB Chain Mainnet'], + mainnet: ['ethereum', 'Ethereum Mainnet'], + goerli: ['ethereum', 'Goerli Testnet'], + rinkeby: ['ethereum', 'Rinkeby Testnet'], + fantom: ['fantom', 'Fantom Mainnet'], + matic: ['polygon', 'Polygon Mainnet'], + xdai: ['gnosis-chain', 'Gnosis Chain Mainnet'], + bsc: ['bnb-chain', 'BNB Chain Mainnet'], }; export const getAvailableAggregators = async ( From af836c29405adbeb994af24e324e83b57a07997f Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Mon, 3 Oct 2022 18:54:32 +0200 Subject: [PATCH 018/207] feat: near conversion payment processor (#921) --- .../test/near/near-native-conversion.test.ts | 45 +++-- .../src/payment/near-conversion.ts | 74 ++++++++ .../src/payment/utils-near.ts | 70 +++++++- .../payment-processor/src/payment/utils.ts | 17 +- .../test/payment/any-to-near.test.ts | 165 ++++++++++++++++++ packages/types/src/request-logic-types.ts | 4 + 6 files changed, 344 insertions(+), 31 deletions(-) create mode 100644 packages/payment-processor/src/payment/near-conversion.ts create mode 100644 packages/payment-processor/test/payment/any-to-near.test.ts diff --git a/packages/payment-detection/test/near/near-native-conversion.test.ts b/packages/payment-detection/test/near/near-native-conversion.test.ts index 1478fff60d..4d5725e68e 100644 --- a/packages/payment-detection/test/near/near-native-conversion.test.ts +++ b/packages/payment-detection/test/near/near-native-conversion.test.ts @@ -29,7 +29,7 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { extensions: { anyToNativeToken: [mockNearPaymentNetwork] }, }; const salt = 'a6475e4c3d45feb6'; -const paymentAddress = 'gus.near'; +const paymentAddress = 'issuer.near'; const feeAddress = 'fee.near'; const network = 'aurora'; const feeAmount = '5'; @@ -57,32 +57,23 @@ const request: any = { }, }; const graphPaymentEvent = { - amount: '5000000000', - amountInCrypto: '10000000000', + // 500 USD + amount: '50000', + amountInCrypto: null, block: 10088347, currency: 'USD', feeAddress, - feeAmount: '5000000', - feeAmountInCrypto: '10000000', + // .05 USD + feeAmount: '5', + feeAmountInCrypto: null, from: 'payer.near', + to: paymentAddress, maxRateTimespan: 0, timestamp: 1643647285, receiptId, gasUsed: '144262', gasPrice: '2425000017', }; -const expectedRetrieverEvent = { - amount: graphPaymentEvent.amount, - name: 'payment', - parameters: { - ...graphPaymentEvent, - amount: undefined, - timestamp: undefined, - to: paymentAddress, - maxRateTimespan: graphPaymentEvent.maxRateTimespan.toString(), - }, - timestamp: graphPaymentEvent.timestamp, -}; describe('Near payments detection', () => { beforeAll(() => { @@ -108,7 +99,25 @@ describe('Near payments detection', () => { ); const events = await infoRetriever.getTransferEvents(); expect(events).toHaveLength(1); - expect(events[0]).toEqual(expectedRetrieverEvent); + expect(events[0]).toEqual({ + amount: graphPaymentEvent.amount, + name: 'payment', + parameters: { + amountInCrypto: null, + block: 10088347, + currency: 'USD', + feeAddress, + feeAmount: '5', + feeAmountInCrypto: undefined, + from: 'payer.near', + to: paymentAddress, + maxRateTimespan: '0', + receiptId, + gasUsed: '144262', + gasPrice: '2425000017', + }, + timestamp: graphPaymentEvent.timestamp, + }); }); it('PaymentNetworkFactory can get the detector (testnet)', async () => { diff --git a/packages/payment-processor/src/payment/near-conversion.ts b/packages/payment-processor/src/payment/near-conversion.ts new file mode 100644 index 0000000000..b76089e014 --- /dev/null +++ b/packages/payment-processor/src/payment/near-conversion.ts @@ -0,0 +1,74 @@ +import { BigNumberish } from 'ethers'; +import { WalletConnection } from 'near-api-js'; + +import { ClientTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; + +import { + getRequestPaymentValues, + validateRequest, + getAmountToPay, + getPaymentExtensionVersion, +} from './utils'; +import { isNearNetwork, processNearPaymentWithConversion } from './utils-near'; +import { IConversionPaymentSettings } from '.'; +import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; + +/** + * Processes the transaction to pay a request in NEAR with on-chain conversion. + * @param request the request to pay + * @param walletConnection the Web3 provider, or signer. Defaults to window.ethereum. + * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. + */ +export async function payNearConversionRequest( + request: ClientTypes.IRequestData, + walletConnection: WalletConnection, + paymentSettings: IConversionPaymentSettings, + amount?: BigNumberish, +): Promise { + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE); + + const currencyManager = paymentSettings.currencyManager || CurrencyManager.getDefault(); + const { paymentReference, paymentAddress, feeAddress, feeAmount, maxRateTimespan, network } = + getRequestPaymentValues(request); + + const requestCurrency = currencyManager.fromStorageCurrency(request.currencyInfo); + if (!requestCurrency) { + throw new UnsupportedCurrencyError(request.currencyInfo); + } + + if (!paymentReference) { + throw new Error('Cannot pay without a paymentReference'); + } + + if (!network || !isNearNetwork(network)) { + throw new Error('Should be a near network'); + } + + const amountToPay = getAmountToPay(request, amount).toString(); + const version = getPaymentExtensionVersion(request); + + return processNearPaymentWithConversion( + walletConnection, + network, + amountToPay, + paymentAddress, + paymentReference, + getTicker(request.currencyInfo), + feeAddress || '0x', + feeAmount || 0, + maxRateTimespan || '0', + version, + ); +} + +const getTicker = (currency: RequestLogicTypes.ICurrency): string => { + switch (currency.type) { + case RequestLogicTypes.CURRENCY.ISO4217: + return currency.value; + default: + // FIXME: Flux oracles are compatible with ERC20 identified by tickers. Ex: USDT, DAI. + // Warning: although Flux oracles are compatible with ETH and BTC, the request contract + // for native payments and conversions only handles 2 decimals, not suited for cryptos. + throw new Error('Near payment with conversion only implemented for fiat denominations.'); + } +}; diff --git a/packages/payment-processor/src/payment/utils-near.ts b/packages/payment-processor/src/payment/utils-near.ts index 373a4073b0..0cf2d779c8 100644 --- a/packages/payment-processor/src/payment/utils-near.ts +++ b/packages/payment-processor/src/payment/utils-near.ts @@ -1,7 +1,10 @@ import { BigNumber, BigNumberish, ethers } from 'ethers'; import { Contract } from 'near-api-js'; import { Near, WalletConnection } from 'near-api-js'; -import { NearNativeTokenPaymentDetector } from '@requestnetwork/payment-detection'; +import { + NearNativeTokenPaymentDetector, + NearConversionNativeTokenPaymentDetector, +} from '@requestnetwork/payment-detection'; export const isValidNearAddress = async (nearNetwork: Near, address: string): Promise => { try { @@ -32,15 +35,12 @@ export const isNearAccountSolvent = ( const GAS_LIMIT_IN_TGAS = 50; const GAS_LIMIT = ethers.utils.parseUnits(GAS_LIMIT_IN_TGAS.toString(), 12); -/** - * Export used for mocking only. - */ export const processNearPayment = async ( walletConnection: WalletConnection, network: string, amount: BigNumberish, to: string, - payment_reference: string, + paymentReference: string, version = '0.2.0', ): Promise => { if (version !== '0.2.0') { @@ -67,7 +67,65 @@ export const processNearPayment = async ( await contract.transfer_with_reference( { to, - payment_reference, + payment_reference: paymentReference, + }, + GAS_LIMIT.toString(), + amount.toString(), + ); + return; + } catch (e) { + throw new Error(`Could not pay Near request. Got ${e.message}`); + } +}; + +/** + * Processes a payment in Near native token, with conversion. + * + * @param amount is defined with 2 decimals, denominated in `currency` + * @param currency is a currency ticker (e.g. "ETH" or "USD") + * @param maxRateTimespan accepts any kind rate's age if '0' + */ +export const processNearPaymentWithConversion = async ( + walletConnection: WalletConnection, + network: string, + amount: BigNumberish, + to: string, + paymentReference: string, + currency: string, + feeAddress: string, + feeAmount: BigNumberish, + maxRateTimespan = '0', + version = '0.1.0', +): Promise => { + if (version !== '0.1.0') { + throw new Error('Native Token with conversion payments on Near only support v0.1.0 extensions'); + } + + if (!(await isValidNearAddress(walletConnection._near, to))) { + throw new Error(`Invalid NEAR payment address: ${to}`); + } + + if (!(await isValidNearAddress(walletConnection._near, feeAddress))) { + throw new Error(`Invalid NEAR fee address: ${feeAddress}`); + } + try { + const contract = new Contract( + walletConnection.account(), + NearConversionNativeTokenPaymentDetector.getContractName(network, version), + { + changeMethods: ['transfer_with_reference'], + viewMethods: [], + }, + ) as any; + await contract.transfer_with_reference( + { + payment_reference: paymentReference, + to, + amount, + currency, + fee_address: feeAddress, + fee_amount: feeAmount, + max_rate_timespan: maxRateTimespan, }, GAS_LIMIT.toString(), amount.toString(), diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index f107162cf1..27c6d5c846 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -206,13 +206,16 @@ export function validateRequest( // Compatibility of the request currency type with the payment network const expectedCurrencyType = currenciesMap[paymentNetworkId]; - const validCurrencyType = - paymentNetworkId === PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY - ? // Any currency type is valid with Any to ERC20 conversion - true - : expectedCurrencyType && - request.currencyInfo.type === expectedCurrencyType && - request.currencyInfo.network; + const validCurrencyType = [ + PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE, + PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + ].includes(paymentNetworkId) + ? // Any currency type is valid with Any to ERC20 / ETH / Native conversion + true + : expectedCurrencyType && + request.currencyInfo.type === expectedCurrencyType && + request.currencyInfo.network; // ERC20 based payment networks are only valid if the request currency has a value const validCurrencyValue = diff --git a/packages/payment-processor/test/payment/any-to-near.test.ts b/packages/payment-processor/test/payment/any-to-near.test.ts new file mode 100644 index 0000000000..9bfd99a7c1 --- /dev/null +++ b/packages/payment-processor/test/payment/any-to-near.test.ts @@ -0,0 +1,165 @@ +import { ExtensionTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; +import * as Utils from '@requestnetwork/utils'; + +import { IConversionPaymentSettings, _getPaymentUrl } from '../../src/payment'; +import * as nearUtils from '../../src/payment/utils-near'; +import { payNearConversionRequest } from '../../src/payment/near-conversion'; + +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable @typescript-eslint/await-thenable */ + +const usdCurrency = { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'USD', +}; + +const salt = 'a6475e4c3d45feb6'; +const paymentAddress = 'gus.near'; +const feeAddress = 'fee.near'; +const network = 'aurora'; +const feeAmount = '5'; +const request: any = { + requestId: '0x123', + expectedAmount: '100', + currencyInfo: usdCurrency, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE]: { + events: [], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + salt, + paymentAddress, + feeAddress, + network, + feeAmount, + }, + version: '0.1.0', + }, + }, +}; + +// Use the default currency manager +const conversionSettings = {} as unknown as IConversionPaymentSettings; + +describe('payNearWithConversionRequest', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('pays a NEAR request with NEAR payment method (with mock)', async () => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + const paymentSpy = jest + .spyOn(nearUtils, 'processNearPaymentWithConversion') + .mockReturnValue(Promise.resolve()); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + + const paymentReference = PaymentReferenceCalculator.calculate( + request.requestId, + salt, + paymentAddress, + ); + + await payNearConversionRequest(request, mockedNearWalletConnection, conversionSettings); + expect(paymentSpy).toHaveBeenCalledWith( + expect.anything(), + 'aurora', + '100', + paymentAddress, + paymentReference, + 'USD', + feeAddress, + feeAmount, + '0', + '0.1.0', + ); + }); + it('throws when tyring to pay another payment extension', async () => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + const paymentSpy = jest + .spyOn(nearUtils, 'processNearPaymentWithConversion') + .mockReturnValue(Promise.resolve()); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + let invalidRequest = Utils.default.deepCopy(request); + invalidRequest = { + ...invalidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: { + ...invalidRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE], + }, + }, + }; + + await expect( + payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-any-to-native-token request', + ); + expect(paymentSpy).toHaveBeenCalledTimes(0); + }); + it('throws when tyring to pay with an unsupported currency', async () => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + const paymentSpy = jest + .spyOn(nearUtils, 'processNearPaymentWithConversion') + .mockReturnValue(Promise.resolve()); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + let invalidRequest = Utils.default.deepCopy(request); + invalidRequest = { + ...invalidRequest, + currencyInfo: { + type: RequestLogicTypes.CURRENCY.BTC, + value: 'BTC', + }, + }; + + await expect( + payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), + ).rejects.toThrowError('Near payment with conversion only implemented for fiat denominations.'); + expect(paymentSpy).toHaveBeenCalledTimes(0); + }); + it('throws when the netwrok is not near', async () => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + const paymentSpy = jest + .spyOn(nearUtils, 'processNearPaymentWithConversion') + .mockReturnValue(Promise.resolve()); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + let invalidRequest = Utils.default.deepCopy(request); + invalidRequest = { + ...invalidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE]: { + ...invalidRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE], + values: { + ...invalidRequest.extensions[PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE].values, + network: 'unknown-network', + }, + }, + }, + }; + + await expect( + payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), + ).rejects.toThrowError('Should be a near network'); + expect(paymentSpy).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/types/src/request-logic-types.ts b/packages/types/src/request-logic-types.ts index 3caa42b8b5..b9a2f93927 100644 --- a/packages/types/src/request-logic-types.ts +++ b/packages/types/src/request-logic-types.ts @@ -182,6 +182,10 @@ export interface IVersionSupportConfig { /** Parameters to create a request */ export interface ICreateParameters { currency: ICurrency; + /** + * `expectedAmount` in `currency`, given with the most precise decimal know for this currency. + * By convention, fiat amounts have a precision of 2, so '1000' for 'EUR' means '10.00 EUR'. + */ expectedAmount: Amount; payee?: Identity.IIdentity; payer?: Identity.IIdentity; From 44dc37496098d5ab09916c37d941dd13ad2d6678 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:27:25 +0200 Subject: [PATCH 019/207] chore(currency): export AggregatorsMap (#925) --- packages/currency/src/index.ts | 6 +++++- packages/currency/test/chainlink-path-aggregators.test.ts | 4 ++-- packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/currency/src/index.ts b/packages/currency/src/index.ts index 165f27f611..c08a5e8f54 100644 --- a/packages/currency/src/index.ts +++ b/packages/currency/src/index.ts @@ -1,6 +1,10 @@ export { getSupportedERC20Tokens } from './erc20'; export { getSupportedERC777Tokens } from './erc777'; -export { conversionSupportedNetworks, CurrencyPairs } from './conversion-aggregators'; +export { + conversionSupportedNetworks, + CurrencyPairs, + AggregatorsMap, +} from './conversion-aggregators'; export { getHash as getCurrencyHash } from './getHash'; export { CurrencyManager } from './currencyManager'; export * from './types'; diff --git a/packages/currency/test/chainlink-path-aggregators.test.ts b/packages/currency/test/chainlink-path-aggregators.test.ts index 6427a2addd..ad91d2f1db 100644 --- a/packages/currency/test/chainlink-path-aggregators.test.ts +++ b/packages/currency/test/chainlink-path-aggregators.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { CurrencyPairs, getPath } from '../src/conversion-aggregators'; +import { AggregatorsMap, getPath } from '../src/conversion-aggregators'; import { CurrencyManager } from '../src'; const currencyManager = CurrencyManager.getDefault(); const USD = currencyManager.from('USD')!; @@ -8,7 +8,7 @@ const EUR = currencyManager.from('EUR')!; const fakeDAI = { hash: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35' }; describe('getPath', () => { - const mockAggregatorPaths: Record = { + const mockAggregatorPaths: AggregatorsMap = { private: { [fakeDAI.hash]: { [USD.hash]: 1, diff --git a/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts b/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts index 0fcb23941a..fe227d6103 100644 --- a/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts +++ b/packages/toolbox/src/commands/chainlink/aggregatorsUtils.ts @@ -1,4 +1,4 @@ -import { CurrencyManager, CurrencyInput, CurrencyPairs } from '@requestnetwork/currency'; +import { CurrencyManager, CurrencyInput, AggregatorsMap } from '@requestnetwork/currency'; import axios from 'axios'; import { RequestLogicTypes } from '@requestnetwork/types'; @@ -93,7 +93,7 @@ const loadCurrencyApi = async (path: string): Promise => { }; export const getCurrencyManager = async (list?: string): Promise => { - const aggregators = await loadCurrencyApi>('/aggregators'); + const aggregators = await loadCurrencyApi('/aggregators'); const currencyList = list ? await loadCurrencyApi(`/list/${list}`) : CurrencyManager.getDefaultList(); From 464a63a1bb12d3fa722bc3610359fa828fd85e5b Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:20:23 +0200 Subject: [PATCH 020/207] fix: rinkeby deprecation workaround (#933) --- packages/payment-detection/test/any/any-to-eth.test.ts | 3 ++- .../payment-detection/test/erc20/escrow-info-retriever.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/payment-detection/test/any/any-to-eth.test.ts b/packages/payment-detection/test/any/any-to-eth.test.ts index 7b5666caff..7880019a31 100644 --- a/packages/payment-detection/test/any/any-to-eth.test.ts +++ b/packages/payment-detection/test/any/any-to-eth.test.ts @@ -68,7 +68,8 @@ describe('Any to ETH payment detection', () => { }, }; - it('RPC Payment detection', async () => { + // FIXME migrate to goerli or mock RPC call + it.skip('RPC Payment detection', async () => { getLogs .mockResolvedValueOnce([ { diff --git a/packages/payment-detection/test/erc20/escrow-info-retriever.test.ts b/packages/payment-detection/test/erc20/escrow-info-retriever.test.ts index 53a6246639..5c66e2c703 100644 --- a/packages/payment-detection/test/erc20/escrow-info-retriever.test.ts +++ b/packages/payment-detection/test/erc20/escrow-info-retriever.test.ts @@ -149,7 +149,8 @@ describe('api/erc20/escrow-info-retriever', () => { 'rinkeby', ); }); - it('should get escrow chain data', async () => { + // FIXME migrate to goerli or mock RPC call + it.skip('should get escrow chain data', async () => { const escrowChainData = await infoRetriever.getEscrowRequestMapping(); expect(escrowChainData.tokenAddress).toEqual('0x745861AeD1EEe363b4AaA5F1994Be40b1e05Ff90'); expect(escrowChainData.payee).toEqual('0xB9B7e0cb2EDF5Ea031C8B297A5A1Fa20379b6A0a'); From 8180cd296390a1c9f01980b086aad3c2a2a05d38 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:33:26 +0200 Subject: [PATCH 021/207] fix(payment-processor): missing NEAR conversion import (#934) --- packages/payment-processor/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index 0bbf676fbc..d67ed06c50 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -6,6 +6,7 @@ export * from './payment/erc20-fee-proxy'; export * from './payment/erc777-stream'; export * from './payment/eth-input-data'; export * from './payment/near-input-data'; +export * from './payment/near-conversion'; export * from './payment/eth-proxy'; export * from './payment/eth-fee-proxy'; export * from './payment/batch-proxy'; From 400b0d503ee69b7abd24d6e5ebc310d7c08699a8 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 6 Oct 2022 19:25:34 +0200 Subject: [PATCH 022/207] refactor: payment network factory (#930) --- packages/data-access/src/data-access.ts | 3 +- packages/data-access/test/data-access.test.ts | 7 +- .../integration-test/test/node-client.test.ts | 2 +- .../test/scheduled/btc-test-data.ts | 1 + .../test/scheduled/escrow-detector.test.ts | 2 +- .../test/{ => scheduled}/native-token.test.ts | 43 +++-- packages/payment-detection/src/index.ts | 3 +- .../src/payment-network-factory.ts | 159 ++++++------------ .../test/near/near-native-conversion.test.ts | 20 +-- .../test/near/near-native.test.ts | 21 +-- .../test/payment-network-factory.test.ts | 114 +++---------- .../src/api/request-network.ts | 77 +++------ .../src/http-request-network.ts | 32 +--- packages/request-client.js/src/index.ts | 3 +- packages/request-client.js/test/index.test.ts | 69 +++++--- 15 files changed, 205 insertions(+), 351 deletions(-) rename packages/integration-test/test/{ => scheduled}/native-token.test.ts (55%) diff --git a/packages/data-access/src/data-access.ts b/packages/data-access/src/data-access.ts index b9f46ab059..9df02bb3d4 100644 --- a/packages/data-access/src/data-access.ts +++ b/packages/data-access/src/data-access.ts @@ -57,8 +57,7 @@ const emptyChannelsWithTopics: DataAccessTypes.IReturnGetChannelsByTopic = { */ export default class DataAccess implements DataAccessTypes.IDataAccess { // Transaction index, that allows storing and retrieving transactions by channel or topic, with time boundaries. - // public for test purpose - public transactionIndex: DataAccessTypes.ITransactionIndex; + private transactionIndex: DataAccessTypes.ITransactionIndex; // boolean to store the initialization state protected isInitialized = false; diff --git a/packages/data-access/test/data-access.test.ts b/packages/data-access/test/data-access.test.ts index 37db3875a2..9f58ed0309 100644 --- a/packages/data-access/test/data-access.test.ts +++ b/packages/data-access/test/data-access.test.ts @@ -4,6 +4,7 @@ import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; import RequestDataAccessBlock from '../src/block'; import DataAccess from '../src/data-access'; +import TransactionIndex from '../src/transaction-index'; // We use this function to flush the call stack // If we don't use this function, the fake timer will be increased before the interval function being called @@ -637,10 +638,10 @@ describe('data-access', () => { readMany: jest.fn(), }; - const dataAccess = new DataAccess(fakeStorageWithNotJsonData); + const transactionIndex = new TransactionIndex(); + const dataAccess = new DataAccess(fakeStorageWithNotJsonData, { transactionIndex }); + const spy = jest.spyOn(transactionIndex, 'addTransaction').mockImplementation(); await dataAccess.initialize(); - const spy = jest.fn(); - dataAccess.transactionIndex.addTransaction = spy; await dataAccess.synchronizeNewDataIds(); expect(spy).not.toHaveBeenCalled(); diff --git a/packages/integration-test/test/node-client.test.ts b/packages/integration-test/test/node-client.test.ts index 6bcc511e98..f09d179c09 100644 --- a/packages/integration-test/test/node-client.test.ts +++ b/packages/integration-test/test/node-client.test.ts @@ -415,7 +415,7 @@ describe('Request client using a request node', () => { await fetchedRequest.refresh(); expect(fetchedRequest.getData().expectedAmount).toBe('0'); - }); + }, 60000); it('create an encrypted and unencrypted request with the same content', async () => { const requestNetwork = new RequestNetwork({ diff --git a/packages/integration-test/test/scheduled/btc-test-data.ts b/packages/integration-test/test/scheduled/btc-test-data.ts index 20470ddee7..24a89eacab 100644 --- a/packages/integration-test/test/scheduled/btc-test-data.ts +++ b/packages/integration-test/test/scheduled/btc-test-data.ts @@ -40,6 +40,7 @@ export const requestData = { currency: { type: RequestLogicTypes.CURRENCY.BTC, value: 'BTC', + network: 'mainnet', }, expectedAmount: '100000000000', payee: payee.identity, diff --git a/packages/integration-test/test/scheduled/escrow-detector.test.ts b/packages/integration-test/test/scheduled/escrow-detector.test.ts index 50e34dfce2..b15382a1a5 100644 --- a/packages/integration-test/test/scheduled/escrow-detector.test.ts +++ b/packages/integration-test/test/scheduled/escrow-detector.test.ts @@ -1,4 +1,4 @@ -import { Erc20PaymentNetwork } from '../../../payment-detection/dist'; +import { Erc20PaymentNetwork } from '@requestnetwork/payment-detection'; import { CurrencyManager } from '@requestnetwork/currency'; import { createMockErc20FeeRequest } from '../utils'; import { mockAdvancedLogic } from './mocks'; diff --git a/packages/integration-test/test/native-token.test.ts b/packages/integration-test/test/scheduled/native-token.test.ts similarity index 55% rename from packages/integration-test/test/native-token.test.ts rename to packages/integration-test/test/scheduled/native-token.test.ts index 113f3a2b61..490f864ac3 100644 --- a/packages/integration-test/test/native-token.test.ts +++ b/packages/integration-test/test/scheduled/native-token.test.ts @@ -3,13 +3,10 @@ import { PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; import { PnReferenceBased } from '@requestnetwork/types/dist/extension-types'; import { AdvancedLogic } from '@requestnetwork/advanced-logic'; import { CurrencyManager } from '@requestnetwork/currency'; +import { omit } from 'lodash'; const advancedLogic = new AdvancedLogic(); -const currency = { - network: 'aurora-testnet', - type: RequestLogicTypes.CURRENCY.ETH, - value: 'NEAR', -}; + const createCreationActionParams: PnReferenceBased.ICreationParameters = { paymentAddress: 'payment.testnet', salt: 'a1a2a3a4a5a6a7a8', @@ -17,32 +14,30 @@ const createCreationActionParams: PnReferenceBased.ICreationParameters = { }; describe('PaymentNetworkFactory and createExtensionsDataForCreation', () => { + const paymentNetworkFactory = new PaymentNetworkFactory( + advancedLogic, + CurrencyManager.getDefault(), + ); it('PaymentNetworkFactory can createPaymentNetwork (mainnet)', async () => { - const paymentNetwork = PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic, - currency: { ...currency, network: 'aurora-testnet' }, - paymentNetworkCreationParameters: { - id: PaymentTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, - parameters: createCreationActionParams, - }, - currencyManager: CurrencyManager.getDefault(), - }); + const paymentNetwork = paymentNetworkFactory.createPaymentNetwork( + PaymentTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, + RequestLogicTypes.CURRENCY.ETH, + 'aurora-testnet', + ); const action = await paymentNetwork.createExtensionsDataForCreation(createCreationActionParams); expect(action.parameters.paymentAddress).toEqual('payment.testnet'); expect(action.parameters.paymentNetworkName).toEqual('aurora-testnet'); }); it('throws without a payment network name', async () => { - const paymentNetwork = PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic, - currency: { ...currency, network: 'aurora-testnet' }, - paymentNetworkCreationParameters: { - id: PaymentTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, - parameters: { ...createCreationActionParams, paymentNetworkName: undefined }, - }, - currencyManager: CurrencyManager.getDefault(), - }); + const paymentNetwork = paymentNetworkFactory.createPaymentNetwork( + PaymentTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, + RequestLogicTypes.CURRENCY.ETH, + 'aurora-testnet', + ); await expect(async () => { - await paymentNetwork.createExtensionsDataForCreation(createCreationActionParams); + await paymentNetwork.createExtensionsDataForCreation( + omit(createCreationActionParams, 'paymentNetworkName'), + ); }).rejects.toThrowError( 'The network name is mandatory for the creation of the extension pn-native-token.', ); diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 34a075b852..64d8931d1f 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -1,4 +1,4 @@ -import PaymentNetworkFactory from './payment-network-factory'; +import { PaymentNetworkFactory, PaymentNetworkOptions } from './payment-network-factory'; import PaymentReferenceCalculator from './payment-reference-calculator'; import * as BtcPaymentNetwork from './btc'; @@ -26,6 +26,7 @@ export type { TheGraphClient } from './thegraph'; export { PaymentNetworkFactory, + PaymentNetworkOptions, PaymentReferenceCalculator, BtcPaymentNetwork, DeclarativePaymentDetector, diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 0c9fa2728e..f75473c0ff 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -1,9 +1,4 @@ -import { - AdvancedLogicTypes, - ExtensionTypes, - PaymentTypes, - RequestLogicTypes, -} from '@requestnetwork/types'; +import { AdvancedLogicTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; import { ICurrencyManager } from '@requestnetwork/currency'; import { IPaymentNetworkModuleByType, ISupportedPaymentNetworkByCurrency } from './types'; import { BtcMainnetAddressBasedDetector } from './btc/mainnet-address-based'; @@ -18,6 +13,7 @@ import { EthFeeProxyPaymentDetector } from './eth/fee-proxy-detector'; import { AnyToERC20PaymentDetector } from './any/any-to-erc20-proxy'; import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector } from './near'; import { AnyToEthFeeProxyPaymentDetector } from './any/any-to-eth-proxy'; +import { getPaymentNetworkExtension } from './utils'; const PN_ID = PaymentTypes.PAYMENT_NETWORK_ID; @@ -62,48 +58,60 @@ const anyCurrencyPaymentNetwork: IPaymentNetworkModuleByType = { [PN_ID.ANY_TO_NATIVE]: NearConversionNativeTokenPaymentDetector, }; +export type PaymentNetworkOptions = { + /** override default bitcoin detection provider */ + bitcoinDetectionProvider?: PaymentTypes.IBitcoinDetectionProvider; + /** the explorer API (eg Etherscan) api keys, for PNs that rely on it. Record by network name */ + explorerApiKeys?: Record; +}; + /** Factory to create the payment network according to the currency and payment network type */ -export default class PaymentNetworkFactory { +export class PaymentNetworkFactory { + /** + * + * @param advancedLogic the advanced-logic layer in charge of the extensions + * @param currencyManager the currency manager handling supported currencies + * @param options the payment network options + */ + constructor( + private readonly advancedLogic: AdvancedLogicTypes.IAdvancedLogic, + private readonly currencyManager: ICurrencyManager, + private readonly options?: PaymentNetworkOptions, + ) {} + /** * Creates a payment network according to payment network creation parameters * It throws if the payment network given is not supported by this library * - * @param advancedLogic the advanced-logic layer in charge of the extensions * @param currency the currency of the request * @param paymentNetworkCreationParameters creation parameters of payment network - * @param bitcoinDetectionProvider bitcoin detection provider - * @param currencyManager the currency manager handling supported currencies * @returns the module to handle the payment network */ - public static createPaymentNetwork({ - advancedLogic, - currency, - paymentNetworkCreationParameters, - bitcoinDetectionProvider, - currencyManager, - }: { - advancedLogic: AdvancedLogicTypes.IAdvancedLogic; - currency: RequestLogicTypes.ICurrency; - paymentNetworkCreationParameters: PaymentTypes.IPaymentNetworkCreateParameters; - bitcoinDetectionProvider?: PaymentTypes.IBitcoinDetectionProvider; - currencyManager: ICurrencyManager; - }): PaymentTypes.IPaymentNetwork { - const paymentNetworkForCurrency = this.supportedPaymentNetworksForCurrency(currency); + public createPaymentNetwork( + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID, + currencyType: RequestLogicTypes.CURRENCY, + currencyNetwork?: string, + ): PaymentTypes.IPaymentNetwork { + const currencyPaymentMap = + supportedPaymentNetwork[currencyType]?.[currencyNetwork || 'mainnet'] || + supportedPaymentNetwork[currencyType]?.['*'] || + {}; + const paymentNetworkMap = { + ...currencyPaymentMap, + ...anyCurrencyPaymentNetwork, + }; - if (!paymentNetworkForCurrency[paymentNetworkCreationParameters.id]) { + if (!paymentNetworkMap[paymentNetworkId]) { throw new Error( - `the payment network id: ${ - paymentNetworkCreationParameters.id - } is not supported for the currency: ${currency.type} on network ${ - currency.network || 'mainnet' + `the payment network id: ${paymentNetworkId} is not supported for the currency: ${currencyType} on network ${ + currencyNetwork || 'mainnet' }`, ); } - - return new paymentNetworkForCurrency[paymentNetworkCreationParameters.id]({ - advancedLogic, - bitcoinDetectionProvider, - currencyManager, + return new paymentNetworkMap[paymentNetworkId]({ + advancedLogic: this.advancedLogic, + currencyManager: this.currencyManager, + ...this.options, }); } @@ -111,84 +119,23 @@ export default class PaymentNetworkFactory { * Gets the module to the payment network of a request * It throws if the payment network found is not supported by this library * - * @param advancedLogic the advanced-logic layer in charge of the extensions * @param request the request - * @param bitcoinDetectionProvider bitcoin detection provider - * @param explorerApiKeys the explorer API (eg Etherscan) api keys, for PNs that rely on it. Record by network name. - * @param currencyManager the currency manager handling supported currencies * @returns the module to handle the payment network or null if no payment network found */ - public static getPaymentNetworkFromRequest({ - advancedLogic, - request, - bitcoinDetectionProvider, - explorerApiKeys, - currencyManager, - }: { - advancedLogic: AdvancedLogicTypes.IAdvancedLogic; - request: RequestLogicTypes.IRequest; - currencyManager: ICurrencyManager; - bitcoinDetectionProvider?: PaymentTypes.IBitcoinDetectionProvider; - explorerApiKeys?: Record; - }): PaymentTypes.IPaymentNetwork | null { - const currency = request.currency; - const extensionPaymentNetwork = Object.values(request.extensions || {}).find( - (extension) => extension.type === ExtensionTypes.TYPE.PAYMENT_NETWORK, - ); + public getPaymentNetworkFromRequest( + request: RequestLogicTypes.IRequest, + ): PaymentTypes.IPaymentNetwork | null { + const pn = getPaymentNetworkExtension(request); - if (!extensionPaymentNetwork) { + if (!pn) { return null; } - const paymentNetworkId = extensionPaymentNetwork.id; - const paymentNetworkForCurrency = this.supportedPaymentNetworksForCurrency(currency); - - if (!paymentNetworkForCurrency[paymentNetworkId]) { - throw new Error( - `the payment network id: ${paymentNetworkId} is not supported for the currency: ${ - currency.type - } on network ${currency.network || 'mainnet'}`, - ); - } - - return new paymentNetworkForCurrency[paymentNetworkId]({ - advancedLogic, - bitcoinDetectionProvider, - explorerApiKeys, - currencyManager, - }); - } - - /** - * Gets the payment networks supported for a Currency object - * - * @param currency The currency to get the supported networks for - */ - public static supportedPaymentNetworksForCurrency( - currency: RequestLogicTypes.ICurrency, - ): IPaymentNetworkModuleByType { - if (!supportedPaymentNetwork[currency.type]) { - return anyCurrencyPaymentNetwork; - } - - const paymentNetwork = - supportedPaymentNetwork[currency.type][currency.network || 'mainnet'] || - supportedPaymentNetwork[currency.type]['*']; - - return { ...paymentNetwork, ...anyCurrencyPaymentNetwork }; - } - - /** - * Checks if a networkId is part of the supported networks for given currency - * - * @param paymentNetworkId The networkId to check is supported by this currency - * @param currency The currency to check the supported networks for - */ - public static currencySupportsPaymentNetwork( - paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID, - currency: RequestLogicTypes.ICurrency, - ): boolean { - const paymentNetworks = this.supportedPaymentNetworksForCurrency(currency); - return !!paymentNetworks[paymentNetworkId]; + const paymentNetworkId = pn.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID; + return this.createPaymentNetwork( + paymentNetworkId, + request.currency.type, + request.currency.network, + ); } } diff --git a/packages/payment-detection/test/near/near-native-conversion.test.ts b/packages/payment-detection/test/near/near-native-conversion.test.ts index 4d5725e68e..c807454d45 100644 --- a/packages/payment-detection/test/near/near-native-conversion.test.ts +++ b/packages/payment-detection/test/near/near-native-conversion.test.ts @@ -5,7 +5,7 @@ import { RequestLogicTypes, } from '@requestnetwork/types'; import { CurrencyDefinition, CurrencyManager } from '@requestnetwork/currency'; -import PaymentNetworkFactory from '../../src/payment-network-factory'; +import { PaymentNetworkFactory } from '../../src/payment-network-factory'; import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; import { NearConversionNativeTokenPaymentDetector, @@ -75,6 +75,7 @@ const graphPaymentEvent = { gasPrice: '2425000017', }; +const paymentNetworkFactory = new PaymentNetworkFactory(mockAdvancedLogic, currencyManager); describe('Near payments detection', () => { beforeAll(() => { graphql.request.mockResolvedValue({ @@ -121,21 +122,16 @@ describe('Near payments detection', () => { }); it('PaymentNetworkFactory can get the detector (testnet)', async () => { - expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }), - ).toBeInstanceOf(NearConversionNativeTokenPaymentDetector); + expect(paymentNetworkFactory.getPaymentNetworkFromRequest(request)).toBeInstanceOf( + NearConversionNativeTokenPaymentDetector, + ); }); it('PaymentNetworkFactory can get the detector (mainnet)', async () => { expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request: { ...request, currency: { ...request.currency, network: 'aurora' } }, - currencyManager, + paymentNetworkFactory.getPaymentNetworkFromRequest({ + ...request, + currency: { ...request.currency, network: 'aurora' }, }), ).toBeInstanceOf(NearConversionNativeTokenPaymentDetector); }); diff --git a/packages/payment-detection/test/near/near-native.test.ts b/packages/payment-detection/test/near/near-native.test.ts index 8bd3e56e4a..251398123f 100644 --- a/packages/payment-detection/test/near/near-native.test.ts +++ b/packages/payment-detection/test/near/near-native.test.ts @@ -5,7 +5,7 @@ import { RequestLogicTypes, } from '@requestnetwork/types'; import { CurrencyManager } from '@requestnetwork/currency'; -import PaymentNetworkFactory from '../../src/payment-network-factory'; +import { PaymentNetworkFactory } from '../../src/payment-network-factory'; import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; import { NearNativeTokenPaymentDetector, NearInfoRetriever } from '../../src/near'; import { deepCopy } from 'ethers/lib/utils'; @@ -43,6 +43,8 @@ const request: any = { }, }; +const paymentNetworkFactory = new PaymentNetworkFactory(mockAdvancedLogic, currencyManager); + describe('Near payments detection', () => { it('NearInfoRetriever can retrieve a NEAR payment', async () => { const paymentReference = PaymentReferenceCalculator.calculate( @@ -69,21 +71,16 @@ describe('Near payments detection', () => { }); it('PaymentNetworkFactory can get the detector (testnet)', async () => { - expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }), - ).toBeInstanceOf(NearNativeTokenPaymentDetector); + expect(paymentNetworkFactory.getPaymentNetworkFromRequest(request)).toBeInstanceOf( + NearNativeTokenPaymentDetector, + ); }); it('PaymentNetworkFactory can get the detector (mainnet)', async () => { expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request: { ...request, currency: { ...request.currency, network: 'aurora' } }, - currencyManager, + paymentNetworkFactory.getPaymentNetworkFromRequest({ + ...request, + currency: { ...request.currency, network: 'aurora' }, }), ).toBeInstanceOf(NearNativeTokenPaymentDetector); }); diff --git a/packages/payment-detection/test/payment-network-factory.test.ts b/packages/payment-detection/test/payment-network-factory.test.ts index dc28c71e8b..900c5f9e1f 100644 --- a/packages/payment-detection/test/payment-network-factory.test.ts +++ b/packages/payment-detection/test/payment-network-factory.test.ts @@ -9,7 +9,7 @@ import { BtcMainnetAddressBasedDetector } from '../src/btc/mainnet-address-based import { DeclarativePaymentDetector } from '../src/declarative'; import { EthInputDataPaymentDetector } from '../src/eth/input-data'; -import PaymentNetworkFactory from '../src/payment-network-factory'; +import { PaymentNetworkFactory } from '../src/payment-network-factory'; const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { applyActionToExtensions(): any { @@ -19,74 +19,38 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { }; const currencyManager = CurrencyManager.getDefault(); - +const paymentNetworkFactory = new PaymentNetworkFactory( + mockAdvancedLogic, + CurrencyManager.getDefault(), +); // Most of the tests are done as integration tests in ../index.test.ts /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('api/payment-network/payment-network-factory', () => { describe('createPaymentNetwork', () => { it('can createPaymentNetwork', async () => { - const paymentNetworkCreationParameters: PaymentTypes.IPaymentNetworkCreateParameters = { - id: PaymentTypes.PAYMENT_NETWORK_ID.BITCOIN_ADDRESS_BASED, - parameters: { - paymentAddress: 'bitcoin address here', - }, - }; - // 'createPayment createPaymentNetwork' expect( - PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic: mockAdvancedLogic, - currency: { - network: 'mainnet', - type: RequestLogicTypes.CURRENCY.BTC, - value: 'BTC', - }, - paymentNetworkCreationParameters, - currencyManager, - }), + paymentNetworkFactory.createPaymentNetwork( + PaymentTypes.PAYMENT_NETWORK_ID.BITCOIN_ADDRESS_BASED, + RequestLogicTypes.CURRENCY.BTC, + ), ).toBeInstanceOf(BtcMainnetAddressBasedDetector); }); it('can createPaymentNetwork with any currency', async () => { - const paymentNetworkCreationParameters: PaymentTypes.IPaymentNetworkCreateParameters = { - id: PaymentTypes.PAYMENT_NETWORK_ID.DECLARATIVE, - parameters: { - paymentAddress: 'bitcoin address here', - }, - }; - // 'createPayment createPaymentNetwork' expect( - PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic: mockAdvancedLogic, - currency: { - network: 'mainnet', - type: RequestLogicTypes.CURRENCY.BTC, - value: 'BTC', - }, - paymentNetworkCreationParameters, - currencyManager, - }), + paymentNetworkFactory.createPaymentNetwork( + PaymentTypes.PAYMENT_NETWORK_ID.DECLARATIVE, + RequestLogicTypes.CURRENCY.BTC, + ), ).toBeInstanceOf(DeclarativePaymentDetector); }); it('cannot createPaymentNetwork with extension id not handled', async () => { - const paymentNetworkCreationParameters: any = { - id: 'ETHEREUM_MAGIC', - parameters: { - paymentAddress: 'bitcoin address here', - }, - }; - // 'should throw wrong' expect(() => { - PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic: mockAdvancedLogic, - currency: { - network: 'mainnet', - type: RequestLogicTypes.CURRENCY.BTC, - value: 'BTC', - }, - paymentNetworkCreationParameters, - currencyManager, - }); + paymentNetworkFactory.createPaymentNetwork( + 'ETHEREUM_MAGIC' as any, + RequestLogicTypes.CURRENCY.BTC, + ); }).toThrowError( 'the payment network id: ETHEREUM_MAGIC is not supported for the currency: BTC', ); @@ -110,13 +74,9 @@ describe('api/payment-network/payment-network-factory', () => { }; // 'createPayment createPaymentNetwork' - expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }), - ).toBeInstanceOf(BtcMainnetAddressBasedDetector); + expect(paymentNetworkFactory.getPaymentNetworkFromRequest(request)).toBeInstanceOf( + BtcMainnetAddressBasedDetector, + ); }); it('can getPaymentNetworkFromRequest with a request without payment network', async () => { const request: any = { @@ -134,13 +94,7 @@ describe('api/payment-network/payment-network-factory', () => { }; // 'createPayment createPaymentNetwork' - expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }), - ).toBeNull(); + expect(paymentNetworkFactory.getPaymentNetworkFromRequest(request)).toBeNull(); }); it('cannot getPaymentNetworkFromRequest with extension id not handled', async () => { @@ -159,11 +113,7 @@ describe('api/payment-network/payment-network-factory', () => { }; // 'should throw wrong' expect(() => { - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }); + paymentNetworkFactory.getPaymentNetworkFromRequest(request); }).toThrowError( 'the payment network id: content-data is not supported for the currency: BTC', ); @@ -181,13 +131,9 @@ describe('api/payment-network/payment-network-factory', () => { }; // 'createPayment getPaymentNetworkFromRequest' - expect( - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - }), - ).toBeInstanceOf(DeclarativePaymentDetector); + expect(paymentNetworkFactory.getPaymentNetworkFromRequest(request)).toBeInstanceOf( + DeclarativePaymentDetector, + ); }); it('can pass options down to the paymentNetwork', async () => { @@ -204,14 +150,10 @@ describe('api/payment-network/payment-network-factory', () => { }, }, }; - const pn = PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: mockAdvancedLogic, - request, - currencyManager, - explorerApiKeys: { - homestead: 'abcd', - }, + const paymentNetworkFactory = new PaymentNetworkFactory(mockAdvancedLogic, currencyManager, { + explorerApiKeys: { homestead: 'abcd' }, }); + const pn = paymentNetworkFactory.getPaymentNetworkFromRequest(request); expect(pn).toBeInstanceOf(EthInputDataPaymentDetector); expect((pn as any).explorerApiKeys).toMatchObject({ homestead: 'abcd', diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index c1695f11cb..3d736a55cf 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -1,6 +1,6 @@ import { utils as ethersUtils } from 'ethers'; import { AdvancedLogic } from '@requestnetwork/advanced-logic'; -import { PaymentNetworkFactory } from '@requestnetwork/payment-detection'; +import { PaymentNetworkFactory, PaymentNetworkOptions } from '@requestnetwork/payment-detection'; import { RequestLogic } from '@requestnetwork/request-logic'; import { TransactionManager } from '@requestnetwork/transaction-manager'; import { @@ -16,7 +16,6 @@ import { } from '@requestnetwork/types'; import Utils from '@requestnetwork/utils'; import { - CurrencyInput, CurrencyManager, ICurrencyManager, UnsupportedCurrencyError, @@ -30,7 +29,7 @@ import localUtils from './utils'; * Entry point of the request-client.js library. Create requests, get requests, manipulate requests. */ export default class RequestNetwork { - public bitcoinDetectionProvider?: PaymentTypes.IBitcoinDetectionProvider; + public paymentNetworkFactory: PaymentNetworkFactory; public supportedIdentities: IdentityTypes.TYPE[] = Utils.identity.supportedIdentities; private requestLogic: RequestLogicTypes.IRequestLogic; @@ -44,29 +43,31 @@ export default class RequestNetwork { * @param dataAccess instance of data-access layer * @param signatureProvider module in charge of the signatures * @param decryptionProvider module in charge of the decryption - * @param bitcoinDetectionProvider bitcoin detection provider * @param currencyManager */ public constructor({ dataAccess, signatureProvider, decryptionProvider, - bitcoinDetectionProvider, currencyManager, + paymentOptions, }: { dataAccess: DataAccessTypes.IDataAccess; signatureProvider?: SignatureProviderTypes.ISignatureProvider; decryptionProvider?: DecryptionProviderTypes.IDecryptionProvider; - bitcoinDetectionProvider?: PaymentTypes.IBitcoinDetectionProvider; - currencies?: CurrencyInput[]; currencyManager?: ICurrencyManager; + paymentOptions?: PaymentNetworkOptions; }) { this.currencyManager = currencyManager || CurrencyManager.getDefault(); this.advancedLogic = new AdvancedLogic(this.currencyManager); this.transaction = new TransactionManager(dataAccess, decryptionProvider); this.requestLogic = new RequestLogic(this.transaction, signatureProvider, this.advancedLogic); this.contentData = new ContentDataExtension(this.advancedLogic); - this.bitcoinDetectionProvider = bitcoinDetectionProvider; + this.paymentNetworkFactory = new PaymentNetworkFactory( + this.advancedLogic, + this.currencyManager, + paymentOptions, + ); } /** @@ -173,7 +174,6 @@ export default class RequestNetwork { options?: { disablePaymentDetection?: boolean; disableEvents?: boolean; - explorerApiKeys?: Record; }, ): Promise { const requestAndMeta: RequestLogicTypes.IReturnGetRequestFromId = @@ -188,14 +188,7 @@ export default class RequestNetwork { const requestState: RequestLogicTypes.IRequest = requestAndMeta.result.request ? requestAndMeta.result.request : (requestAndMeta.result.pending as RequestLogicTypes.IRequest); - const paymentNetwork: PaymentTypes.IPaymentNetwork | null = - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: this.advancedLogic, - bitcoinDetectionProvider: this.bitcoinDetectionProvider, - request: requestState, - explorerApiKeys: options?.explorerApiKeys, - currencyManager: this.currencyManager, - }); + const paymentNetwork = this.paymentNetworkFactory.getPaymentNetworkFromRequest(requestState); // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { @@ -281,13 +274,8 @@ export default class RequestNetwork { ? requestFromLogic.request : (requestFromLogic.pending as RequestLogicTypes.IRequest); - const paymentNetwork: PaymentTypes.IPaymentNetwork | null = - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: this.advancedLogic, - bitcoinDetectionProvider: this.bitcoinDetectionProvider, - request: requestState, - currencyManager: this.currencyManager, - }); + const paymentNetwork = + this.paymentNetworkFactory.getPaymentNetworkFromRequest(requestState); // create the request object const request = new Request( @@ -340,13 +328,8 @@ export default class RequestNetwork { ? requestFromLogic.request : (requestFromLogic.pending as RequestLogicTypes.IRequest); - const paymentNetwork: PaymentTypes.IPaymentNetwork | null = - PaymentNetworkFactory.getPaymentNetworkFromRequest({ - advancedLogic: this.advancedLogic, - bitcoinDetectionProvider: this.bitcoinDetectionProvider, - request: requestState, - currencyManager: this.currencyManager, - }); + const paymentNetwork = + this.paymentNetworkFactory.getPaymentNetworkFromRequest(requestState); // create the request object const request = new Request( @@ -401,7 +384,6 @@ export default class RequestNetwork { ...parameters.requestInfo, currency, }; - const paymentNetworkCreationParameters = parameters.paymentNetwork; const contentData = parameters.contentData; const topics = parameters.topics?.slice() || []; @@ -416,24 +398,19 @@ export default class RequestNetwork { const copiedRequestParameters = Utils.deepCopy(requestParameters); copiedRequestParameters.extensionsData = []; - let paymentNetwork: PaymentTypes.IPaymentNetwork | null = null; - if (paymentNetworkCreationParameters) { - paymentNetwork = PaymentNetworkFactory.createPaymentNetwork({ - advancedLogic: this.advancedLogic, - bitcoinDetectionProvider: this.bitcoinDetectionProvider, - currency: requestParameters.currency, - paymentNetworkCreationParameters, - currencyManager: this.currencyManager, - }); - - if (paymentNetwork) { - // create the extensions data for the payment network - copiedRequestParameters.extensionsData.push( - await paymentNetwork.createExtensionsDataForCreation( - paymentNetworkCreationParameters.parameters, - ), - ); - } + const paymentNetwork = parameters.paymentNetwork + ? this.paymentNetworkFactory.createPaymentNetwork( + parameters.paymentNetwork.id, + requestParameters.currency.type, + requestParameters.currency.network, + ) + : null; + + if (paymentNetwork) { + // create the extensions data for the payment network + copiedRequestParameters.extensionsData.push( + await paymentNetwork.createExtensionsDataForCreation(parameters.paymentNetwork?.parameters), + ); } if (contentData) { diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index bbac8ab27c..d2d64fae46 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -6,9 +6,9 @@ import { SignatureProviderTypes, } from '@requestnetwork/types'; import { AxiosRequestConfig } from 'axios'; +import { PaymentNetworkOptions } from '@requestnetwork/payment-detection'; import RequestNetwork from './api/request-network'; import HttpDataAccess from './http-data-access'; -import HttpMetaMaskDataAccess from './http-metamask-data-access'; import MockDataAccess from './mock-data-access'; import MockStorage from './mock-storage'; @@ -16,9 +16,6 @@ import MockStorage from './mock-storage'; * Exposes RequestNetwork module configured to use http-data-access. */ export default class HttpRequestNetwork extends RequestNetwork { - /** Public for test purpose */ - public _mockStorage: MockStorage | undefined; - /** * Creates an instance of HttpRequestNetwork. * @@ -27,7 +24,6 @@ export default class HttpRequestNetwork extends RequestNetwork { * @param options.useMockStorage When true, will use a mock storage in memory. Meant to simplify local development and should never be used in production. * @param options.signatureProvider Module to handle the signature. If not given it will be impossible to create new transaction (it requires to sign). * @param options.useLocalEthereumBroadcast When true, persisting use the node only for IPFS but persisting on ethereum through local provider (given in ethereumProviderUrl). - * @param options.ethereumProviderUrl Url of the Ethereum provider use to persist transactions if useLocalEthereumBroadcast is true. * @param options.currencies custom currency list * @param options.currencyManager custom currency manager (will override `currencies`) */ @@ -36,13 +32,11 @@ export default class HttpRequestNetwork extends RequestNetwork { decryptionProvider, httpConfig, nodeConnectionConfig, - useLocalEthereumBroadcast, signatureProvider, useMockStorage, - web3, - ethereumProviderUrl, currencies, currencyManager, + paymentOptions, }: { decryptionProvider?: DecryptionProviderTypes.IDecryptionProvider; httpConfig?: Partial; @@ -50,10 +44,9 @@ export default class HttpRequestNetwork extends RequestNetwork { signatureProvider?: SignatureProviderTypes.ISignatureProvider; useMockStorage?: boolean; useLocalEthereumBroadcast?: boolean; - web3?: any; - ethereumProviderUrl?: string; currencies?: CurrencyInput[]; currencyManager?: ICurrencyManager; + paymentOptions?: PaymentNetworkOptions; } = { httpConfig: {}, nodeConnectionConfig: {}, @@ -61,27 +54,14 @@ export default class HttpRequestNetwork extends RequestNetwork { useMockStorage: false, }, ) { - let _mockStorage: MockStorage | undefined; - if (useMockStorage) { - _mockStorage = new MockStorage(); - } const dataAccess: DataAccessTypes.IDataAccess = useMockStorage - ? // useMockStorage === true => use mock data-access - new MockDataAccess(_mockStorage!) - : // useMockStorage === false - useLocalEthereumBroadcast - ? // useLocalEthereumBroadcast === true => use http-metamask-data-access - new HttpMetaMaskDataAccess({ httpConfig, nodeConnectionConfig, web3, ethereumProviderUrl }) - : // useLocalEthereumBroadcast === false => use http-data-access - new HttpDataAccess({ httpConfig, nodeConnectionConfig }); + ? new MockDataAccess(new MockStorage()) + : new HttpDataAccess({ httpConfig, nodeConnectionConfig }); if (!currencyManager) { currencyManager = new CurrencyManager(currencies || CurrencyManager.getDefaultList()); } - super({ dataAccess, signatureProvider, decryptionProvider, currencyManager }); - - // store it for test purpose - this._mockStorage = _mockStorage; + super({ dataAccess, signatureProvider, decryptionProvider, currencyManager, paymentOptions }); } } diff --git a/packages/request-client.js/src/index.ts b/packages/request-client.js/src/index.ts index 07e0cfa4d8..14f0ad9487 100644 --- a/packages/request-client.js/src/index.ts +++ b/packages/request-client.js/src/index.ts @@ -3,6 +3,7 @@ import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; import Request from './api/request'; import Utils from './api/utils'; import { default as RequestNetwork } from './http-request-network'; +import { default as RequestNetworkBase } from './api/request-network'; import * as Types from './types'; -export { PaymentReferenceCalculator, Request, RequestNetwork, Types, Utils }; +export { PaymentReferenceCalculator, Request, RequestNetwork, RequestNetworkBase, Types, Utils }; diff --git a/packages/request-client.js/test/index.test.ts b/packages/request-client.js/test/index.test.ts index bb7521c145..9bf8bb86a6 100644 --- a/packages/request-client.js/test/index.test.ts +++ b/packages/request-client.js/test/index.test.ts @@ -13,7 +13,7 @@ import Utils from '@requestnetwork/utils'; import { ethers } from 'ethers'; import AxiosMockAdapter from 'axios-mock-adapter'; -import { Request, RequestNetwork } from '../src/index'; +import { Request, RequestNetwork, RequestNetworkBase } from '../src/index'; import * as TestData from './data-test'; import * as TestDataRealBTC from './data-test-real-btc'; @@ -27,6 +27,9 @@ import EtherscanProviderMock from './etherscan-mock'; import httpConfigDefaults from '../src/http-config-defaults'; import { IRequestDataWithEvents } from '../src/types'; import { CurrencyManager } from '@requestnetwork/currency'; +import HttpMetaMaskDataAccess from '../src/http-metamask-data-access'; +import MockDataAccess from '../src/mock-data-access'; +import MockStorage from '../src/mock-storage'; const packageJson = require('../package.json'); @@ -137,10 +140,11 @@ describe('index', () => { const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, + paymentOptions: { + bitcoinDetectionProvider: mockBTCProvider, + }, }); - requestNetwork.bitcoinDetectionProvider = mockBTCProvider; - const paymentNetwork: PaymentTypes.IPaymentNetworkCreateParameters = { id: PaymentTypes.PAYMENT_NETWORK_ID.DECLARATIVE, parameters: {}, @@ -172,10 +176,11 @@ describe('index', () => { const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, + paymentOptions: { + bitcoinDetectionProvider: mockBTCProvider, + }, }); - requestNetwork.bitcoinDetectionProvider = mockBTCProvider; - const paymentNetwork: PaymentTypes.IPaymentNetworkCreateParameters = { id: PaymentTypes.PAYMENT_NETWORK_ID.DECLARATIVE, parameters: {}, @@ -205,15 +210,17 @@ describe('index', () => { result: { transactions: [] }, }); - const requestNetwork = new RequestNetwork({ - httpConfig, - ethereumProviderUrl: 'http://localhost:8545', + const requestNetwork = new RequestNetworkBase({ + dataAccess: new HttpMetaMaskDataAccess({ + httpConfig, + ethereumProviderUrl: 'http://localhost:8545', + }), signatureProvider: TestData.fakeSignatureProvider, - useLocalEthereumBroadcast: true, + paymentOptions: { + bitcoinDetectionProvider: mockBTCProvider, + }, }); - requestNetwork.bitcoinDetectionProvider = mockBTCProvider; - const paymentNetwork: PaymentTypes.IPaymentNetworkCreateParameters = { id: PaymentTypes.PAYMENT_NETWORK_ID.TESTNET_BITCOIN_ADDRESS_BASED, parameters: { @@ -248,10 +255,11 @@ describe('index', () => { const requestNetwork = new RequestNetwork({ httpConfig, signatureProvider: TestData.fakeSignatureProvider, + paymentOptions: { + bitcoinDetectionProvider: mockBTCProvider, + }, }); - requestNetwork.bitcoinDetectionProvider = mockBTCProvider; - const paymentNetwork: PaymentTypes.IPaymentNetworkCreateParameters = { id: PaymentTypes.PAYMENT_NETWORK_ID.BITCOIN_ADDRESS_BASED, parameters: { @@ -480,9 +488,11 @@ describe('index', () => { }); it('works with mocked storage', async () => { - const requestNetwork = new RequestNetwork({ + const mockStorage = new MockStorage(); + const mockDataAccess = new MockDataAccess(mockStorage); + const requestNetwork = new RequestNetworkBase({ + dataAccess: mockDataAccess, signatureProvider: TestData.fakeSignatureProvider, - useMockStorage: true, }); const request = await requestNetwork.createRequest({ requestInfo: TestData.parametersWithoutExtensionsData, @@ -503,13 +513,15 @@ describe('index', () => { }); it('works with mocked storage emitting error when append', async () => { - const requestNetwork = new RequestNetwork({ + const mockStorage = new MockStorage(); + const mockDataAccess = new MockDataAccess(mockStorage); + const requestNetwork = new RequestNetworkBase({ signatureProvider: TestData.fakeSignatureProvider, - useMockStorage: true, + dataAccess: mockDataAccess, }); // ask mock up storage to emit error next append call() - requestNetwork._mockStorage!._makeNextAppendFailInsteadOfConfirmed(); + mockStorage._makeNextAppendFailInsteadOfConfirmed(); const request = await requestNetwork.createRequest({ requestInfo: TestData.parametersWithoutExtensionsData, @@ -532,13 +544,15 @@ describe('index', () => { }); it('works with mocked storage emitting error when append waitForConfirmation will throw', async () => { - const requestNetworkInside = new RequestNetwork({ + const mockStorage = new MockStorage(); + const mockDataAccess = new MockDataAccess(mockStorage); + const requestNetworkInside = new RequestNetworkBase({ signatureProvider: TestData.fakeSignatureProvider, - useMockStorage: true, + dataAccess: mockDataAccess, }); // ask mock up storage to emit error next append call() - requestNetworkInside._mockStorage!._makeNextAppendFailInsteadOfConfirmed(); + mockStorage._makeNextAppendFailInsteadOfConfirmed(); const request = await requestNetworkInside.createRequest({ requestInfo: TestData.parametersWithoutExtensionsData, @@ -589,10 +603,11 @@ describe('index', () => { const requestNetwork = new RequestNetwork({ signatureProvider: TestData.fakeSignatureProvider, useMockStorage: true, + paymentOptions: { + bitcoinDetectionProvider: mockBTCProvider, + }, }); - requestNetwork.bitcoinDetectionProvider = mockBTCProvider; - const paymentNetwork: PaymentTypes.IPaymentNetworkCreateParameters = { id: PaymentTypes.PAYMENT_NETWORK_ID.TESTNET_BITCOIN_ADDRESS_BASED, parameters: { @@ -668,9 +683,11 @@ describe('index', () => { }); it('works with mocked storage emitting error when append an accept', async () => { - const requestNetwork = new RequestNetwork({ + const mockStorage = new MockStorage(); + const mockDataAccess = new MockDataAccess(mockStorage); + const requestNetwork = new RequestNetworkBase({ signatureProvider: TestData.fakeSignatureProvider, - useMockStorage: true, + dataAccess: mockDataAccess, }); const request = await requestNetwork.createRequest({ @@ -680,7 +697,7 @@ describe('index', () => { await request.waitForConfirmation(); // ask mock up storage to emit error next append call() - requestNetwork._mockStorage!._makeNextAppendFailInsteadOfConfirmed(); + mockStorage._makeNextAppendFailInsteadOfConfirmed(); await request.accept(TestData.payer.identity); let data = request.getData(); From cd4f55d9a759fc6b4837ab7f0ba6a037efecfb37 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 6 Oct 2022 19:34:16 +0200 Subject: [PATCH 023/207] refactor(data-access): thegraph-data-access package (#931) --- .circleci/config.yml | 4 ++ README.md | 39 +++++------ package.json | 2 +- .../src/combined-data-access.ts} | 0 packages/data-access/src/index.ts | 1 + packages/ethereum-storage/package.json | 2 + .../src/ethereum-storage-ethers.ts} | 20 ++---- packages/ethereum-storage/src/index.ts | 1 + packages/request-node/package.json | 3 +- packages/request-node/src/server.ts | 2 +- ...heGraphRequestNode.ts => thegraph-node.ts} | 22 +++++-- packages/request-node/src/thegraph/index.ts | 12 ---- .../test/thegraph/persistTransaction.test.ts | 2 +- packages/request-node/tsconfig.build.json | 1 + packages/thegraph-data-access/.nycrc | 19 ++++++ packages/thegraph-data-access/jest.config.js | 5 ++ packages/thegraph-data-access/package.json | 63 ++++++++++++++++++ .../src/data-access.ts} | 66 ++++++++++++------- packages/thegraph-data-access/src/index.ts | 9 +++ .../src/pending-store.ts} | 0 .../src}/queries.ts | 0 .../src/subgraph-client.ts} | 0 .../thegraph-data-access/tsconfig.build.json | 10 +++ packages/thegraph-data-access/tsconfig.json | 3 + packages/types/src/storage-types.ts | 11 +++- 25 files changed, 214 insertions(+), 83 deletions(-) rename packages/{request-node/src/thegraph/CombinedDataAccess.ts => data-access/src/combined-data-access.ts} (100%) rename packages/{request-node/src/thegraph/TheGraphStorage.ts => ethereum-storage/src/ethereum-storage-ethers.ts} (84%) rename packages/request-node/src/{thegraph/TheGraphRequestNode.ts => thegraph-node.ts} (69%) delete mode 100644 packages/request-node/src/thegraph/index.ts create mode 100644 packages/thegraph-data-access/.nycrc create mode 100644 packages/thegraph-data-access/jest.config.js create mode 100644 packages/thegraph-data-access/package.json rename packages/{request-node/src/thegraph/TheGraphDataAccess.ts => thegraph-data-access/src/data-access.ts} (87%) create mode 100644 packages/thegraph-data-access/src/index.ts rename packages/{request-node/src/thegraph/PendingStore.ts => thegraph-data-access/src/pending-store.ts} (100%) rename packages/{request-node/src/thegraph => thegraph-data-access/src}/queries.ts (100%) rename packages/{request-node/src/thegraph/subgraphClient.ts => thegraph-data-access/src/subgraph-client.ts} (100%) create mode 100644 packages/thegraph-data-access/tsconfig.build.json create mode 100644 packages/thegraph-data-access/tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index eaeaeedf5e..f2ada1c70a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -95,6 +95,10 @@ jobs: name: Generate Payment Detection queries command: yarn workspace @requestnetwork/payment-detection run codegen + - run: + name: Build all packages (tsc) + command: yarn build:tsc + - persist_to_workspace: root: *working_directory paths: . diff --git a/README.md b/README.md index b465c40015..f0fd17a915 100644 --- a/README.md +++ b/README.md @@ -18,25 +18,26 @@ Join the [Request Hub][requesthub-slack-url] to get in touch with us. ### Published Packages -| Package | Version | Description | -| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| [`@requestnetwork/advanced-logic`](/packages/advanced-logic) | [![npm](https://img.shields.io/npm/v/@requestnetwork/advanced-logic.svg)](https://www.npmjs.com/package/@requestnetwork/advanced-logic) | Extensions to the protocol | -| [`@requestnetwork/request-client.js`](/packages/request-client.js) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-client.js.svg)](https://www.npmjs.com/package/@requestnetwork/request-client.js) | Library to use Request nodes as servers | -| [`@requestnetwork/data-access`](/packages/data-access) | [![npm](https://img.shields.io/npm/v/@requestnetwork/data-access.svg)](https://www.npmjs.com/package/@requestnetwork/data-access) | Indexing an batching of transactions | -| [`@requestnetwork/data-format`](/packages/data-format) | [![npm](https://img.shields.io/npm/v/@requestnetwork/data-format.svg)](https://www.npmjs.com/package/@requestnetwork/data-format) | Standards for data stored on Request, like invoices format | -| [`@requestnetwork/epk-signature`](/packages/epk-signature) | [![npm](https://img.shields.io/npm/v/@requestnetwork/epk-signature.svg)](https://www.npmjs.com/package/@requestnetwork/epk-signature) | Sign requests using Ethereum private keys | -| [`@requestnetwork/ethereum-storage`](/packages/ethereum-storage) | [![npm](https://img.shields.io/npm/v/@requestnetwork/ethereum-storage.svg)](https://www.npmjs.com/package/@requestnetwork/ethereum-storage) | Storage of Request data on Ethereum and IPFS | -| [`@requestnetwork/epk-decryption`](/packages/epk-decryption) | [![npm](https://img.shields.io/npm/v/@requestnetwork/epk-decryption.svg)](https://www.npmjs.com/package/@requestnetwork/epk-decryption) | Decrypt encrypted requests using Ethereum private keys | -| [`@requestnetwork/payment-detection`](/packages/payment-detection) | [![npm](https://img.shields.io/npm/v/@requestnetwork/payment-detection.svg)](https://www.npmjs.com/package/@requestnetwork/payment-detection) | Client-side payment detection, to compute the balance. | -| [`@requestnetwork/payment-processor`](/packages/payment-processor) | [![npm](https://img.shields.io/npm/v/@requestnetwork/payment-processor.svg)](https://www.npmjs.com/package/@requestnetwork/payment-processor) | Pay a request using a web3 wallet | -| [`@requestnetwork/request-logic`](/packages/request-logic) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-logic.svg)](https://www.npmjs.com/package/@requestnetwork/request-logic) | The Request business logic: properties and actions of requests | -| [`@requestnetwork/request-node`](/packages/request-node) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-node.svg)](https://www.npmjs.com/package/@requestnetwork/request-node) | Web server that allows easy access to Request system | -| [`@requestnetwork/transaction-manager`](/packages/transaction-manager) | [![npm](https://img.shields.io/npm/v/@requestnetwork/transaction-manager.svg)](https://www.npmjs.com/package/@requestnetwork/transaction-manager) | Creates transactions to be sent to Data Access, managing encryption | -| [`@requestnetwork/types`](/packages/types) | [![npm](https://img.shields.io/npm/v/@requestnetwork/types.svg)](https://www.npmjs.com/package/@requestnetwork/types) | Typescript types shared across @requestnetwork packages | -| [`@requestnetwork/utils`](/packages/utils) | [![npm](https://img.shields.io/npm/v/@requestnetwork/utils.svg)](https://www.npmjs.com/package/@requestnetwork/utils) | Collection of tools shared between the @requestnetwork packages | -| [`@requestnetwork/web3-signature`](/packages/web3-signature) | [![npm](https://img.shields.io/npm/v/@requestnetwork/web3-signature.svg)](https://www.npmjs.com/package/@requestnetwork/web3-signature) | Sign requests using web3 tools (like Metamask) | -| [`@requestnetwork/multi-format`](/packages/multi-format) | [![npm](https://img.shields.io/npm/v/@requestnetwork/multi-format.svg)](https://www.npmjs.com/package/@requestnetwork/multi-format) | Serialize and deserialize object in the Request Network protocol | -| [`@requestnetwork/smart-contracts`](/packages/smart-contracts) | [![npm](https://img.shields.io/npm/v/@requestnetwork/smart-contracts.svg)](https://www.npmjs.com/package/@requestnetwork/smart-contracts) | Sources and artifacts of the smart contracts | +| Package | Version | Description | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [`@requestnetwork/advanced-logic`](/packages/advanced-logic) | [![npm](https://img.shields.io/npm/v/@requestnetwork/advanced-logic.svg)](https://www.npmjs.com/package/@requestnetwork/advanced-logic) | Extensions to the protocol | +| [`@requestnetwork/request-client.js`](/packages/request-client.js) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-client.js.svg)](https://www.npmjs.com/package/@requestnetwork/request-client.js) | Library to use Request nodes as servers | +| [`@requestnetwork/data-access`](/packages/data-access) | [![npm](https://img.shields.io/npm/v/@requestnetwork/data-access.svg)](https://www.npmjs.com/package/@requestnetwork/data-access) | Indexing an batching of transactions | +| [`@requestnetwork/data-format`](/packages/data-format) | [![npm](https://img.shields.io/npm/v/@requestnetwork/data-format.svg)](https://www.npmjs.com/package/@requestnetwork/data-format) | Standards for data stored on Request, like invoices format | +| [`@requestnetwork/epk-signature`](/packages/epk-signature) | [![npm](https://img.shields.io/npm/v/@requestnetwork/epk-signature.svg)](https://www.npmjs.com/package/@requestnetwork/epk-signature) | Sign requests using Ethereum private keys | +| [`@requestnetwork/ethereum-storage`](/packages/ethereum-storage) | [![npm](https://img.shields.io/npm/v/@requestnetwork/ethereum-storage.svg)](https://www.npmjs.com/package/@requestnetwork/ethereum-storage) | Storage of Request data on Ethereum and IPFS, with custom indexing | +| [`@requestnetwork/epk-decryption`](/packages/epk-decryption) | [![npm](https://img.shields.io/npm/v/@requestnetwork/epk-decryption.svg)](https://www.npmjs.com/package/@requestnetwork/epk-decryption) | Decrypt encrypted requests using Ethereum private keys | +| [`@requestnetwork/payment-detection`](/packages/payment-detection) | [![npm](https://img.shields.io/npm/v/@requestnetwork/payment-detection.svg)](https://www.npmjs.com/package/@requestnetwork/payment-detection) | Client-side payment detection, to compute the balance. | +| [`@requestnetwork/payment-processor`](/packages/payment-processor) | [![npm](https://img.shields.io/npm/v/@requestnetwork/payment-processor.svg)](https://www.npmjs.com/package/@requestnetwork/payment-processor) | Pay a request using a web3 wallet | +| [`@requestnetwork/request-logic`](/packages/request-logic) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-logic.svg)](https://www.npmjs.com/package/@requestnetwork/request-logic) | The Request business logic: properties and actions of requests | +| [`@requestnetwork/request-node`](/packages/request-node) | [![npm](https://img.shields.io/npm/v/@requestnetwork/request-node.svg)](https://www.npmjs.com/package/@requestnetwork/request-node) | Web server that allows easy access to Request system | +| [`@requestnetwork/transaction-manager`](/packages/transaction-manager) | [![npm](https://img.shields.io/npm/v/@requestnetwork/transaction-manager.svg)](https://www.npmjs.com/package/@requestnetwork/transaction-manager) | Creates transactions to be sent to Data Access, managing encryption | +| [`@requestnetwork/types`](/packages/types) | [![npm](https://img.shields.io/npm/v/@requestnetwork/types.svg)](https://www.npmjs.com/package/@requestnetwork/types) | Typescript types shared across @requestnetwork packages | +| [`@requestnetwork/utils`](/packages/utils) | [![npm](https://img.shields.io/npm/v/@requestnetwork/utils.svg)](https://www.npmjs.com/package/@requestnetwork/utils) | Collection of tools shared between the @requestnetwork packages | +| [`@requestnetwork/web3-signature`](/packages/web3-signature) | [![npm](https://img.shields.io/npm/v/@requestnetwork/web3-signature.svg)](https://www.npmjs.com/package/@requestnetwork/web3-signature) | Sign requests using web3 tools (like Metamask) | +| [`@requestnetwork/multi-format`](/packages/multi-format) | [![npm](https://img.shields.io/npm/v/@requestnetwork/multi-format.svg)](https://www.npmjs.com/package/@requestnetwork/multi-format) | Serialize and deserialize object in the Request Network protocol | +| [`@requestnetwork/thegraph-data-access`](/packages/thegraph-data-access) | [![npm](https://img.shields.io/npm/v/@requestnetwork/thegraph-data-access.svg)](https://www.npmjs.com/package/@requestnetwork/thegraph-data-access) | Storage of Request data on Ethereum and IPFS, indexed by TheGraph | +| [`@requestnetwork/smart-contracts`](/packages/smart-contracts) | [![npm](https://img.shields.io/npm/v/@requestnetwork/smart-contracts.svg)](https://www.npmjs.com/package/@requestnetwork/smart-contracts) | Sources and artifacts of the smart contracts | ### Private Packages diff --git a/package.json b/package.json index 4126373771..38d361c92d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "lerna run build", "clean": "lerna run clean", - "build:tsc": "tsc -b packages/advanced-logic/tsconfig.build.json packages/currency/tsconfig.build.json packages/data-access/tsconfig.build.json packages/data-format/tsconfig.build.json packages/epk-decryption/tsconfig.build.json packages/epk-signature/tsconfig.build.json packages/ethereum-storage/tsconfig.build.json packages/integration-test/tsconfig.build.json packages/multi-format/tsconfig.build.json packages/payment-detection/tsconfig.build.json packages/prototype-estimator/tsconfig.build.json packages/request-client.js/tsconfig.build.json packages/request-logic/tsconfig.build.json packages/request-node/tsconfig.build.json packages/smart-contracts/tsconfig.build.json packages/toolbox/tsconfig.build.json packages/transaction-manager/tsconfig.build.json packages/types/tsconfig.build.json packages/usage-examples/tsconfig.build.json packages/utils/tsconfig.build.json packages/web3-signature/tsconfig.build.json", + "build:tsc": "tsc -b packages/**/tsconfig.build.json", "lint": "eslint . --fix --quiet", "lint:check": "eslint . --quiet", "lint-staged": "lint-staged", diff --git a/packages/request-node/src/thegraph/CombinedDataAccess.ts b/packages/data-access/src/combined-data-access.ts similarity index 100% rename from packages/request-node/src/thegraph/CombinedDataAccess.ts rename to packages/data-access/src/combined-data-access.ts diff --git a/packages/data-access/src/index.ts b/packages/data-access/src/index.ts index 59a666cdaa..c3240bb96e 100644 --- a/packages/data-access/src/index.ts +++ b/packages/data-access/src/index.ts @@ -1,3 +1,4 @@ export { default as DataAccess } from './data-access'; export { default as TransactionIndex } from './transaction-index'; export { default as Block } from './block'; +export { CombinedDataAccess } from './combined-data-access'; diff --git a/packages/ethereum-storage/package.json b/packages/ethereum-storage/package.json index 2b290ddf3b..74b861ab06 100644 --- a/packages/ethereum-storage/package.json +++ b/packages/ethereum-storage/package.json @@ -45,6 +45,7 @@ "@requestnetwork/utils": "0.35.0", "axios": "0.27.2", "bluebird": "3.7.2", + "eip1559-fee-suggestions-ethers": "1.3.3", "ethers": "5.5.1", "form-data": "3.0.0", "ipfs-unixfs": "6.0.7", @@ -52,6 +53,7 @@ "qs": "6.10.3", "shelljs": "0.8.5", "tslib": "2.3.1", + "typed-emitter": "1.4.0", "web3-eth": "1.3.6", "web3-utils": "1.3.6", "yargs": "16.2.0" diff --git a/packages/request-node/src/thegraph/TheGraphStorage.ts b/packages/ethereum-storage/src/ethereum-storage-ethers.ts similarity index 84% rename from packages/request-node/src/thegraph/TheGraphStorage.ts rename to packages/ethereum-storage/src/ethereum-storage-ethers.ts index b6db87b0d1..a598850b35 100644 --- a/packages/request-node/src/thegraph/TheGraphStorage.ts +++ b/packages/ethereum-storage/src/ethereum-storage-ethers.ts @@ -6,21 +6,20 @@ import { LogTypes, StorageTypes } from '@requestnetwork/types'; import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; import { RequestOpenHashSubmitter } from '@requestnetwork/smart-contracts/types'; import { suggestFees } from 'eip1559-fee-suggestions-ethers'; -import { GasPriceDefiner } from '@requestnetwork/ethereum-storage'; -type TheGraphStorageProps = { +type StorageProps = { network: string; signer: Signer; ipfsStorage: StorageTypes.IIpfsStorage; logger?: LogTypes.ILogger; }; -export type TheGraphStorageEventEmitter = TypedEmitter<{ +export type StorageEventEmitter = TypedEmitter<{ confirmed: (receipt: ContractReceipt) => void; error: (error: unknown) => void; }>; -export class TheGraphStorage { +export class EthereumStorageEthers implements StorageTypes.IStorageWrite { private readonly logger: LogTypes.ILogger; private readonly ipfsStorage: StorageTypes.IIpfsStorage; private readonly hashSubmitter: RequestOpenHashSubmitter; @@ -28,7 +27,7 @@ export class TheGraphStorage { private readonly provider: providers.JsonRpcProvider; private enableEip1559 = true; - constructor({ network, signer, ipfsStorage, logger }: TheGraphStorageProps) { + constructor({ network, signer, ipfsStorage, logger }: StorageProps) { this.logger = logger || new Utils.SimpleLogger(); this.ipfsStorage = ipfsStorage; this.network = network; @@ -49,7 +48,7 @@ export class TheGraphStorage { ); this.enableEip1559 = false; } - this.logger.debug('TheGraph storage initialized'); + this.logger.debug(`${EthereumStorageEthers.name} storage initialized`); } async append(content: string): Promise { @@ -65,13 +64,6 @@ export class TheGraphStorage { const maxFeePerGas = maxPriorityFeePerGas.add(suggestedFee.baseFeeSuggestion); overrides.maxPriorityFeePerGas = maxPriorityFeePerGas; overrides.maxFeePerGas = maxFeePerGas; - } else { - // retro-compatibility for networks where the eth_feeHistory RPC method is not available (pre EIP-1559) - const gasPriceDefiner = new GasPriceDefiner(); - overrides.gasPrice = await gasPriceDefiner.getGasPrice( - StorageTypes.GasPriceType.FAST, - this.network, - ); } const tx = await this.hashSubmitter.submitHash( ipfsHash, @@ -79,7 +71,7 @@ export class TheGraphStorage { overrides, ); - const eventEmitter = new EventEmitter() as TheGraphStorageEventEmitter; + const eventEmitter = new EventEmitter() as StorageEventEmitter; const result: StorageTypes.IEntry = { id: ipfsHash, content, diff --git a/packages/ethereum-storage/src/index.ts b/packages/ethereum-storage/src/index.ts index 3cdfaf2e78..8452969add 100644 --- a/packages/ethereum-storage/src/index.ts +++ b/packages/ethereum-storage/src/index.ts @@ -1,3 +1,4 @@ export { EthereumStorage } from './ethereum-storage'; +export { EthereumStorageEthers } from './ethereum-storage-ethers'; export { GasPriceDefiner } from './gas-price-definer'; export { IpfsStorage } from './ipfs-storage'; diff --git a/packages/request-node/package.json b/packages/request-node/package.json index c8868f99af..7c603e38ce 100644 --- a/packages/request-node/package.json +++ b/packages/request-node/package.json @@ -45,13 +45,13 @@ "@requestnetwork/data-access": "0.26.0", "@requestnetwork/ethereum-storage": "0.26.0", "@requestnetwork/smart-contracts": "0.28.0", + "@requestnetwork/thegraph-data-access": "0.35.0", "@requestnetwork/types": "0.35.0", "@requestnetwork/utils": "0.35.0", "@truffle/hdwallet-provider": "1.2.3", "chalk": "4.1.0", "cors": "2.8.5", "dotenv": "8.2.0", - "eip1559-fee-suggestions-ethers": "1.3.3", "ethers": "5.5.1", "express": "4.17.1", "graphql": "15.5.0", @@ -81,7 +81,6 @@ "ts-jest": "26.3.0", "ts-node": "9.0.0", "ts-node-dev": "1.0.0-pre.62", - "typed-emitter": "1.4.0", "typescript": "4.4.4" }, "gitHead": "6155223cfce769e48ccae480c510b35b4f54b4d0" diff --git a/packages/request-node/src/server.ts b/packages/request-node/src/server.ts index 7dbc72f0f7..4fe4d01e57 100755 --- a/packages/request-node/src/server.ts +++ b/packages/request-node/src/server.ts @@ -5,7 +5,7 @@ import * as config from './config'; import { Logger } from './logger'; import { RequestNode } from './requestNode'; import withShutdown from 'http-shutdown'; -import { TheGraphRequestNode } from './thegraph'; +import { TheGraphRequestNode } from './thegraph-node'; // Initialize the node logger const { logLevel, logMode } = config.getLogConfig(); diff --git a/packages/request-node/src/thegraph/TheGraphRequestNode.ts b/packages/request-node/src/thegraph-node.ts similarity index 69% rename from packages/request-node/src/thegraph/TheGraphRequestNode.ts rename to packages/request-node/src/thegraph-node.ts index aecb0e8e86..81308be702 100644 --- a/packages/request-node/src/thegraph/TheGraphRequestNode.ts +++ b/packages/request-node/src/thegraph-node.ts @@ -3,14 +3,17 @@ import { providers, Wallet } from 'ethers'; import { NonceManager } from '@ethersproject/experimental'; import { LogTypes } from '@requestnetwork/types'; -import { TheGraphDataAccess } from './TheGraphDataAccess'; -import { RequestNodeBase } from '../requestNodeBase'; -import * as config from '../config'; -import { getIpfsStorage } from '../storageUtils'; +import { RequestNodeBase } from './requestNodeBase'; +import * as config from './config'; +import { getIpfsStorage } from './storageUtils'; +import Utils from '@requestnetwork/utils'; +import { TheGraphDataAccess } from '@requestnetwork/thegraph-data-access'; +import { EthereumStorageEthers } from '@requestnetwork/ethereum-storage'; export class TheGraphRequestNode extends RequestNodeBase { constructor(url: string, logger?: LogTypes.ILogger) { const initializationStoragePath = config.getInitializationStorageFilePath(); + logger = logger || new Utils.SimpleLogger(); const store = initializationStoragePath ? new KeyvFile({ @@ -25,12 +28,17 @@ export class TheGraphRequestNode extends RequestNodeBase { ); const signer = new NonceManager(wallet); const ipfsStorage = getIpfsStorage(logger); + const storage = new EthereumStorageEthers({ + ipfsStorage, + signer, + network, + logger, + }); const dataAccess = new TheGraphDataAccess({ graphql: { url }, - ipfsStorage, + storage, network, - signer, - logger: logger, + logger, }); super(dataAccess, ipfsStorage, store, logger); diff --git a/packages/request-node/src/thegraph/index.ts b/packages/request-node/src/thegraph/index.ts deleted file mode 100644 index 2e25c3c79c..0000000000 --- a/packages/request-node/src/thegraph/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { TheGraphRequestNode } from './TheGraphRequestNode'; -export { CombinedDataAccess } from './CombinedDataAccess'; -export { - TheGraphDataAccess, - TheGraphDataRead, - TheGraphDataWrite, - TheGraphDataAccessOptions, -} from './TheGraphDataAccess'; -export { PendingStore } from './PendingStore'; -export { TheGraphStorage } from './TheGraphStorage'; -export { SubgraphClient } from './subgraphClient'; -export * as queries from './queries'; diff --git a/packages/request-node/test/thegraph/persistTransaction.test.ts b/packages/request-node/test/thegraph/persistTransaction.test.ts index bb7fead804..8df2ce7f6b 100644 --- a/packages/request-node/test/thegraph/persistTransaction.test.ts +++ b/packages/request-node/test/thegraph/persistTransaction.test.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { RequestNodeBase } from '../../src/requestNodeBase'; -import { TheGraphRequestNode } from '../../src/thegraph'; +import { TheGraphRequestNode } from '../../src/thegraph-node'; import * as core from 'express-serve-static-core'; const subgraphUrl = 'http://localhost:8000/subgraphs/name/RequestNetwork/request-storage'; diff --git a/packages/request-node/tsconfig.build.json b/packages/request-node/tsconfig.build.json index ef22d05262..57b1d05782 100644 --- a/packages/request-node/tsconfig.build.json +++ b/packages/request-node/tsconfig.build.json @@ -9,6 +9,7 @@ "references": [ { "path": "../data-access/tsconfig.build.json" }, { "path": "../ethereum-storage/tsconfig.build.json" }, + { "path": "../thegraph-data-access/tsconfig.build.json" }, { "path": "../types/tsconfig.build.json" } ] } diff --git a/packages/thegraph-data-access/.nycrc b/packages/thegraph-data-access/.nycrc new file mode 100644 index 0000000000..8f593ebeb3 --- /dev/null +++ b/packages/thegraph-data-access/.nycrc @@ -0,0 +1,19 @@ +{ + "extension": [ + ".ts" + ], + "include": [ + "src/*.ts", + "src/**/*.ts" + ], + "require": [ + "ts-node/register" + ], + "reporter": [ + "text-summary", + "json", + "html" + ], + "sourceMap":true, + "all": true +} diff --git a/packages/thegraph-data-access/jest.config.js b/packages/thegraph-data-access/jest.config.js new file mode 100644 index 0000000000..aa4a4384f8 --- /dev/null +++ b/packages/thegraph-data-access/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverage: true, +}; diff --git a/packages/thegraph-data-access/package.json b/packages/thegraph-data-access/package.json new file mode 100644 index 0000000000..a98c2f2eea --- /dev/null +++ b/packages/thegraph-data-access/package.json @@ -0,0 +1,63 @@ +{ + "name": "@requestnetwork/thegraph-data-access", + "version": "0.35.0", + "publishConfig": { + "access": "public" + }, + "description": "Request Storage backed by TheGraph indexing.", + "keywords": [ + "requestnetwork", + "utils" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/RequestNetwork/requestNetwork.git" + }, + "homepage": "https://github.com/RequestNetwork/requestNetwork/tree/master/packages/utils#readme", + "bugs": { + "url": "https://github.com/RequestNetwork/requestNetwork/issues" + }, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "directories": { + "lib": "src", + "test": "test" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "shx rm -rf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", + "lint": "eslint . --fix", + "lint:check": "eslint .", + "prepare": "yarn run build", + "test": "jest", + "test:watch": "yarn test --watch" + }, + "dependencies": { + "@requestnetwork/data-access": "0.26.0", + "@requestnetwork/smart-contracts": "0.28.0", + "@requestnetwork/types": "0.35.0", + "@requestnetwork/utils": "0.35.0", + "ethers": "5.5.1", + "graphql-request": "3.4.0", + "tslib": "2.3.1", + "typed-emitter": "1.4.0" + }, + "devDependencies": { + "@types/jest": "26.0.13", + "jest": "26.4.2", + "prettier": "2.2.1", + "shx": "0.3.2", + "source-map-support": "0.5.19", + "ts-jest": "26.3.0", + "ts-node": "9.0.0", + "typescript": "4.4.4" + }, + "gitHead": "6155223cfce769e48ccae480c510b35b4f54b4d0" +} diff --git a/packages/request-node/src/thegraph/TheGraphDataAccess.ts b/packages/thegraph-data-access/src/data-access.ts similarity index 87% rename from packages/request-node/src/thegraph/TheGraphDataAccess.ts rename to packages/thegraph-data-access/src/data-access.ts index e9c1bb0a34..273b1ad9dc 100644 --- a/packages/request-node/src/thegraph/TheGraphDataAccess.ts +++ b/packages/thegraph-data-access/src/data-access.ts @@ -1,27 +1,28 @@ import { EventEmitter } from 'events'; import TypedEmitter from 'typed-emitter'; -import { BigNumber, Signer } from 'ethers'; +import { BigNumber } from 'ethers'; import Utils from '@requestnetwork/utils'; import { Block } from '@requestnetwork/data-access'; import { DataAccessTypes, LogTypes, StorageTypes } from '@requestnetwork/types'; import { Transaction } from './queries'; -import { SubgraphClient } from './subgraphClient'; -import { TheGraphStorage } from './TheGraphStorage'; -import { CombinedDataAccess } from './CombinedDataAccess'; -import { PendingStore } from './PendingStore'; +import { SubgraphClient } from './subgraph-client'; +import { CombinedDataAccess } from '@requestnetwork/data-access'; +import { PendingStore } from './pending-store'; -export type TheGraphDataAccessOptions = { - ipfsStorage: StorageTypes.IIpfsStorage; - graphql: { url: string } & RequestInit; - signer: Signer; +type TheGraphDataAccessBaseOptions = { network: string; logger?: LogTypes.ILogger; pendingStore?: PendingStore; }; +export type TheGraphDataAccessOptions = TheGraphDataAccessBaseOptions & { + graphql: { url: string } & RequestInit; + storage?: StorageTypes.IStorageWrite; +}; + type DataAccessEventEmitter = TypedEmitter<{ confirmed: (data: DataAccessTypes.IReturnPersistTransactionRaw) => void; error: (error: unknown) => void; @@ -57,7 +58,7 @@ export class TheGraphDataRead implements DataAccessTypes.IDataRead { constructor( private readonly graphql: SubgraphClient, - { network, pendingStore }: Pick, + { network, pendingStore }: TheGraphDataAccessBaseOptions, ) { this.network = network; this.pendingStore = pendingStore; @@ -214,16 +215,9 @@ export class TheGraphDataWrite implements DataAccessTypes.IDataWrite { private pendingStore?: PendingStore; constructor( - protected storage: TheGraphStorage, + protected readonly storage: StorageTypes.IStorageWrite, private readonly graphql: SubgraphClient, - { - network, - logger, - pendingStore, - }: Pick< - TheGraphDataAccessOptions, - 'ipfsStorage' | 'network' | 'signer' | 'logger' | 'pendingStore' - >, + { network, logger, pendingStore }: TheGraphDataAccessBaseOptions, ) { this.logger = logger || new Utils.SimpleLogger(); this.network = network; @@ -310,29 +304,53 @@ export class TheGraphDataWrite implements DataAccessTypes.IDataWrite { } } +class NoopDataWrite implements DataAccessTypes.IDataWrite { + async initialize(): Promise { + // no-op + } + + async close(): Promise { + // no-op + } + + persistTransaction(): Promise { + throw new Error( + `cannot call persistTranscation without storage. Specify storage on ${TheGraphDataAccess.name}`, + ); + } +} + export class TheGraphDataAccess extends CombinedDataAccess { private readonly graphql: SubgraphClient; - private readonly storage: TheGraphStorage; + private readonly storage: StorageTypes.IStorageWrite | undefined; - constructor({ graphql, ...options }: TheGraphDataAccessOptions) { + constructor({ graphql, storage, ...options }: TheGraphDataAccessOptions) { const { url, ...rest } = graphql; if (!options.pendingStore) { options.pendingStore = new PendingStore(); } const graphqlClient = new SubgraphClient(url, rest); - const storage = new TheGraphStorage(options); + const reader = new TheGraphDataRead(graphqlClient, options); - const writer = new TheGraphDataWrite(storage, graphqlClient, options); + + const writer = storage + ? new TheGraphDataWrite(storage, graphqlClient, options) + : new NoopDataWrite(); + super(reader, writer); this.graphql = graphqlClient; this.storage = storage; } async _getStatus(): Promise { + let storage: any = null; + if (this.storage && '_getStatus' in this.storage) { + storage = await (this.storage as StorageTypes.IStorage)._getStatus(); + } return { lastBlock: await this.graphql.getBlockNumber(), endpoint: this.graphql.endpoint, - storage: await this.storage._getStatus(), + storage, }; } } diff --git a/packages/thegraph-data-access/src/index.ts b/packages/thegraph-data-access/src/index.ts new file mode 100644 index 0000000000..cb60f77cb5 --- /dev/null +++ b/packages/thegraph-data-access/src/index.ts @@ -0,0 +1,9 @@ +export { + TheGraphDataAccess, + TheGraphDataRead, + TheGraphDataWrite, + TheGraphDataAccessOptions, +} from './data-access'; +export { PendingStore } from './pending-store'; +export { SubgraphClient } from './subgraph-client'; +export * as queries from './queries'; diff --git a/packages/request-node/src/thegraph/PendingStore.ts b/packages/thegraph-data-access/src/pending-store.ts similarity index 100% rename from packages/request-node/src/thegraph/PendingStore.ts rename to packages/thegraph-data-access/src/pending-store.ts diff --git a/packages/request-node/src/thegraph/queries.ts b/packages/thegraph-data-access/src/queries.ts similarity index 100% rename from packages/request-node/src/thegraph/queries.ts rename to packages/thegraph-data-access/src/queries.ts diff --git a/packages/request-node/src/thegraph/subgraphClient.ts b/packages/thegraph-data-access/src/subgraph-client.ts similarity index 100% rename from packages/request-node/src/thegraph/subgraphClient.ts rename to packages/thegraph-data-access/src/subgraph-client.ts diff --git a/packages/thegraph-data-access/tsconfig.build.json b/packages/thegraph-data-access/tsconfig.build.json new file mode 100644 index 0000000000..791700b847 --- /dev/null +++ b/packages/thegraph-data-access/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../types/tsconfig.build.json" }] +} diff --git a/packages/thegraph-data-access/tsconfig.json b/packages/thegraph-data-access/tsconfig.json new file mode 100644 index 0000000000..41716a7dd5 --- /dev/null +++ b/packages/thegraph-data-access/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig" +} diff --git a/packages/types/src/storage-types.ts b/packages/types/src/storage-types.ts index 4ab5c6702d..d4902f1fc2 100644 --- a/packages/types/src/storage-types.ts +++ b/packages/types/src/storage-types.ts @@ -2,14 +2,21 @@ import { EventEmitter } from 'events'; import { BigNumber } from 'ethers'; -/** Interface of the storage */ -export interface IStorage { +export interface IStorageWrite { initialize: () => Promise; append: (data: string) => Promise; +} + +export interface IStorageRead { + initialize: () => Promise; read: (dataId: string) => Promise; readMany: (dataIds: string[]) => Promise; getData: (options?: ITimestampBoundaries) => Promise; getIgnoredData: () => Promise; +} + +/** Interface of the storage */ +export interface IStorage extends IStorageRead, IStorageWrite { _getStatus: (detailed?: boolean) => Promise; } From f063864c62cb4ab79ce097a26f7c061674e36cac Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Fri, 7 Oct 2022 11:26:49 +0200 Subject: [PATCH 024/207] fix: NEAR Conversion version and processor (#935) --- .../src/extensions/payment-network/any-to-near.ts | 2 +- .../payment-network/any/generator-data-create.ts | 12 ++++++------ packages/payment-processor/src/payment/index.ts | 7 ++++--- .../payment-processor/src/payment/near-conversion.ts | 3 ++- packages/payment-processor/src/payment/utils-near.ts | 9 ++++++--- .../test/payment/any-to-near.test.ts | 5 +++-- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts index 95799cf16d..3915af0ac5 100644 --- a/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-near.ts @@ -3,7 +3,7 @@ import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwor import { UnsupportedNetworkError } from './address-based'; import AnyToNativeTokenPaymentNetwork from './any-to-native'; -const CURRENT_VERSION = '0.2.0'; +const CURRENT_VERSION = '0.1.0'; const supportedNetworks = ['aurora', 'aurora-testnet']; export default class AnyToNearPaymentNetwork extends AnyToNativeTokenPaymentNetwork { diff --git a/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts b/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts index 4385d7cc29..d54320dcb9 100644 --- a/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts +++ b/packages/advanced-logic/test/utils/payment-network/any/generator-data-create.ts @@ -93,7 +93,7 @@ export const actionCreationWithNativeTokenPayment: ExtensionTypes.IAction = { @@ -108,7 +108,7 @@ export const actionCreationWithAnyToNativeTokenPayment: ExtensionTypes.IAction { const paymentNetwork = getPaymentNetwork(request); - if (!paymentNetwork || !supportedNetworks.includes(paymentNetwork)) { + if (!paymentNetwork || !noConversionNetworks.includes(paymentNetwork)) { throw new UnsupportedNetworkError(paymentNetwork); } @@ -216,6 +216,7 @@ export async function hasSufficientFunds( /** * Verifies the address has enough funds to pay an amount in a given currency. + * Supported chains: EVMs and Near. * * @param fromAddress the address willing to pay * @param amount diff --git a/packages/payment-processor/src/payment/near-conversion.ts b/packages/payment-processor/src/payment/near-conversion.ts index b76089e014..afb50fcd8b 100644 --- a/packages/payment-processor/src/payment/near-conversion.ts +++ b/packages/payment-processor/src/payment/near-conversion.ts @@ -41,7 +41,7 @@ export async function payNearConversionRequest( } if (!network || !isNearNetwork(network)) { - throw new Error('Should be a near network'); + throw new Error('Should be a Near network'); } const amountToPay = getAmountToPay(request, amount).toString(); @@ -56,6 +56,7 @@ export async function payNearConversionRequest( getTicker(request.currencyInfo), feeAddress || '0x', feeAmount || 0, + paymentSettings.maxToSpend, maxRateTimespan || '0', version, ); diff --git a/packages/payment-processor/src/payment/utils-near.ts b/packages/payment-processor/src/payment/utils-near.ts index 0cf2d779c8..de74d1524b 100644 --- a/packages/payment-processor/src/payment/utils-near.ts +++ b/packages/payment-processor/src/payment/utils-near.ts @@ -34,6 +34,8 @@ export const isNearAccountSolvent = ( const GAS_LIMIT_IN_TGAS = 50; const GAS_LIMIT = ethers.utils.parseUnits(GAS_LIMIT_IN_TGAS.toString(), 12); +const GAS_LIMIT_NATIVE = GAS_LIMIT.toString(); +const GAS_LIMIT_CONVERSION_TO_NATIVE = GAS_LIMIT.mul(2).toString(); export const processNearPayment = async ( walletConnection: WalletConnection, @@ -69,7 +71,7 @@ export const processNearPayment = async ( to, payment_reference: paymentReference, }, - GAS_LIMIT.toString(), + GAS_LIMIT_NATIVE, amount.toString(), ); return; @@ -94,6 +96,7 @@ export const processNearPaymentWithConversion = async ( currency: string, feeAddress: string, feeAmount: BigNumberish, + maxToSpend: BigNumberish, maxRateTimespan = '0', version = '0.1.0', ): Promise => { @@ -127,8 +130,8 @@ export const processNearPaymentWithConversion = async ( fee_amount: feeAmount, max_rate_timespan: maxRateTimespan, }, - GAS_LIMIT.toString(), - amount.toString(), + GAS_LIMIT_CONVERSION_TO_NATIVE, + maxToSpend.toString(), ); return; } catch (e) { diff --git a/packages/payment-processor/test/payment/any-to-near.test.ts b/packages/payment-processor/test/payment/any-to-near.test.ts index 9bfd99a7c1..a1c78fd98a 100644 --- a/packages/payment-processor/test/payment/any-to-near.test.ts +++ b/packages/payment-processor/test/payment/any-to-near.test.ts @@ -41,7 +41,7 @@ const request: any = { }; // Use the default currency manager -const conversionSettings = {} as unknown as IConversionPaymentSettings; +const conversionSettings: IConversionPaymentSettings = { maxToSpend: '30' }; describe('payNearWithConversionRequest', () => { afterEach(() => { @@ -75,6 +75,7 @@ describe('payNearWithConversionRequest', () => { 'USD', feeAddress, feeAmount, + conversionSettings.maxToSpend, '0', '0.1.0', ); @@ -159,7 +160,7 @@ describe('payNearWithConversionRequest', () => { await expect( payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), - ).rejects.toThrowError('Should be a near network'); + ).rejects.toThrowError('Should be a Near network'); expect(paymentSpy).toHaveBeenCalledTimes(0); }); }); From ce9990f704cd374218769ff4cd9f4d2e8e041bbd Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 7 Oct 2022 14:01:16 +0200 Subject: [PATCH 025/207] fix: min gas (#940) --- packages/ethereum-storage/src/ethereum-storage-ethers.ts | 8 ++++++-- .../payment-detection/test/eth/info-retriever.test.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ethereum-storage/src/ethereum-storage-ethers.ts b/packages/ethereum-storage/src/ethereum-storage-ethers.ts index a598850b35..6f8184e0a6 100644 --- a/packages/ethereum-storage/src/ethereum-storage-ethers.ts +++ b/packages/ethereum-storage/src/ethereum-storage-ethers.ts @@ -62,8 +62,12 @@ export class EthereumStorageEthers implements StorageTypes.IStorageWrite { ); const maxPriorityFeePerGas = BigNumber.from(suggestedFee.maxPriorityFeeSuggestions.urgent); const maxFeePerGas = maxPriorityFeePerGas.add(suggestedFee.baseFeeSuggestion); - overrides.maxPriorityFeePerGas = maxPriorityFeePerGas; - overrides.maxFeePerGas = maxFeePerGas; + if (maxPriorityFeePerGas.gt(0)) { + overrides.maxPriorityFeePerGas = maxPriorityFeePerGas; + } + if (maxFeePerGas.gt(0)) { + overrides.maxFeePerGas = maxFeePerGas; + } } const tx = await this.hashSubmitter.submitHash( ipfsHash, diff --git a/packages/payment-detection/test/eth/info-retriever.test.ts b/packages/payment-detection/test/eth/info-retriever.test.ts index de5f76798b..635c5b4c09 100644 --- a/packages/payment-detection/test/eth/info-retriever.test.ts +++ b/packages/payment-detection/test/eth/info-retriever.test.ts @@ -33,7 +33,7 @@ describe('api/eth/info-retriever', () => { ); expect(typeof events[0].parameters!.block).toBe('number'); expect(typeof events[0].parameters!.confirmations).toBe('number'); - }); + }, 10000); it('throws when trying to use it in local', async () => { const infoRetreiver = new EthInputDataInfoRetriever( From 8fc5ba89f0960996feb63f2ee4a17c82af69561a Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:30:16 +0200 Subject: [PATCH 026/207] fix: celo api (#941) --- .../payment-detection/src/eth/multichainExplorerApiProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts b/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts index 94054856a5..da341869c6 100644 --- a/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts +++ b/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts @@ -33,7 +33,7 @@ export class MultichainExplorerApiProvider extends ethers.providers.EtherscanPro case 'fuse': return 'https://explorer.fuse.io'; case 'celo': - return 'https://explorer.celo.org'; + return 'https://explorer.celo.org/mainnet'; case 'matic': return 'https://api.polygonscan.com'; case 'fantom': From fbd313b7acb33e5a9794cd9159754688150aff93 Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Fri, 7 Oct 2022 16:37:50 +0200 Subject: [PATCH 027/207] chore: update husky (#939) --- .husky/pre-commit | 4 ++ package.json | 3 +- packages/advanced-logic/package.json | 1 - packages/data-access/package.json | 1 - packages/data-format/package.json | 1 - packages/epk-decryption/package.json | 1 - packages/epk-signature/package.json | 1 - packages/ethereum-storage/package.json | 1 - packages/integration-test/package.json | 1 - packages/multi-format/package.json | 1 - packages/payment-detection/package.json | 1 - packages/payment-processor/package.json | 1 - packages/prototype-estimator/package.json | 1 - packages/request-client.js/package.json | 1 - packages/request-logic/package.json | 1 - packages/request-node/package.json | 1 - packages/thegraph-data-access/package.json | 1 - packages/toolbox/package.json | 2 - packages/transaction-manager/package.json | 1 - packages/usage-examples/package.json | 1 - packages/utils/package.json | 1 - packages/web3-signature/package.json | 1 - yarn.lock | 51 ++++------------------ 23 files changed, 15 insertions(+), 64 deletions(-) create mode 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..5a182ef106 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint-staged diff --git a/package.json b/package.json index 38d361c92d..f8e6a73a36 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "url": "git+https://github.com/RequestNetwork/requestNetwork.git" }, "scripts": { + "prepare": "husky install", "build": "lerna run build", "clean": "lerna run clean", "build:tsc": "tsc -b packages/**/tsconfig.build.json", @@ -41,7 +42,7 @@ "eslint-plugin-import": "2.22.1", "eslint-plugin-jsdoc": "32.3.0", "eslint-plugin-prefer-arrow": "1.2.3", - "husky": "4.3.0", + "husky": "8.0.1", "lerna": "3.22.1", "lint-staged": "10.5.4", "npm-package-json-lint": "5.1.0", diff --git a/packages/advanced-logic/package.json b/packages/advanced-logic/package.json index a758727b47..d0b5a0d8f8 100644 --- a/packages/advanced-logic/package.json +++ b/packages/advanced-logic/package.json @@ -51,7 +51,6 @@ "@types/lodash": "4.14.161", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "ts-jest": "26.3.0", "ts-node": "9.0.0", diff --git a/packages/data-access/package.json b/packages/data-access/package.json index 6f1b1da473..649fca3fd5 100644 --- a/packages/data-access/package.json +++ b/packages/data-access/package.json @@ -53,7 +53,6 @@ "@types/node": "14.14.16", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/data-format/package.json b/packages/data-format/package.json index ab9519ac15..b68ca8be08 100644 --- a/packages/data-format/package.json +++ b/packages/data-format/package.json @@ -47,7 +47,6 @@ "devDependencies": { "@types/node": "14.14.16", "nyc": "15.1.0", - "prettier": "2.2.1", "rimraf": "3.0.2", "shx": "0.3.2", "ts-node": "9.0.0", diff --git a/packages/epk-decryption/package.json b/packages/epk-decryption/package.json index 018f663774..c48acf6865 100644 --- a/packages/epk-decryption/package.json +++ b/packages/epk-decryption/package.json @@ -53,7 +53,6 @@ "jest": "26.4.2", "npm-run-all": "4.1.5", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "terser-webpack-plugin": "4.2.3", diff --git a/packages/epk-signature/package.json b/packages/epk-signature/package.json index 0b9f93b2dd..72a92ce1a9 100644 --- a/packages/epk-signature/package.json +++ b/packages/epk-signature/package.json @@ -52,7 +52,6 @@ "jest": "26.4.2", "npm-run-all": "4.1.5", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "terser-webpack-plugin": "4.2.3", diff --git a/packages/ethereum-storage/package.json b/packages/ethereum-storage/package.json index 74b861ab06..2dfb1437f7 100644 --- a/packages/ethereum-storage/package.json +++ b/packages/ethereum-storage/package.json @@ -68,7 +68,6 @@ "axios-mock-adapter": "1.19.0", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "solium": "1.2.5", "source-map-support": "0.5.19", diff --git a/packages/integration-test/package.json b/packages/integration-test/package.json index eac0ee6b1e..b195cb1aeb 100644 --- a/packages/integration-test/package.json +++ b/packages/integration-test/package.json @@ -59,7 +59,6 @@ "ethers": "5.5.1", "jest": "26.4.2", "npm-run-all": "4.1.5", - "prettier": "2.2.1", "ts-jest": "26.3.0", "ts-node": "9.0.0", "tslib": "2.3.1", diff --git a/packages/multi-format/package.json b/packages/multi-format/package.json index 7dbb6a1ca4..061a3c9697 100644 --- a/packages/multi-format/package.json +++ b/packages/multi-format/package.json @@ -47,7 +47,6 @@ "@types/jest": "26.0.13", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/payment-detection/package.json b/packages/payment-detection/package.json index c7e120ed3e..964234ec43 100644 --- a/packages/payment-detection/package.json +++ b/packages/payment-detection/package.json @@ -64,7 +64,6 @@ "@types/jest": "26.0.13", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/payment-processor/package.json b/packages/payment-processor/package.json index db4f11a71e..d97b39c9bd 100644 --- a/packages/payment-processor/package.json +++ b/packages/payment-processor/package.json @@ -53,7 +53,6 @@ "@types/jest": "26.0.13", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/prototype-estimator/package.json b/packages/prototype-estimator/package.json index 19c79c9067..549bb13340 100644 --- a/packages/prototype-estimator/package.json +++ b/packages/prototype-estimator/package.json @@ -45,7 +45,6 @@ "devDependencies": { "@types/benchmark": "2.1.0", "@types/node": "14.14.16", - "prettier": "2.2.1", "ts-node": "9.0.0", "typescript": "4.4.4" } diff --git a/packages/request-client.js/package.json b/packages/request-client.js/package.json index 52a61fe002..36a0002a36 100644 --- a/packages/request-client.js/package.json +++ b/packages/request-client.js/package.json @@ -68,7 +68,6 @@ "jest": "26.4.2", "npm-run-all": "4.1.5", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "terser-webpack-plugin": "4.2.3", diff --git a/packages/request-logic/package.json b/packages/request-logic/package.json index cd4d5a1dfb..aee8dd270f 100644 --- a/packages/request-logic/package.json +++ b/packages/request-logic/package.json @@ -52,7 +52,6 @@ "@types/semver": "7.3.4", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/request-node/package.json b/packages/request-node/package.json index 7c603e38ce..fb9e01dee3 100644 --- a/packages/request-node/package.json +++ b/packages/request-node/package.json @@ -74,7 +74,6 @@ "@types/supertest": "2.0.10", "@types/yargs": "15.0.5", "jest": "26.4.2", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "supertest": "5.0.0", diff --git a/packages/thegraph-data-access/package.json b/packages/thegraph-data-access/package.json index a98c2f2eea..58f4e156b6 100644 --- a/packages/thegraph-data-access/package.json +++ b/packages/thegraph-data-access/package.json @@ -52,7 +52,6 @@ "devDependencies": { "@types/jest": "26.0.13", "jest": "26.4.2", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/toolbox/package.json b/packages/toolbox/package.json index 88cad70d27..67ff1b21d6 100644 --- a/packages/toolbox/package.json +++ b/packages/toolbox/package.json @@ -61,8 +61,6 @@ "@types/inquirer": "8.1.3", "@types/yargs": "16.0.1", "cross-env": "7.0.2", - "husky": "4.3.0", - "prettier": "2.2.1", "shx": "0.3.2", "ts-node": "9.0.0", "typescript": "4.4.4" diff --git a/packages/transaction-manager/package.json b/packages/transaction-manager/package.json index ef53f05703..24deaabd8b 100644 --- a/packages/transaction-manager/package.json +++ b/packages/transaction-manager/package.json @@ -48,7 +48,6 @@ "@types/jest": "26.0.13", "jest": "26.4.2", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/usage-examples/package.json b/packages/usage-examples/package.json index 4873a1d9e8..438e9322dd 100644 --- a/packages/usage-examples/package.json +++ b/packages/usage-examples/package.json @@ -42,7 +42,6 @@ "tslib": "2.3.1" }, "devDependencies": { - "prettier": "2.2.1", "ts-node": "9.0.0", "typescript": "4.4.4" } diff --git a/packages/utils/package.json b/packages/utils/package.json index 2a7f108ee4..59eda1e68e 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -50,7 +50,6 @@ "@types/eccrypto": "1.1.2", "@types/jest": "26.0.13", "jest": "26.4.2", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "ts-jest": "26.3.0", diff --git a/packages/web3-signature/package.json b/packages/web3-signature/package.json index 7639ba6537..58e0b061c8 100644 --- a/packages/web3-signature/package.json +++ b/packages/web3-signature/package.json @@ -53,7 +53,6 @@ "jest": "26.4.2", "npm-run-all": "4.1.5", "nyc": "15.1.0", - "prettier": "2.2.1", "shx": "0.3.2", "source-map-support": "0.5.19", "terser-webpack-plugin": "4.2.3", diff --git a/yarn.lock b/yarn.lock index e7a36c6898..bc7034f669 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10035,11 +10035,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -13829,13 +13824,6 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== - dependencies: - semver-regex "^2.0.0" - find-yarn-workspace-root@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db" @@ -15529,21 +15517,10 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de" - integrity sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA== - dependencies: - chalk "^4.0.0" - ci-info "^2.0.0" - compare-versions "^3.6.0" - cosmiconfig "^7.0.0" - find-versions "^3.2.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" +husky@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" + integrity sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw== i18next@^17.0.16: version "17.3.1" @@ -21818,11 +21795,6 @@ prettier@2.1.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== -prettier@2.2.1, prettier@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== - prettier@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" @@ -21833,6 +21805,11 @@ prettier@^1.14.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + pretty-error@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" @@ -23634,11 +23611,6 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== - "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -28968,11 +28940,6 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - which-typed-array@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" From 2ba9cf25ae387faac6b8adaa5095f0eaba975b16 Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Sat, 8 Oct 2022 11:18:35 +0200 Subject: [PATCH 028/207] fix: replace blockscout by gnosisscan (#942) --- .../payment-detection/src/eth/multichainExplorerApiProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts b/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts index da341869c6..20d8eab3b7 100644 --- a/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts +++ b/packages/payment-detection/src/eth/multichainExplorerApiProvider.ts @@ -29,7 +29,7 @@ export class MultichainExplorerApiProvider extends ethers.providers.EtherscanPro switch (this.network.name) { case 'sokol': case 'xdai': - return `https://blockscout.com/poa/${this.network.name}`; + return 'https://api.gnosisscan.io'; case 'fuse': return 'https://explorer.fuse.io'; case 'celo': From a9e3f3e093843a1187f624be21833a2dbae0f87b Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Mon, 10 Oct 2022 09:48:35 +0200 Subject: [PATCH 029/207] fix(request-client): remove useLocalEthereumBroadcast from tests (#937) --- .../src/http-metamask-data-access.ts | 2 +- .../src/http-request-network.ts | 3 -- packages/request-client.js/src/index.ts | 11 +++++- .../test/index-persist-from-metamask.html | 35 ++++++------------- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index 52d1d50cd5..dd13b9fc0d 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -52,7 +52,7 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { // Creates a local or default provider this.provider = web3 - ? new ethers.providers.Web3Provider(web3.currentProvider) + ? new ethers.providers.Web3Provider(web3) : new ethers.providers.JsonRpcProvider({ url: ethereumProviderUrl }); } diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index d2d64fae46..81e9b0b2a3 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -23,7 +23,6 @@ export default class HttpRequestNetwork extends RequestNetwork { * @param options.nodeConnectionConfig Configuration options to connect to the node. Follows Axios configuration format. * @param options.useMockStorage When true, will use a mock storage in memory. Meant to simplify local development and should never be used in production. * @param options.signatureProvider Module to handle the signature. If not given it will be impossible to create new transaction (it requires to sign). - * @param options.useLocalEthereumBroadcast When true, persisting use the node only for IPFS but persisting on ethereum through local provider (given in ethereumProviderUrl). * @param options.currencies custom currency list * @param options.currencyManager custom currency manager (will override `currencies`) */ @@ -43,14 +42,12 @@ export default class HttpRequestNetwork extends RequestNetwork { nodeConnectionConfig?: AxiosRequestConfig; signatureProvider?: SignatureProviderTypes.ISignatureProvider; useMockStorage?: boolean; - useLocalEthereumBroadcast?: boolean; currencies?: CurrencyInput[]; currencyManager?: ICurrencyManager; paymentOptions?: PaymentNetworkOptions; } = { httpConfig: {}, nodeConnectionConfig: {}, - useLocalEthereumBroadcast: false, useMockStorage: false, }, ) { diff --git a/packages/request-client.js/src/index.ts b/packages/request-client.js/src/index.ts index 14f0ad9487..3273105601 100644 --- a/packages/request-client.js/src/index.ts +++ b/packages/request-client.js/src/index.ts @@ -4,6 +4,15 @@ import Request from './api/request'; import Utils from './api/utils'; import { default as RequestNetwork } from './http-request-network'; import { default as RequestNetworkBase } from './api/request-network'; +import { default as HttpMetaMaskDataAccess } from './http-metamask-data-access'; import * as Types from './types'; -export { PaymentReferenceCalculator, Request, RequestNetwork, RequestNetworkBase, Types, Utils }; +export { + PaymentReferenceCalculator, + Request, + RequestNetwork, + RequestNetworkBase, + HttpMetaMaskDataAccess, + Types, + Utils, +}; diff --git a/packages/request-client.js/test/index-persist-from-metamask.html b/packages/request-client.js/test/index-persist-from-metamask.html index 0f3f6fc560..68455ef9d4 100644 --- a/packages/request-client.js/test/index-persist-from-metamask.html +++ b/packages/request-client.js/test/index-persist-from-metamask.html @@ -2,6 +2,7 @@ @requestnetwork/request-client.js Test Page + @@ -13,25 +14,10 @@

Important

The dependencies must be built. (yarn build)