-
Notifications
You must be signed in to change notification settings - Fork 91
feat: create deploySingleRequestProxy functionality in the sdk
#1474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
aimensahnoun
merged 32 commits into
master
from
1396-support-singlerequestproxy-in-the-request-network-sdk
Oct 30, 2024
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
8fee6fe
feat: create `deploySingleRequestProxy` functionality in the sdk
aimensahnoun 489d89b
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun 6ef577b
fix: update `SingleRequestFactory` abi
aimensahnoun 05c1d5c
fix: read proxy address from event correctly
aimensahnoun de3492a
feat: update `inMemoryRequest` to work with `SingleRequestProxy`
aimensahnoun e907a79
feat: add method to execute payments through SingleRequestProxy
aimensahnoun d5dacc8
test: add initial test
aimensahnoun 85563be
feat: add `SingleRequestProxyFactory` to local deployment
aimensahnoun 4024f7b
feat: add artifact information for private network
aimensahnoun 19768f2
test: update tests to use private network
aimensahnoun 0068fb8
fix: format issue
aimensahnoun 085dc4c
chore: remove single request proxy tests
aimensahnoun eec0486
chore: remove single request proxy factory deployment from private ne…
aimensahnoun 6305682
refactor: update test deployment order
aimensahnoun f915c61
test: initiale single request proxy test
aimensahnoun df6af29
fix: fix valid request object
aimensahnoun 8df842f
test: full test suite for `deploySingleRequestProxy` for Ethereum proxy
aimensahnoun 1fd4723
test: add "ERC20SingleRequestProxy" test
aimensahnoun ff74af2
feat: add more checks to validate singleRequestProxy
aimensahnoun 7a0cebf
test: add tests for paying with single request proxy
aimensahnoun 635cc6f
fix: ERC20 payment test
aimensahnoun 0029ac4
fix: use paymentRecipient instead of payee identity address
aimensahnoun 485d6b4
Retrigger CI
aimensahnoun 954e443
fix: fix payee test
aimensahnoun 5ca23a8
refactor: update single request proxy SDK
aimensahnoun b0ec7c8
fix: fix event handling
aimensahnoun 57665b0
fix: wrong payment test
aimensahnoun 9cef13e
refactor: update tests ot use new event format
aimensahnoun d637a93
Merge branch 'master' into 1396-support-singlerequestproxy-in-the-req…
aimensahnoun 41d2499
refactor: split payment funciton into smaller functions
aimensahnoun 47afd3b
Merge branch '1396-support-singlerequestproxy-in-the-request-network-…
aimensahnoun 958ebf1
chore: add comments
aimensahnoun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
271 changes: 271 additions & 0 deletions
271
packages/payment-processor/src/payment/single-request-proxy.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string> { | ||
| 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, | ||
| ); | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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(); | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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'); | ||
| } | ||
MantisClone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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<void> { | ||
| 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<void> { | ||
| 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<void> { | ||
| 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<void> { | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
aimensahnoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (isERC20) { | ||
| await payWithERC20SingleRequestProxy(singleRequestProxyAddress, signer, amount); | ||
| } else { | ||
| await payWithEthereumSingleRequestProxy(singleRequestProxyAddress, signer, amount); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.