Skip to content
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

feat: create deploySingleRequestProxy functionality in the sdk #1474

Merged
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 Oct 22, 2024
489d89b
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun Oct 22, 2024
6ef577b
fix: update `SingleRequestFactory` abi
aimensahnoun Oct 23, 2024
05c1d5c
fix: read proxy address from event correctly
aimensahnoun Oct 23, 2024
de3492a
feat: update `inMemoryRequest` to work with `SingleRequestProxy`
aimensahnoun Oct 23, 2024
e907a79
feat: add method to execute payments through SingleRequestProxy
aimensahnoun Oct 23, 2024
d5dacc8
test: add initial test
aimensahnoun Oct 25, 2024
85563be
feat: add `SingleRequestProxyFactory` to local deployment
aimensahnoun Oct 25, 2024
4024f7b
feat: add artifact information for private network
aimensahnoun Oct 25, 2024
19768f2
test: update tests to use private network
aimensahnoun Oct 25, 2024
0068fb8
fix: format issue
aimensahnoun Oct 25, 2024
085dc4c
chore: remove single request proxy tests
aimensahnoun Oct 25, 2024
eec0486
chore: remove single request proxy factory deployment from private ne…
aimensahnoun Oct 27, 2024
6305682
refactor: update test deployment order
aimensahnoun Oct 27, 2024
f915c61
test: initiale single request proxy test
aimensahnoun Oct 27, 2024
df6af29
fix: fix valid request object
aimensahnoun Oct 27, 2024
8df842f
test: full test suite for `deploySingleRequestProxy` for Ethereum proxy
aimensahnoun Oct 28, 2024
1fd4723
test: add "ERC20SingleRequestProxy" test
aimensahnoun Oct 28, 2024
ff74af2
feat: add more checks to validate singleRequestProxy
aimensahnoun Oct 28, 2024
7a0cebf
test: add tests for paying with single request proxy
aimensahnoun Oct 28, 2024
635cc6f
fix: ERC20 payment test
aimensahnoun Oct 28, 2024
0029ac4
fix: use paymentRecipient instead of payee identity address
aimensahnoun Oct 28, 2024
485d6b4
Retrigger CI
aimensahnoun Oct 28, 2024
954e443
fix: fix payee test
aimensahnoun Oct 28, 2024
5ca23a8
refactor: update single request proxy SDK
aimensahnoun Oct 28, 2024
b0ec7c8
fix: fix event handling
aimensahnoun Oct 28, 2024
57665b0
fix: wrong payment test
aimensahnoun Oct 28, 2024
9cef13e
refactor: update tests ot use new event format
aimensahnoun Oct 28, 2024
d637a93
Merge branch 'master' into 1396-support-singlerequestproxy-in-the-req…
aimensahnoun Oct 28, 2024
41d2499
refactor: split payment funciton into smaller functions
aimensahnoun Oct 29, 2024
47afd3b
Merge branch '1396-support-singlerequestproxy-in-the-request-network-…
aimensahnoun Oct 29, 2024
958ebf1
chore: add comments
aimensahnoun Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/payment-processor/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
271 changes: 271 additions & 0 deletions packages/payment-processor/src/payment/single-request-proxy.ts
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,
);

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<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> {
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);
}
}
Loading