From 1ae8468104aa316b7072e73dafd8e4bd382487c8 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 23 Sep 2021 10:49:56 +0200 Subject: [PATCH 1/4] fix: updated Near payment detection + gas increase (#589) --- .../src/near-info-retriever.ts | 22 +++++++++++++++---- .../test/near-native.test.ts | 14 ++++++------ .../src/payment/utils-near.ts | 2 +- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/payment-detection/src/near-info-retriever.ts b/packages/payment-detection/src/near-info-retriever.ts index c14a9a2652..91b04876b9 100644 --- a/packages/payment-detection/src/near-info-retriever.ts +++ b/packages/payment-detection/src/near-info-retriever.ts @@ -55,14 +55,14 @@ export class NearInfoRetriever { COALESCE(a.args::json->>'deposit', '') as deposit, COALESCE(a.args::json->>'method_name', '') as method_name, COALESCE((a.args::json->'args_json')::json->>'to', '') as "to", - (a.args::json->'args_json')::json->>'payment_reference' as paymentReference, + (a.args::json->'args_json')::json->>'payment_reference' as "paymentReference", (select MAX(block_height) from blocks) - b.block_height as confirmations FROM transactions t INNER JOIN transaction_actions a ON (a.transaction_hash = t.transaction_hash) INNER JOIN receipts r ON (r.originated_from_transaction_hash = t.transaction_hash) INNER JOIN blocks b ON (b.block_timestamp = r.included_in_block_timestamp) INNER JOIN execution_outcomes e ON (e.receipt_id = r.receipt_id) - INNER JOIN action_receipt_actions ra ON (ra.receipt_id = r.receipt_id) + INNER JOIN action_receipt_actions ra ON (ra.receipt_id = r.receipt_id) WHERE r.receiver_account_id = :paymentAddress AND r.predecessor_account_id != 'system' AND a.action_kind = 'FUNCTION_CALL' @@ -71,10 +71,24 @@ export class NearInfoRetriever { AND b.block_height >= (select MAX(block_height) from blocks) - 1e8 AND (a.args::json->'args_json')::json->>'payment_reference' = :paymentReference AND ra.action_kind = 'TRANSFER' + -- Check that the transaction did not revert: + AND EXISTS( + SELECT 1 + FROM execution_outcome_receipts eor, + action_receipt_actions ara, + execution_outcomes eo + WHERE eor.executed_receipt_id = t.converted_into_receipt_id + AND ara.receipt_id = eor.produced_receipt_id + AND eo.receipt_id = eor.produced_receipt_id + AND ara.action_kind = 'FUNCTION_CALL' + AND COALESCE(ara.args::json->>'method_name', '') = 'on_transfer_with_reference' + AND eo.status = 'SUCCESS_VALUE') ORDER BY b.block_height DESC LIMIT 100`; + return new Promise((resolve, reject) => { try { + // eslint-disable-next-line const autobahn = require('autobahn'); const connection: any = new autobahn.Connection({ url: this.nearWebSocketUrl, @@ -90,9 +104,9 @@ export class NearInfoRetriever { paymentReference: this.paymentReference, }, ]) - .then((data: any) => { + .then((data: NearIndexerTransaction[]) => { connection.close(); - resolve(data as NearIndexerTransaction[]); + resolve(data); }) .catch((err: Error) => { reject(`Could not connect to Near indexer web socket: ${err.message}.\n${err.stack}`); diff --git a/packages/payment-detection/test/near-native.test.ts b/packages/payment-detection/test/near-native.test.ts index 93036e3df4..62da1f0515 100644 --- a/packages/payment-detection/test/near-native.test.ts +++ b/packages/payment-detection/test/near-native.test.ts @@ -22,10 +22,10 @@ const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { }, extensions: { nativeToken: [mockNearPaymentNetwork] }, }; -const salt = 'f60b918fa5e83c1d'; -const paymentAddress = 'yoissuer.testnet'; +const salt = '360ab22e5fb6c41c'; +const paymentAddress = 'pay.testnet'; const request: any = { - requestId: '01edb4d8d3396bd688ffa028fbcebd224ba0fdfb5f690eeb45b86aa02cb7d58891', + requestId: '017a738821782329122ffb1b944dc2bbcecc56fdc8d95b050fe49a1fc04349a9c4', currency: { network: 'aurora-testnet', type: RequestLogicTypes.CURRENCY.ETH, @@ -39,7 +39,7 @@ const request: any = { paymentAddress, salt, }, - version: '0.1.0', + version: '0.2.0', }, }, }; @@ -55,7 +55,7 @@ describe('Near payments detection', () => { const infoRetriever = new NearInfoRetriever( paymentReference, paymentAddress, - 'dev-1626339335241-5544297', + 'dev-1631521265288-35171138540673', 'com.nearprotocol.testnet.explorer.select:INDEXER_BACKEND', PaymentTypes.EVENTS_NAMES.PAYMENT, 'aurora-testnet', @@ -63,7 +63,7 @@ describe('Near payments detection', () => { const events = await infoRetriever.getTransferEvents(); expect(events).toHaveLength(1); - expect(events[0].amount).toBe('400000000000000000000000'); + expect(events[0].amount).toBe('2000000000000000000000000'); }); it('PaymentNetworkFactory can get the detector (testnet)', async () => { @@ -92,8 +92,8 @@ describe('Near payments detection', () => { }); const balance = await paymentDetector.getBalance(request); - expect(balance.balance).toBe('400000000000000000000000'); expect(balance.events).toHaveLength(1); + expect(balance.balance).toBe('2000000000000000000000000'); }); describe('Edge cases for NearNativeTokenPaymentDetector', () => { diff --git a/packages/payment-processor/src/payment/utils-near.ts b/packages/payment-processor/src/payment/utils-near.ts index f941233fb3..f17ee4efe4 100644 --- a/packages/payment-processor/src/payment/utils-near.ts +++ b/packages/payment-processor/src/payment/utils-near.ts @@ -29,7 +29,7 @@ export const isNearAccountSolvent = ( }); }; -const GAS_LIMIT_IN_TGAS = 30; +const GAS_LIMIT_IN_TGAS = 50; const GAS_LIMIT = ethers.utils.parseUnits(GAS_LIMIT_IN_TGAS.toString(), 12); /** From c78803fb1333917b843db935df0114a50e294f5f Mon Sep 17 00:00:00 2001 From: Vincent <4611986+vrolland@users.noreply.github.com> Date: Thu, 23 Sep 2021 17:14:14 +0200 Subject: [PATCH 2/4] feat: payment detection for ethereum fee proxy (#585) --- .../ethereum/fee-proxy-contract.ts | 2 +- .../ethereum/fee-proxy-contract.test.ts | 6 +- .../fee-proxy-contract-add-data-generator.ts | 10 +- ...ee-proxy-contract-create-data-generator.ts | 20 +-- .../integration-test/test/node-client.test.ts | 59 +++++++ .../src/eth/fee-proxy-detector.ts | 81 +++++++++ packages/payment-detection/src/eth/index.ts | 3 +- .../src/eth/proxy-info-retriever.ts | 31 +++- .../src/fee-reference-based-detector.ts | 77 +++++++++ packages/payment-detection/src/index.ts | 3 +- .../src/payment-network-factory.ts | 2 + .../test/eth/fee-proxy-detector.test.ts | 157 ++++++++++++++++++ .../test/eth/proxy-info-retriever.test.ts | 6 +- .../test/payment/eth-fee-proxy.test.ts | 2 +- .../lib/artifacts/EthereumFeeProxy/index.ts | 4 +- packages/types/src/payment-types.ts | 16 +- 16 files changed, 450 insertions(+), 29 deletions(-) create mode 100644 packages/payment-detection/src/eth/fee-proxy-detector.ts create mode 100644 packages/payment-detection/src/fee-reference-based-detector.ts create mode 100644 packages/payment-detection/test/eth/fee-proxy-detector.test.ts diff --git a/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts b/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts index e1b44d7adf..81401e75ed 100644 --- a/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts +++ b/packages/advanced-logic/src/extensions/payment-network/ethereum/fee-proxy-contract.ts @@ -2,7 +2,7 @@ import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import FeeReferenceBasedPaymentNetwork from '../fee-reference-based'; import * as walletAddressValidator from 'wallet-address-validator'; -const CURRENT_VERSION = '0.2.0'; +const CURRENT_VERSION = '0.1.0'; /** * Implementation of the payment network to pay in Ethereum, including third-party fees payment, based on a reference provided to a proxy contract. diff --git a/packages/advanced-logic/test/extensions/payment-network/ethereum/fee-proxy-contract.test.ts b/packages/advanced-logic/test/extensions/payment-network/ethereum/fee-proxy-contract.test.ts index 7f499f4032..e9cb64e779 100644 --- a/packages/advanced-logic/test/extensions/payment-network/ethereum/fee-proxy-contract.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/ethereum/fee-proxy-contract.test.ts @@ -32,7 +32,7 @@ describe('extensions/payment-network/ethereum/fee-proxy-contract', () => { refundAddress: '0x0000000000000000000000000000000000000003', salt: 'ea3bc7caf64110ca', }, - version: '0.2.0', + version: '0.1.0', }); }); @@ -52,7 +52,7 @@ describe('extensions/payment-network/ethereum/fee-proxy-contract', () => { refundAddress: '0x0000000000000000000000000000000000000002', salt: 'ea3bc7caf64110ca', }, - version: '0.2.0', + version: '0.1.0', }); }); @@ -68,7 +68,7 @@ describe('extensions/payment-network/ethereum/fee-proxy-contract', () => { parameters: { salt: 'ea3bc7caf64110ca', }, - version: '0.2.0', + version: '0.1.0', }); }); diff --git a/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-add-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-add-data-generator.ts index 3f9ed6982f..4a0a928c2e 100644 --- a/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-add-data-generator.ts +++ b/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-add-data-generator.ts @@ -62,7 +62,7 @@ export const extensionStateWithPaymentAfterCreation = { values: { paymentAddress, }, - version: '0.2.0', + version: '0.1.0', }, }; @@ -87,7 +87,7 @@ export const extensionStateWithRefundAfterCreation = { values: { refundAddress, }, - version: '0.2.0', + version: '0.1.0', }, }; @@ -114,7 +114,7 @@ export const extensionStateWithFeeAfterCreation = { feeAddress, feeAmount, }, - version: '0.2.0', + version: '0.1.0', }, }; @@ -159,7 +159,7 @@ export const requestStateCreatedEmptyThenAddPayment: RequestLogicTypes.IRequest requestId: TestData.requestIdMock, state: RequestLogicTypes.STATE.CREATED, timestamp: TestData.arbitraryTimestamp, - version: '0.2.0', + version: '0.1.0', }; export const requestStateCreatedEmptyThenAddFee: RequestLogicTypes.IRequest = { @@ -201,5 +201,5 @@ export const requestStateCreatedEmptyThenAddFee: RequestLogicTypes.IRequest = { requestId: TestData.requestIdMock, state: RequestLogicTypes.STATE.CREATED, timestamp: TestData.arbitraryTimestamp, - version: '0.2.0', + version: '0.1.0', }; diff --git a/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-create-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-create-data-generator.ts index 5adc0b3de2..a9a8bb8304 100644 --- a/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-create-data-generator.ts +++ b/packages/advanced-logic/test/utils/payment-network/ethereum/fee-proxy-contract-create-data-generator.ts @@ -24,7 +24,7 @@ export const actionCreationFull = { refundAddress, salt, }, - version: '0.2.0', + version: '0.1.0', }; export const actionCreationOnlyPayment = { action: 'create', @@ -32,7 +32,7 @@ export const actionCreationOnlyPayment = { parameters: { paymentAddress, }, - version: '0.2.0', + version: '0.1.0', }; export const actionCreationOnlyRefund = { action: 'create', @@ -40,7 +40,7 @@ export const actionCreationOnlyRefund = { parameters: { refundAddress, }, - version: '0.2.0', + version: '0.1.0', }; export const actionCreationOnlyFee = { action: 'create', @@ -49,13 +49,13 @@ export const actionCreationOnlyFee = { feeAddress, feeAmount, }, - version: '0.2.0', + version: '0.1.0', }; export const actionCreationEmpty = { action: 'create', id: ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT, parameters: {}, - version: '0.2.0', + version: '0.1.0', }; // --------------------------------------------------------------------- @@ -84,7 +84,7 @@ export const extensionFullState = { refundAddress, salt, }, - version: '0.2.0', + version: '0.1.0', }, }; export const extensionStateCreatedEmpty = { @@ -99,7 +99,7 @@ export const extensionStateCreatedEmpty = { id: ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT, type: ExtensionTypes.TYPE.PAYMENT_NETWORK, values: {}, - version: '0.2.0', + version: '0.1.0', }, }; @@ -144,7 +144,7 @@ export const requestStateNoExtensions: RequestLogicTypes.IRequest = { requestId: TestData.requestIdMock, state: RequestLogicTypes.STATE.CREATED, timestamp: TestData.arbitraryTimestamp, - version: '0.2.0', + version: '0.1.0', }; export const requestFullStateCreated: RequestLogicTypes.IRequest = { @@ -186,7 +186,7 @@ export const requestFullStateCreated: RequestLogicTypes.IRequest = { requestId: TestData.requestIdMock, state: RequestLogicTypes.STATE.CREATED, timestamp: TestData.arbitraryTimestamp, - version: '0.2.0', + version: '0.1.0', }; export const requestStateCreatedEmpty: RequestLogicTypes.IRequest = { @@ -228,5 +228,5 @@ export const requestStateCreatedEmpty: RequestLogicTypes.IRequest = { requestId: TestData.requestIdMock, state: RequestLogicTypes.STATE.CREATED, timestamp: TestData.arbitraryTimestamp, - version: '0.2.0', + version: '0.1.0', }; diff --git a/packages/integration-test/test/node-client.test.ts b/packages/integration-test/test/node-client.test.ts index 12e4f87c19..d26e6371b1 100644 --- a/packages/integration-test/test/node-client.test.ts +++ b/packages/integration-test/test/node-client.test.ts @@ -624,3 +624,62 @@ describe('ERC20 localhost request creation and detection test', () => { expect(event?.parameters?.maxRateTimespan).toBe('1000000'); }); }); + +describe('ETH localhost request creation and detection test', () => { + const ethRequestCreationHash: Types.IRequestInfo = { + currency: { + network: 'private', + type: Types.RequestLogic.CURRENCY.ETH, + value: Types.RequestLogic.CURRENCY.ETH, + }, + expectedAmount: '1000', + payee: payeeIdentity, + payer: payerIdentity, + }; + + it('can create ETH requests and pay with ETH Fee proxy', async () => { + const currencies = [ + ...CurrencyManager.getDefaultList() + ]; + const requestNetwork = new RequestNetwork({ + signatureProvider, + useMockStorage: true, + currencies, + }); + + const paymentNetworkETHFeeProxy: PaymentTypes.IPaymentNetworkCreateParameters = { + id: PaymentTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT, + parameters: { + paymentAddress: '0xc12F17Da12cd01a9CDBB216949BA0b41A6Ffc4EB', + feeAddress: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2', + feeAmount: '200', + network: 'private', + maxRateTimespan: 1000000, + }, + }; + + const request = await requestNetwork.createRequest({ + paymentNetwork: paymentNetworkETHFeeProxy, + requestInfo: ethRequestCreationHash, + signer: payeeIdentity, + }); + + let data = await request.refresh(); + expect(data.balance).toBeNull(); + + const paymentTx = await payRequest(data, wallet); + await paymentTx.wait(); + + data = await request.refresh(); + expect(data.balance?.balance).toBe('1000'); + expect(data.balance?.events.length).toBe(1); + const event = data.balance?.events[0]; + expect(event?.amount).toBe('1000'); + expect(event?.name).toBe('payment'); + + expect(event?.parameters?.feeAmount).toBe('200'); + expect(event?.parameters?.feeAddress).toBe('0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2'); + // amount in crypto after apply the rates of the fake aggregators + expect(event?.parameters?.to).toBe('0xc12F17Da12cd01a9CDBB216949BA0b41A6Ffc4EB'); + }); +}); diff --git a/packages/payment-detection/src/eth/fee-proxy-detector.ts b/packages/payment-detection/src/eth/fee-proxy-detector.ts new file mode 100644 index 0000000000..e2a957316f --- /dev/null +++ b/packages/payment-detection/src/eth/fee-proxy-detector.ts @@ -0,0 +1,81 @@ +import * as SmartContracts from '@requestnetwork/smart-contracts'; +import { AdvancedLogicTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types'; + +import ProxyEthereumInfoRetriever from './proxy-info-retriever'; +import FeeReferenceBasedDetector from '../fee-reference-based-detector'; + +// interface of the object indexing the proxy contract version +interface IProxyContractVersion { + [version: string]: string; +} + +const PROXY_CONTRACT_ADDRESS_MAP: IProxyContractVersion = { + ['0.1.0']: '0.1.0', +}; + +/** + * Handle payment networks with ETH input data extension + */ +export default class ETHFeeProxyDetector extends FeeReferenceBasedDetector { + /** + * @param extension The advanced logic payment network extensions + */ + public constructor({ advancedLogic }: { advancedLogic: AdvancedLogicTypes.IAdvancedLogic }) { + super( + advancedLogic.extensions.feeProxyContractEth, + ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT, + ); + } + + /** + * Extracts the balance and events of an address + * + * @private + * @param address 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 + * @param paymentReference The reference to identify the payment + * @param paymentNetworkVersion the version of the payment network + * @returns The balance + */ + protected async extractEvents( + address: string, + eventName: PaymentTypes.EVENTS_NAMES, + network: string, + paymentReference: string, + paymentNetworkVersion: string, + ): Promise { + const proxyContractArtifact = await this.safeGetProxyArtifact(network, paymentNetworkVersion); + + if (!proxyContractArtifact) { + throw Error('ETH fee proxy contract not found'); + } + + const proxyInfoRetriever = new ProxyEthereumInfoRetriever( + paymentReference, + proxyContractArtifact.address, + proxyContractArtifact.creationBlockNumber, + address, + eventName, + network, + ); + + return await proxyInfoRetriever.getTransferEvents(); + } + + /* + * Fetches events from the Ethereum Proxy, or returns null + */ + private async safeGetProxyArtifact(network: string, paymentNetworkVersion: string) { + const contractVersion = PROXY_CONTRACT_ADDRESS_MAP[paymentNetworkVersion]; + try { + return SmartContracts.ethereumFeeProxyArtifact.getDeploymentInformation( + network, + contractVersion, + ); + } catch (error) { + console.warn(error); + } + return null; + } +} diff --git a/packages/payment-detection/src/eth/index.ts b/packages/payment-detection/src/eth/index.ts index 515d0d45c2..8525e65404 100644 --- a/packages/payment-detection/src/eth/index.ts +++ b/packages/payment-detection/src/eth/index.ts @@ -1,3 +1,4 @@ import InputData from './input-data'; +import FeeProxyDetector from './fee-proxy-detector'; -export { InputData }; +export { InputData, FeeProxyDetector }; diff --git a/packages/payment-detection/src/eth/proxy-info-retriever.ts b/packages/payment-detection/src/eth/proxy-info-retriever.ts index 4feed7e405..075c9ee188 100644 --- a/packages/payment-detection/src/eth/proxy-info-retriever.ts +++ b/packages/payment-detection/src/eth/proxy-info-retriever.ts @@ -6,6 +6,7 @@ import { parseLogArgs } from '../utils'; // The Ethereum proxy smart contract ABI fragment containing TransferWithReference event const ethProxyContractAbiFragment = [ 'event TransferWithReference(address to,uint256 amount,bytes indexed paymentReference)', + 'event TransferWithReferenceAndFee(address to,uint256 amount,bytes indexed paymentReference,uint256 feeAmount,address feeAddress)', ]; /** TransferWithReference event */ @@ -15,6 +16,12 @@ type TransferWithReferenceArgs = { paymentReference: string; }; +/** TransferWithReferenceAndFee event */ +type TransferWithReferenceAndFeeArgs = TransferWithReferenceArgs & { + feeAmount: BigNumber; + feeAddress: string; +}; + /** * Retrieves a list of payment events from a payment reference, a destination address, a token address and a proxy contract */ @@ -64,7 +71,24 @@ export default class ProxyEthereumInfoRetriever filter.toBlock = 'latest'; // Get the event logs - const logs = await this.provider.getLogs(filter); + const proxyLogs = await this.provider.getLogs(filter); + + // Create a filter to find all the Fee Transfer logs with the payment reference + const feeFilter = this.contractProxy.filters.TransferWithReferenceAndFee( + null, + null, + '0x' + this.paymentReference, + null, + null, + ) as ethers.providers.Filter; + feeFilter.fromBlock = this.proxyCreationBlockNumber; + feeFilter.toBlock = 'latest'; + + // Get the fee proxy contract event logs + const feeProxyLogs = await this.provider.getLogs(feeFilter); + + // Merge both events + const logs = [...proxyLogs, ...feeProxyLogs]; // Parses, filters and creates the events from the logs of the proxy contract const eventPromises = logs @@ -72,7 +96,7 @@ export default class ProxyEthereumInfoRetriever .map((log) => { const parsedLog = this.contractProxy.interface.parseLog(log); return { - parsedLog: parseLogArgs(parsedLog), + parsedLog: parseLogArgs(parsedLog), blockNumber: log.blockNumber, transactionHash: log.transactionHash, }; @@ -86,6 +110,9 @@ export default class ProxyEthereumInfoRetriever parameters: { block: blockNumber, txHash: transactionHash, + to: this.toAddress, + feeAddress: parsedLog.feeAddress, + feeAmount: parsedLog.feeAmount?.toString() || undefined, }, timestamp: (await this.provider.getBlock(blockNumber || 0)).timestamp, })); diff --git a/packages/payment-detection/src/fee-reference-based-detector.ts b/packages/payment-detection/src/fee-reference-based-detector.ts new file mode 100644 index 0000000000..71813c5a1f --- /dev/null +++ b/packages/payment-detection/src/fee-reference-based-detector.ts @@ -0,0 +1,77 @@ +import { ExtensionTypes, PaymentTypes } from '@requestnetwork/types'; +import Utils from '@requestnetwork/utils'; +import ReferenceBasedDetector from './reference-based-detector'; + +/** + * Abstract class to extend to get the payment balance of reference based requests + */ +export default abstract class FeeReferenceBasedDetector< + TPaymentEventParameters +> extends ReferenceBasedDetector { + /** + * @param extension The advanced logic payment network extension, reference based + * @param extensionType Example : ExtensionTypes.ID.PAYMENT_NETWORK_ETH_INPUT_DATA + */ + public constructor( + protected extension: ExtensionTypes.PnFeeReferenceBased.IFeeReferenceBased, + protected extensionType: ExtensionTypes.ID, + ) { + super(extension, extensionType); + } + + /** + * Creates the extensions data for the creation of this extension. + * Will set a salt if none is already given + * + * @param paymentNetworkCreationParameters Parameters to create the extension + * @returns The extensionData object + */ + public async createExtensionsDataForCreation( + paymentNetworkCreationParameters: ExtensionTypes.PnFeeReferenceBased.ICreationParameters, + ): Promise { + // If no salt is given, generate one + paymentNetworkCreationParameters.salt = + paymentNetworkCreationParameters.salt || (await Utils.crypto.generate8randomBytes()); + + return this.extension.createCreationAction({ + feeAddress: paymentNetworkCreationParameters.feeAddress, + feeAmount: paymentNetworkCreationParameters.feeAmount, + paymentAddress: paymentNetworkCreationParameters.paymentAddress, + refundAddress: paymentNetworkCreationParameters.refundAddress, + ...paymentNetworkCreationParameters, + }); + } + + /** + * Creates the extensions data to add fee address and amount + * + * @param Parameters to add refund information + * @returns The extensionData object + */ + public createExtensionsDataForAddFeeInformation( + parameters: ExtensionTypes.PnFeeReferenceBased.IAddFeeParameters, + ): ExtensionTypes.IAction { + return this.extension.createAddFeeAction({ + feeAddress: parameters.feeAddress, + feeAmount: parameters.feeAmount, + }); + } + + /** + * Extracts payment events of an address matching an address and a payment reference + * + * @param address 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 + * @param paymentReference The reference to identify the payment + * @param paymentNetworkVersion the version of the payment network + * @returns The balance + */ + protected abstract extractEvents( + address: string, + eventName: PaymentTypes.EVENTS_NAMES, + network: string, + paymentReference: string, + paymentNetworkVersion: string, + ): Promise[]>; +} diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 8e67d0c3a4..ca570b865e 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -4,7 +4,7 @@ import PaymentReferenceCalculator from './payment-reference-calculator'; import * as BtcPaymentNetwork from './btc'; import DeclarativePaymentNetwork from './declarative'; import * as Erc20PaymentNetwork from './erc20'; -import { InputData as EthPaymentNetwork } from './eth'; +import { InputData as EthPaymentNetwork, FeeProxyDetector as EthFeePaymentNetwork } from './eth'; import { initPaymentDetectionApiKeys, setProviderFactory, getDefaultProvider } from './provider'; import { getTheGraphClient, networkSupportsTheGraph } from './thegraph'; import { parseLogArgs, padAmountForChainlink, unpadAmountFromChainlink } from './utils'; @@ -25,6 +25,7 @@ export { DeclarativePaymentNetwork, Erc20PaymentNetwork, EthPaymentNetwork, + EthFeePaymentNetwork, Near, setProviderFactory, initPaymentDetectionApiKeys, diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 445a6ddf57..10d316d2c7 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -12,6 +12,7 @@ import ERC20AddressBased from './erc20/address-based'; import ERC20FeeProxyContract from './erc20/fee-proxy-contract'; import ERC20ProxyContract from './erc20/proxy-contract'; import EthInputData from './eth/input-data'; +import ETHFeeProxyDetector from './eth/fee-proxy-detector'; import AnyToErc20Proxy from './any/any-to-erc20-proxy-contract'; import NearNativeTokenPaymentDetector from './near-detector'; @@ -39,6 +40,7 @@ const supportedPaymentNetwork: PaymentTypes.ISupportedPaymentNetworkByCurrency = }, '*': { [ExtensionTypes.ID.PAYMENT_NETWORK_ETH_INPUT_DATA]: EthInputData, + [ExtensionTypes.ID.PAYMENT_NETWORK_ETH_FEE_PROXY_CONTRACT]: ETHFeeProxyDetector, }, }, }; diff --git a/packages/payment-detection/test/eth/fee-proxy-detector.test.ts b/packages/payment-detection/test/eth/fee-proxy-detector.test.ts new file mode 100644 index 0000000000..4c7d9b6c48 --- /dev/null +++ b/packages/payment-detection/test/eth/fee-proxy-detector.test.ts @@ -0,0 +1,157 @@ +import { + AdvancedLogicTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import ETHFeeProxyDetector from '../../src/eth/fee-proxy-detector'; + +let ethFeeProxyDetector: ETHFeeProxyDetector; + +const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { + applyActionToExtensions(): any { + return; + }, + extensions: { + feeProxyContractEth: { + createAddPaymentAddressAction(): any { + return; + }, + createAddRefundAddressAction(): any { + return; + }, + createCreationAction(): any { + return; + }, + createAddFeeAction(): any { + return; + }, + supportedNetworks: ['private'] + }, + }, +}; + +/* eslint-disable @typescript-eslint/no-unused-expressions */ +describe('api/eth/fee-proxy-contract', () => { + beforeEach(() => { + ethFeeProxyDetector = new ETHFeeProxyDetector({ + advancedLogic: mockAdvancedLogic, + }); + }); + + it('can createExtensionsDataForCreation', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createCreationAction', + ); + + await ethFeeProxyDetector.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + }); + + expect(spy).toHaveBeenCalledWith({ + feeAddress: undefined, + feeAmount: undefined, + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + }); + }); + + it('can createExtensionsDataForCreation with fee amount and address', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createCreationAction', + ); + + await ethFeeProxyDetector.createExtensionsDataForCreation({ + feeAddress: 'fee address', + feeAmount: '2000', + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + }); + + expect(spy).toHaveBeenCalledWith({ + feeAddress: 'fee address', + feeAmount: '2000', + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + }); + }); + + it('can createExtensionsDataForCreation without salt', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createCreationAction', + ); + + await ethFeeProxyDetector.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + }); + + // Can't check parameters since salt is generated in createExtensionsDataForCreation + expect(spy).toHaveBeenCalled(); + }); + + it('can createExtensionsDataForAddPaymentInformation', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createAddPaymentAddressAction', + ); + + ethFeeProxyDetector.createExtensionsDataForAddPaymentInformation({ + paymentAddress: 'ethereum address', + }); + + expect(spy).toHaveBeenCalledWith({ + paymentAddress: 'ethereum address', + }); + }); + + it('can createExtensionsDataForAddRefundInformation', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createAddRefundAddressAction', + ); + + ethFeeProxyDetector.createExtensionsDataForAddRefundInformation({ + refundAddress: 'ethereum address', + }); + + expect(spy).toHaveBeenCalledWith({ + refundAddress: 'ethereum address', + }); + }); + + it('can createExtensionsDataForAddFeeInformation', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.feeProxyContractEth, + 'createAddFeeAction', + ); + + ethFeeProxyDetector.createExtensionsDataForAddFeeInformation({ + feeAddress: 'ethereum address', + feeAmount: '2000', + }); + + expect(spy).toHaveBeenCalledWith({ + feeAddress: 'ethereum address', + feeAmount: '2000', + }); + }); + + it('should not throw when getBalance fail', async () => { + expect( + await ethFeeProxyDetector.getBalance({ currency: {network: 'private'}, extensions: {} } as RequestLogicTypes.IRequest), + ).toEqual({ + balance: null, + error: { + code: PaymentTypes.BALANCE_ERROR_CODE.WRONG_EXTENSION, + message: 'The request does not have the extension: pn-eth-fee-proxy-contract', + }, + events: [], + }); + }); +}); diff --git a/packages/payment-detection/test/eth/proxy-info-retriever.test.ts b/packages/payment-detection/test/eth/proxy-info-retriever.test.ts index 76defab2e3..c302010e4f 100644 --- a/packages/payment-detection/test/eth/proxy-info-retriever.test.ts +++ b/packages/payment-detection/test/eth/proxy-info-retriever.test.ts @@ -22,7 +22,11 @@ describe('api/eth/proxy-info-retriever', () => { ); // inject mock provider.getLogs() - infoRetriever.provider.getLogs = (): any => { + infoRetriever.provider.getLogs = (filter): any => { + // return nothing when it's from the "eth-fee-proxy" event (as we use the same getLogs for both contracts) + if(filter.topics && filter.topics[0] === '0xa1c241e337c4610a9d0f881111e977e9dc8690c85fe2108897bb1483c66e6a96') { + return [] + } return [ { address: proxyContractAddress, diff --git a/packages/payment-processor/test/payment/eth-fee-proxy.test.ts b/packages/payment-processor/test/payment/eth-fee-proxy.test.ts index 6bab017756..1e56b36348 100644 --- a/packages/payment-processor/test/payment/eth-fee-proxy.test.ts +++ b/packages/payment-processor/test/payment/eth-fee-proxy.test.ts @@ -51,7 +51,7 @@ const validRequest: ClientTypes.IRequestData = { paymentAddress, salt: 'salt', }, - version: '0.2.0', + version: '0.1.0', }, }, extensionsData: [], diff --git a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts index 11ac4dd93a..0e90c7a1e8 100644 --- a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/index.ts @@ -5,7 +5,7 @@ import type { EthereumFeeProxy } from '../../../types/EthereumFeeProxy'; export const ethereumFeeProxyArtifact = new ContractArtifact( { - '0.2.0': { + '0.1.0': { abi: ABI_0_1_0, deployment: { private: { @@ -15,5 +15,5 @@ export const ethereumFeeProxyArtifact = new ContractArtifact( }, }, }, - '0.2.0', + '0.1.0', ); diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 28268e8be0..e3aaed54af 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -146,10 +146,22 @@ export interface IETHPaymentEventParameters { confirmations?: number; txHash?: string; } +/** Parameters for events of ERC20 payments with fees */ +export interface IETHFeePaymentEventParameters extends IETHPaymentEventParameters { + feeAddress?: string; + feeAmount?: string; + feeAmountInCrypto?: string; + amountInCrypto?: string; +} + /** ETH Payment Network Event */ -export type ETHPaymentNetworkEvent = IPaymentNetworkEvent; +export type ETHPaymentNetworkEvent = IPaymentNetworkEvent< + IETHPaymentEventParameters | IETHFeePaymentEventParameters +>; /** ETH BalanceWithEvents */ -export type ETHBalanceWithEvents = IBalanceWithEvents; +export type ETHBalanceWithEvents = IBalanceWithEvents< + IETHPaymentEventParameters | IETHFeePaymentEventParameters +>; /** Parameters for events of BTC payments */ export interface IBTCPaymentEventParameters { From 8607627fabbf55543c178e5fda68bfa43abc3580 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 23 Sep 2021 18:02:41 +0200 Subject: [PATCH 3/4] fix: don't check ETH solvency for a Gnosis provider (#590) --- packages/payment-processor/src/payment/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index fafd8d0e8a..7ff224130e 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -191,9 +191,11 @@ export async function isSolvent( } const provider = providerOptions.provider; const ethBalance = await provider.getBalance(fromAddress); - const needsGas = !['Safe Multisig WalletConnect', 'Gnosis Safe Multisig'].includes( - (provider as any)?.provider?.wc?._peerMeta?.name, - ); + const needsGas = + !(provider as any)?.provider?.safe?.safeAddress && + !['Safe Multisig WalletConnect', 'Gnosis Safe Multisig'].includes( + (provider as any)?.provider?.wc?._peerMeta?.name, + ); if (currency.type === 'ETH') { return ethBalance.gt(amount); From eb08b857dd80e345c8799190fa1211fe0560a5e2 Mon Sep 17 00:00:00 2001 From: Vincent <4611986+vrolland@users.noreply.github.com> Date: Fri, 24 Sep 2021 11:05:39 +0200 Subject: [PATCH 4/4] feat: ethereum fee proxy with transfer exact eth amount (#591) --- .../src/contracts/EthereumFeeProxy.sol | 44 ++++++++++++++++--- .../lib/artifacts/EthereumFeeProxy/0.1.0.json | 33 ++++++++++++++ .../test/contracts/EthereumFeeProxy.test.ts | 21 +++++++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/src/contracts/EthereumFeeProxy.sol b/packages/smart-contracts/src/contracts/EthereumFeeProxy.sol index ee12e8c570..91fec635bc 100644 --- a/packages/smart-contracts/src/contracts/EthereumFeeProxy.sol +++ b/packages/smart-contracts/src/contracts/EthereumFeeProxy.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + /** * @title EthereumFeeProxy * @notice This contract performs an Ethereum transfer with a Fee sent to a third address and stores a reference */ -contract EthereumFeeProxy { +contract EthereumFeeProxy is ReentrancyGuard{ // Event to declare a transfer with a reference event TransferWithReferenceAndFee( address to, @@ -15,12 +17,12 @@ contract EthereumFeeProxy { address feeAddress ); - // Fallback function returns funds to the sender receive() external payable { revert("not payable receive"); } + /** * @notice Performs an Ethereum transfer with a reference * @param _to Transfer recipient @@ -37,8 +39,40 @@ contract EthereumFeeProxy { external payable { - _to.transfer(msg.value - _feeAmount); + transferExactEthWithReferenceAndFee( + _to, + msg.value - _feeAmount, + _paymentReference, + _feeAmount, + _feeAddress + ); + } + + + /** + * @notice Performs an Ethereum transfer with a reference with an exact amount of eth + * @param _to Transfer recipient + * @param _amount Amount to transfer + * @param _paymentReference Reference of the payment related + * @param _feeAmount The amount of the payment fee (part of the msg.value) + * @param _feeAddress The fee recipient + */ + function transferExactEthWithReferenceAndFee( + address payable _to, + uint256 _amount, + bytes calldata _paymentReference, + uint256 _feeAmount, + address payable _feeAddress + ) + nonReentrant + public + payable + { + _to.transfer(_amount); _feeAddress.transfer(_feeAmount); - emit TransferWithReferenceAndFee(_to, msg.value - _feeAmount, _paymentReference, _feeAmount, _feeAddress); + // transfer the remaining ethers to the sender + payable(msg.sender).transfer(msg.value - _amount - _feeAmount); + + emit TransferWithReferenceAndFee(_to, _amount, _paymentReference, _feeAmount, _feeAddress); } -} +} \ No newline at end of file diff --git a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/0.1.0.json index 3de97b349e..bc764a7465 100644 --- a/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/EthereumFeeProxy/0.1.0.json @@ -37,6 +37,39 @@ "name": "TransferWithReferenceAndFee", "type": "event" }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_feeAmount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "_feeAddress", + "type": "address" + } + ], + "name": "transferExactEthWithReferenceAndFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/smart-contracts/test/contracts/EthereumFeeProxy.test.ts b/packages/smart-contracts/test/contracts/EthereumFeeProxy.test.ts index 580e341c43..4633effcde 100644 --- a/packages/smart-contracts/test/contracts/EthereumFeeProxy.test.ts +++ b/packages/smart-contracts/test/contracts/EthereumFeeProxy.test.ts @@ -45,6 +45,27 @@ describe('contract: EthereumFeeProxy', () => { expect(contractBalance.toString()).to.equals("0"); }); + it('allows to pays exact eth with a reference with extra msg.value', async () => { + const toOldBalance = await provider.getBalance(to); + const feeAddressOldBalance = await provider.getBalance(feeAddress); + + await expect( + ethFeeProxy.transferExactEthWithReferenceAndFee(to, amount, referenceExample, feeAmount, feeAddress, { + value: amount.add(feeAmount).add('10000'), + }), + ).to.emit(ethFeeProxy, 'TransferWithReferenceAndFee') + .withArgs(to, amount.toString(), ethers.utils.keccak256(referenceExample), feeAmount.toString(), feeAddress); + + const toNewBalance = await provider.getBalance(to); + const feeAddressNewBalance = await provider.getBalance(feeAddress); + const contractBalance = await provider.getBalance(ethFeeProxy.address); + + // Check balance changes + expect(toNewBalance.toString()).to.equals(toOldBalance.add(amount).toString()); + expect(feeAddressNewBalance.toString()).to.equals(feeAddressOldBalance.add(feeAmount).toString()); + expect(contractBalance.toString()).to.equals("0"); + }); + it('cannot transfer if msg.value is too low', async () => { await expect( ethFeeProxy.transferWithReferenceAndFee(to, referenceExample, amount, feeAddress, {