From e91492cdc93b4ab3c3d7c5113e9dff225b4ba129 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Wed, 30 Oct 2024 14:19:46 +0300 Subject: [PATCH] feat: create `deploySingleRequestProxy` functionality in the sdk (#1474) --- packages/payment-processor/src/index.ts | 2 + .../src/payment/single-request-proxy.ts | 271 ++++++++++++++ .../test/payment/single-request-proxy.test.ts | 340 ++++++++++++++++++ .../src/api/request-network.ts | 25 +- .../scripts/test-deploy-all.ts | 2 + .../test-deploy-single-request-proxy.ts | 33 ++ .../contracts/SingleRequestProxyFactory.sol | 8 +- .../SingleRequestProxyFactory/0.1.0.json | 284 +++++++++++++++ .../SingleRequestProxyFactory/index.ts | 24 ++ .../src/lib/artifacts/index.ts | 1 + 10 files changed, 972 insertions(+), 18 deletions(-) create mode 100644 packages/payment-processor/src/payment/single-request-proxy.ts create mode 100644 packages/payment-processor/test/payment/single-request-proxy.test.ts create mode 100644 packages/smart-contracts/scripts/test-deploy-single-request-proxy.ts create mode 100644 packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/index.ts diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index 00779e34bf..178c46ec28 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -27,6 +27,8 @@ export * from './payment/encoder-approval'; export * as Escrow from './payment/erc20-escrow-payment'; export * from './payment/prepared-transaction'; export * from './payment/utils-near'; +export * from './payment/single-request-proxy'; + import * as utils from './payment/utils'; export { utils }; diff --git a/packages/payment-processor/src/payment/single-request-proxy.ts b/packages/payment-processor/src/payment/single-request-proxy.ts new file mode 100644 index 0000000000..eb5e4f62b0 --- /dev/null +++ b/packages/payment-processor/src/payment/single-request-proxy.ts @@ -0,0 +1,271 @@ +import { Contract, Signer, ethers } from 'ethers'; +import { + PaymentReferenceCalculator, + getPaymentNetworkExtension, +} from '@requestnetwork/payment-detection'; +import { ClientTypes, ExtensionTypes, CurrencyTypes } from '@requestnetwork/types'; +import { singleRequestProxyFactoryArtifact } from '@requestnetwork/smart-contracts'; +import { IERC20__factory } from '@requestnetwork/smart-contracts/types'; + +/** + * Deploys a Single Request Proxy contract for a given request. + * + * @param request - The request data object containing payment network and currency information. + * @param signer - The Ethereum signer used to deploy the contract. + * @returns A Promise that resolves to the address of the deployed Single Request Proxy contract. + * @throws {Error} If the payment network is unsupported, payment chain is not found, payee is not found, or if there are invalid payment network values. + * + * @remarks + * This function supports deploying proxies for ERC20_FEE_PROXY_CONTRACT and ETH_FEE_PROXY_CONTRACT payment networks. + * It uses the SingleRequestProxyFactory contract to create either an ERC20 or Ethereum Single Request Proxy. + * The function calculates the payment reference and handles the deployment transaction, including waiting for confirmation. + * The factory address is automatically determined based on the payment chain using the singleRequestProxyFactoryArtifact. + */ +export async function deploySingleRequestProxy( + request: ClientTypes.IRequestData, + signer: Signer, +): Promise { + const requestPaymentNetwork = getPaymentNetworkExtension(request); + + // Check if the payment network is supported, only ERC20_FEE_PROXY_CONTRACT and ETH_FEE_PROXY_CONTRACT are supported + if ( + !requestPaymentNetwork || + (requestPaymentNetwork.id !== ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT && + requestPaymentNetwork.id !== ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT) + ) { + throw new Error('Unsupported payment network'); + } + + const paymentChain = request.currencyInfo.network; + if (!paymentChain) { + throw new Error('Payment chain not found'); + } + + // Use artifact's default address for the payment chain + const singleRequestProxyFactory = singleRequestProxyFactoryArtifact.connect( + paymentChain as CurrencyTypes.EvmChainName, + signer, + ); + + if (!singleRequestProxyFactory.address) { + throw new Error(`SingleRequestProxyFactory not found on chain ${paymentChain}`); + } + + const salt = requestPaymentNetwork?.values?.salt; + const feeAddress = requestPaymentNetwork?.values?.feeAddress; + const feeAmount = requestPaymentNetwork?.values?.feeAmount; + const paymentRecipient = requestPaymentNetwork?.values?.paymentAddress; + + if (!salt || !feeAddress || !feeAmount || !paymentRecipient) { + throw new Error('Invalid payment network values'); + } + + const paymentReference = `0x${PaymentReferenceCalculator.calculate( + request.requestId, + salt, + paymentRecipient, + )}`; + + const isERC20 = + requestPaymentNetwork.id === ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT; + + let tx; + + if (isERC20) { + const tokenAddress = request.currencyInfo.value; + tx = await singleRequestProxyFactory.createERC20SingleRequestProxy( + paymentRecipient, + tokenAddress, + paymentReference, + feeAddress, + feeAmount, + ); + } else { + tx = await singleRequestProxyFactory.createEthereumSingleRequestProxy( + paymentRecipient, + paymentReference, + feeAddress, + feeAmount, + ); + } + + const receipt = await tx.wait(); + + const event = receipt.events?.[0]; + + if (!event) { + throw new Error('Single request proxy creation event not found'); + } + + const proxyAddress = ethers.utils.defaultAbiCoder.decode(['address', 'address'], event.data)[0]; + + if (!proxyAddress) { + throw new Error('Proxy address not found in event data'); + } + + return proxyAddress; +} + +/** + * Validates that a contract is a SingleRequestProxy by checking required methods + * @param proxyAddress - The address of the contract to validate + * @param signer - The Ethereum signer used to interact with the contract + * @throws {Error} If the contract is not a valid SingleRequestProxy + */ +async function validateSingleRequestProxy(proxyAddress: string, signer: Signer): Promise { + const proxyInterface = new ethers.utils.Interface([ + 'function payee() view returns (address)', + 'function paymentReference() view returns (bytes)', + 'function feeAddress() view returns (address)', + 'function feeAmount() view returns (uint256)', + ]); + + const proxyContract = new Contract(proxyAddress, proxyInterface, signer); + + try { + await Promise.all([ + proxyContract.payee(), + proxyContract.paymentReference(), + proxyContract.feeAddress(), + proxyContract.feeAmount(), + ]); + } catch (error) { + throw new Error('Invalid SingleRequestProxy contract'); + } +} + +/** + * Executes a payment through an ERC20SingleRequestProxy contract + * @param proxyAddress - The address of the SingleRequestProxy contract + * @param signer - The Ethereum signer used to execute the payment transaction + * @param amount - The amount to be paid + * @throws {Error} If the contract is not an ERC20SingleRequestProxy + */ +export async function payWithERC20SingleRequestProxy( + proxyAddress: string, + signer: Signer, + amount: string, +): Promise { + if (!amount || ethers.BigNumber.from(amount).lte(0)) { + throw new Error('Amount must be a positive number'); + } + + const proxyInterface = new ethers.utils.Interface([ + 'function tokenAddress() view returns (address)', + ]); + + const proxyContract = new Contract(proxyAddress, proxyInterface, signer); + + let tokenAddress: string; + try { + // Attempt to fetch the token address from the proxy contract, to determine if it's an ERC20 SingleRequestProxy. + tokenAddress = await proxyContract.tokenAddress(); + } catch { + throw new Error('Contract is not an ERC20SingleRequestProxy'); + } + + const erc20Contract = IERC20__factory.connect(tokenAddress, signer); + + // Transfer tokens to the proxy + const transferTx = await erc20Contract.transfer(proxyAddress, amount); + await transferTx.wait(); + + // Trigger the proxy's receive function to finalize payment + const triggerTx = await signer.sendTransaction({ + to: proxyAddress, + value: ethers.constants.Zero, + }); + await triggerTx.wait(); +} + +/** + * Executes a payment through an EthereumSingleRequestProxy contract + * @param proxyAddress - The address of the SingleRequestProxy contract + * @param signer - The Ethereum signer used to execute the payment transaction + * @param amount - The amount to be paid + * @throws {Error} If the contract is an ERC20SingleRequestProxy + */ +export async function payWithEthereumSingleRequestProxy( + proxyAddress: string, + signer: Signer, + amount: string, +): Promise { + if (!amount || ethers.BigNumber.from(amount).lte(0)) { + throw new Error('Amount must be a positive number'); + } + + const proxyInterface = new ethers.utils.Interface([ + 'function tokenAddress() view returns (address)', + ]); + + const proxyContract = new Contract(proxyAddress, proxyInterface, signer); + + try { + // Attempt to fetch the token address from the proxy contract, to determine if it's an Ethereum SingleRequestProxy. + await proxyContract.tokenAddress(); + + // If the token address is fetched, it means the contract is an ERC20SingleRequestProxy. + throw new Error('Contract is not an EthereumSingleRequestProxy'); + } catch (error) { + // If the token address is not fetched, it means the contract is an EthereumSingleRequestProxy. + if (error.message === 'Contract is not an EthereumSingleRequestProxy') { + // If the error message is 'Contract is not an EthereumSingleRequestProxy', throw the error. + throw error; + } + } + + const tx = await signer.sendTransaction({ + to: proxyAddress, + value: amount, + }); + await tx.wait(); +} + +/** + * Executes a payment through a Single Request Proxy contract. + * + * @param singleRequestProxyAddress - The address of the deployed Single Request Proxy contract. + * @param signer - The Ethereum signer used to execute the payment transaction. + * @param amount - The amount to be paid, as a string representation of the value. + * @returns A Promise that resolves when the payment transaction is confirmed. + * @throws {Error} If the SingleRequestProxy contract is invalid. + * @throws {Error} If the proxy contract type cannot be determined, or if any transaction fails. + * + * @remarks + * This function supports both ERC20 and Ethereum payments. + * For ERC20 payments, it first transfers the tokens to the proxy contract and then triggers the payment. + * For Ethereum payments, it directly sends the Ether to the proxy contract. + * The function automatically detects whether the proxy is for ERC20 or Ethereum based on the contract interface. + */ +export async function payRequestWithSingleRequestProxy( + singleRequestProxyAddress: string, + signer: Signer, + amount: string, +): Promise { + if (!amount || ethers.BigNumber.from(amount).lte(0)) { + throw new Error('Amount must be a positive number'); + } + + // Validate the SingleRequestProxy contract + await validateSingleRequestProxy(singleRequestProxyAddress, signer); + + const proxyInterface = new ethers.utils.Interface([ + 'function tokenAddress() view returns (address)', + ]); + + const proxyContract = new Contract(singleRequestProxyAddress, proxyInterface, signer); + + let isERC20: boolean; + try { + await proxyContract.tokenAddress(); + isERC20 = true; + } catch { + isERC20 = false; + } + + if (isERC20) { + await payWithERC20SingleRequestProxy(singleRequestProxyAddress, signer, amount); + } else { + await payWithEthereumSingleRequestProxy(singleRequestProxyAddress, signer, amount); + } +} diff --git a/packages/payment-processor/test/payment/single-request-proxy.test.ts b/packages/payment-processor/test/payment/single-request-proxy.test.ts new file mode 100644 index 0000000000..6f5cf0f27c --- /dev/null +++ b/packages/payment-processor/test/payment/single-request-proxy.test.ts @@ -0,0 +1,340 @@ +import { singleRequestProxyFactoryArtifact } from '@requestnetwork/smart-contracts'; +import { TestERC20__factory } from '@requestnetwork/smart-contracts/types'; +import { + ClientTypes, + CurrencyTypes, + ExtensionTypes, + IdentityTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { providers, Wallet, utils } from 'ethers'; +import { + deploySingleRequestProxy, + payRequestWithSingleRequestProxy, + payWithEthereumSingleRequestProxy, + payWithERC20SingleRequestProxy, +} from '../../src/payment/single-request-proxy'; + +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const paymentAddress = '0x1234567890123456789012345678901234567890'; +const payerAddress = '0x91087544a744f5ffd8213323a36e073a13320714'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); +const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; + +const baseRequest: Omit< + ClientTypes.IRequestData, + 'currency' | 'currencyInfo' | 'extensions' | 'version' +> = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: paymentAddress, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: payerAddress, + }, + events: [], + expectedAmount: '100', + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: 'abcd', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, +}; + +const ethRequest: ClientTypes.IRequestData = { + ...baseRequest, + currency: 'ETH-private', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ETH, + value: RequestLogicTypes.CURRENCY.ETH, + }, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress: feeAddress, + feeAmount: '2', + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }, + version: '2.0.3', +}; + +const erc20Request: ClientTypes.IRequestData = { + ...baseRequest, + currency: 'DAI', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20, + value: erc20ContractAddress, + }, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress: feeAddress, + feeAmount: '2', + paymentAddress: paymentAddress, + salt: 'salt', + }, + version: '0.1.0', + }, + }, + version: '1.0', +}; + +describe('deploySingleRequestProxy', () => { + it('should throw error if payment network not supported', async () => { + // Create a request with an unsupported payment network + const invalidRequestUnsupportedPaymentNetwork = { + ...baseRequest, + currency: 'ETH-private', + currencyInfo: { + network: 'private' as CurrencyTypes.ChainName, + type: RequestLogicTypes.CURRENCY.ETH, + value: RequestLogicTypes.CURRENCY.ETH, + }, + version: '2.0.3', + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: {}, + version: '0.1.0', + }, + }, + }; + + await expect( + deploySingleRequestProxy(invalidRequestUnsupportedPaymentNetwork, wallet), + ).rejects.toThrow('Unsupported payment network'); + }); + + it('should throw error if request has no network', async () => { + const invalidRequestWithoutNetwork = { ...ethRequest, currencyInfo: {} }; + + // @ts-expect-error: Request with empty currencyInfo + await expect(deploySingleRequestProxy(invalidRequestWithoutNetwork, wallet)).rejects.toThrow( + 'Payment chain not found', + ); + }); + + it('should throw error if request has no network values', async () => { + const invalidRequestWithoutNetworkValues = { + ...ethRequest, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT]: { + ...ethRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT], + values: {}, + }, + }, + }; + + await expect( + deploySingleRequestProxy(invalidRequestWithoutNetworkValues, wallet), + ).rejects.toThrow('Invalid payment network values'); + }); + + it('should throw an error if the request has no extension', async () => { + const invalidRequestWithoutExtensions = { ...ethRequest, extensions: {} }; + + await expect(deploySingleRequestProxy(invalidRequestWithoutExtensions, wallet)).rejects.toThrow( + 'Unsupported payment network', + ); + }); + + it('should deploy EthereumSingleRequestProxy and emit event', async () => { + const singleRequestProxyFactory = singleRequestProxyFactoryArtifact.connect('private', wallet); + + const initialEventCount = await provider.getBlockNumber(); + + const walletAddress = await wallet.getAddress(); + + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + + expect(proxyAddress).toBeDefined(); + expect(typeof proxyAddress).toBe('string'); + expect(proxyAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); + + const latestBlock = await provider.getBlockNumber(); + const events = await singleRequestProxyFactory.queryFilter( + singleRequestProxyFactory.filters.EthereumSingleRequestProxyCreated(), + initialEventCount, + latestBlock, + ); + + expect(events.length).toBeGreaterThan(0); + + const eventData = utils.defaultAbiCoder.decode(['address', 'address'], events[0].data); + + expect(eventData[0]).toBe(proxyAddress); + }); + + it('should deploy ERC20SingleRequestProxy and emit event', async () => { + const singleRequestProxyFactory = singleRequestProxyFactoryArtifact.connect('private', wallet); + + const initialEventCount = await provider.getBlockNumber(); + + const walletAddress = await wallet.getAddress(); + + const proxyAddress = await deploySingleRequestProxy(erc20Request, wallet); + + expect(proxyAddress).toBeDefined(); + expect(typeof proxyAddress).toBe('string'); + expect(proxyAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); + + const latestBlock = await provider.getBlockNumber(); + const events = await singleRequestProxyFactory.queryFilter( + singleRequestProxyFactory.filters.ERC20SingleRequestProxyCreated(), + initialEventCount, + latestBlock, + ); + + expect(events.length).toBeGreaterThan(0); + + const eventData = utils.defaultAbiCoder.decode(['address', 'address'], events[0].data); + + expect(eventData[0]).toBe(proxyAddress); + }); + + it('should throw error when trying to pay with invalid single request proxy', async () => { + const invalidProxy = '0x1234567890123456789012345678901234567890'; + + await expect(payRequestWithSingleRequestProxy(invalidProxy, wallet, '100')).rejects.toThrow( + 'Invalid SingleRequestProxy contract', + ); + }); + + it('should throw error when amount is not a positive number', async () => { + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + + await expect(payRequestWithSingleRequestProxy(proxyAddress, wallet, '0')).rejects.toThrow( + 'Amount must be a positive number', + ); + }); + + it('should pay with EthereumSingleRequestProxy', async () => { + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + + const walletBalanceBefore = await provider.getBalance(wallet.address); + + await payRequestWithSingleRequestProxy(proxyAddress, wallet, '1000'); + + const walletBalanceAfter = await provider.getBalance(wallet.address); + + expect(walletBalanceAfter.toBigInt()).toBeLessThan(walletBalanceBefore.toBigInt()); + }); + + it('should pay with ERC20SingleRequestProxy', async () => { + const amount = '200'; + + const testERC20 = await new TestERC20__factory(wallet).deploy(1000); + + const updatedERC20Request = { + ...erc20Request, + currencyInfo: { ...erc20Request.currencyInfo, value: testERC20.address }, + }; + + const proxyAddress = await deploySingleRequestProxy(updatedERC20Request, wallet); + + const initialProxyBalance = await testERC20.balanceOf(wallet.address); + + await payRequestWithSingleRequestProxy(proxyAddress, wallet, amount); + + const finalProxyBalance = await testERC20.balanceOf(wallet.address); + + expect(finalProxyBalance.toBigInt()).toBeLessThan(initialProxyBalance.toBigInt()); + }); +}); + +describe('payWithEthereumSingleRequestProxy', () => { + it('should throw error when amount is not a positive number', async () => { + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + + await expect(payWithEthereumSingleRequestProxy(proxyAddress, wallet, '0')).rejects.toThrow( + 'Amount must be a positive number', + ); + }); + + it('should throw error when contract is an ERC20SingleRequestProxy', async () => { + const proxyAddress = await deploySingleRequestProxy(erc20Request, wallet); + + await expect(payWithEthereumSingleRequestProxy(proxyAddress, wallet, '1000')).rejects.toThrow( + 'Contract is not an EthereumSingleRequestProxy', + ); + }); + + it('should successfully pay with ETH', async () => { + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + const amount = '1000'; + + const walletBalanceBefore = await provider.getBalance(wallet.address); + + await payWithEthereumSingleRequestProxy(proxyAddress, wallet, amount); + + const walletBalanceAfter = await provider.getBalance(wallet.address); + + expect(walletBalanceAfter.toBigInt()).toBeLessThan(walletBalanceBefore.toBigInt()); + }); +}); + +describe('payWithERC20SingleRequestProxy', () => { + it('should throw error when amount is not a positive number', async () => { + const proxyAddress = await deploySingleRequestProxy(erc20Request, wallet); + + await expect(payWithERC20SingleRequestProxy(proxyAddress, wallet, '0')).rejects.toThrow( + 'Amount must be a positive number', + ); + }); + + it('should throw error when contract is not an ERC20SingleRequestProxy', async () => { + const proxyAddress = await deploySingleRequestProxy(ethRequest, wallet); + + await expect(payWithERC20SingleRequestProxy(proxyAddress, wallet, '1000')).rejects.toThrow( + 'Contract is not an ERC20SingleRequestProxy', + ); + }); + + it('should successfully pay with ERC20 tokens', async () => { + const amount = '200'; + const testERC20 = await new TestERC20__factory(wallet).deploy(1000); + + const updatedERC20Request = { + ...erc20Request, + currencyInfo: { ...erc20Request.currencyInfo, value: testERC20.address }, + }; + + const proxyAddress = await deploySingleRequestProxy(updatedERC20Request, wallet); + const initialBalance = await testERC20.balanceOf(wallet.address); + + await payWithERC20SingleRequestProxy(proxyAddress, wallet, amount); + + const finalBalance = await testERC20.balanceOf(wallet.address); + + expect(finalBalance.toBigInt()).toBeLessThan(initialBalance.toBigInt()); + }); +}); diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index cdadae3e79..267fb2d579 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -504,10 +504,9 @@ export default class RequestNetwork { requestId: string, ): ClientTypes.IRequestData { const requestData = JSON.parse(transactionData.data as string).data; - const originalExtensionsData = requestData.parameters.extensionsData; const newExtensions: RequestLogicTypes.IExtensionStates = {}; - for (const extension of originalExtensionsData) { + for (const extension of requestData.parameters.extensionsData) { if (extension.id !== ExtensionTypes.OTHER_ID.CONTENT_DATA) { newExtensions[extension.id] = { events: [ @@ -516,6 +515,8 @@ export default class RequestNetwork { parameters: { paymentAddress: extension.parameters.paymentAddress, salt: extension.parameters.salt, + feeAddress: extension.parameters.feeAddress, + feeAmount: extension.parameters.feeAmount, }, timestamp: requestData.parameters.timestamp, }, @@ -529,6 +530,8 @@ export default class RequestNetwork { sentPaymentAmount: '0', sentRefundAmount: '0', paymentAddress: extension.parameters.paymentAddress, + feeAddress: extension.parameters.feeAddress, + feeAmount: extension.parameters.feeAmount, }, version: extension.version, }; @@ -536,27 +539,21 @@ export default class RequestNetwork { } return { - requestId: requestId, - currency: requestData.parameters.currency.type, + ...requestData.parameters, + requestId, meta: null, balance: null, - expectedAmount: requestData.parameters.expectedAmount, - contentData: requestData.parameters.extensionsData.find( - (ext: ExtensionTypes.IAction) => ext.id === ExtensionTypes.OTHER_ID.CONTENT_DATA, - )?.parameters.content, + currency: requestData.parameters.currency.type, currencyInfo: { type: requestData.parameters.currency.type, network: requestData.parameters.currency.network, value: requestData.parameters.currency.value || '', }, + contentData: requestData.parameters.extensionsData.find( + (ext: ExtensionTypes.IAction) => ext.id === ExtensionTypes.OTHER_ID.CONTENT_DATA, + )?.parameters.content, pending: null, extensions: newExtensions, - extensionsData: requestData.parameters.extensionsData, - timestamp: requestData.parameters.timestamp, - version: requestData.parameters.version, - creator: requestData.parameters.creator, - state: requestData.parameters.state, - events: requestData.parameters.events, }; } } diff --git a/packages/smart-contracts/scripts/test-deploy-all.ts b/packages/smart-contracts/scripts/test-deploy-all.ts index 567b0ccc2c..f1a6807510 100644 --- a/packages/smart-contracts/scripts/test-deploy-all.ts +++ b/packages/smart-contracts/scripts/test-deploy-all.ts @@ -7,6 +7,7 @@ import { deployBatchPayment } from './test-deploy-batch-erc-eth-deployment'; import { deploySuperFluid } from './test-deploy-superfluid'; import { deployBatchConversionPayment } from './test-deploy-batch-conversion-deployment'; import { deployERC20TransferableReceivable } from './test-deploy-erc20-transferable-receivable'; +import { deploySingleRequestProxyFactory } from './test-deploy-single-request-proxy'; // Deploys, set up the contracts export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment): Promise { @@ -18,4 +19,5 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); await deployERC20TransferableReceivable(_args, hre, mainPaymentAddresses); + await deploySingleRequestProxyFactory(_args, hre, mainPaymentAddresses); } diff --git a/packages/smart-contracts/scripts/test-deploy-single-request-proxy.ts b/packages/smart-contracts/scripts/test-deploy-single-request-proxy.ts new file mode 100644 index 0000000000..ddeade9201 --- /dev/null +++ b/packages/smart-contracts/scripts/test-deploy-single-request-proxy.ts @@ -0,0 +1,33 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; + +interface FeeProxyAddresses { + ERC20FeeProxyAddress: string; + ETHFeeProxyAddress: string; +} + +export async function deploySingleRequestProxyFactory( + args: any, + hre: HardhatRuntimeEnvironment, + feeProxyAddresses: FeeProxyAddresses, +) { + try { + const [deployer] = await hre.ethers.getSigners(); + const { address: SingleRequestProxyFactoryAddress } = await deployOne( + args, + hre, + 'SingleRequestProxyFactory', + { + constructorArguments: [ + feeProxyAddresses.ETHFeeProxyAddress, + feeProxyAddresses.ERC20FeeProxyAddress, + deployer.address, + ], + }, + ); + + console.log(`SingleRequestProxyFactory Contract deployed: ${SingleRequestProxyFactoryAddress}`); + } catch (e) { + console.error(e); + } +} diff --git a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol index 441e3739ef..6ac89e9d9b 100644 --- a/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol +++ b/packages/smart-contracts/src/contracts/SingleRequestProxyFactory.sol @@ -19,14 +19,14 @@ contract SingleRequestProxyFactory is Ownable { address public erc20FeeProxy; event EthereumSingleRequestProxyCreated( - address indexed proxyAddress, - address indexed payee, + address proxyAddress, + address payee, bytes indexed paymentReference ); event ERC20SingleRequestProxyCreated( - address indexed proxyAddress, - address indexed payee, + address proxyAddress, + address payee, address tokenAddress, bytes indexed paymentReference ); diff --git a/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/0.1.0.json new file mode 100644 index 0000000000..59d1284fbb --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/0.1.0.json @@ -0,0 +1,284 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_ethereumFeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_erc20FeeProxy", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newERC20FeeProxy", + "type": "address" + } + ], + "name": "ERC20FeeProxyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proxyAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "payee", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "ERC20SingleRequestProxyCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newEthereumFeeProxy", + "type": "address" + } + ], + "name": "EthereumFeeProxyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proxyAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "payee", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "EthereumSingleRequestProxyCreated", + "type": "event" + }, + { + "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": "_payee", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_paymentReference", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_feeAmount", + "type": "uint256" + } + ], + "name": "createERC20SingleRequestProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_payee", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_paymentReference", + "type": "bytes" + }, + { + "internalType": "address", + "name": "_feeAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_feeAmount", + "type": "uint256" + } + ], + "name": "createEthereumSingleRequestProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "erc20FeeProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ethereumFeeProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newERC20FeeProxy", + "type": "address" + } + ], + "name": "setERC20FeeProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newEthereumFeeProxy", + "type": "address" + } + ], + "name": "setEthereumFeeProxy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/index.ts b/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/index.ts new file mode 100644 index 0000000000..43493b04b3 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/SingleRequestProxyFactory/index.ts @@ -0,0 +1,24 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; + +import type { SingleRequestProxyFactory } from '../../../types'; + +export const singleRequestProxyFactoryArtifact = new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0x9d075ae44D859191C121d7522da0Cc3B104b8837', + creationBlockNumber: 0, + }, + sepolia: { + address: '0x435E81E12136414e2c09cAFe05E902E23bD46030', + creationBlockNumber: 6965557, + }, + }, + }, + }, + '0.1.0', +); diff --git a/packages/smart-contracts/src/lib/artifacts/index.ts b/packages/smart-contracts/src/lib/artifacts/index.ts index 280f1b04a9..20597971ad 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -14,6 +14,7 @@ export * from './ERC20EscrowToPay'; export * from './BatchPayments'; export * from './BatchNoConversionPayments'; export * from './BatchConversionPayments'; +export * from './SingleRequestProxyFactory'; /** * Request Storage */