|
| 1 | +import { CurrencyTypes, PaymentTypes } from '@requestnetwork/types'; |
| 2 | +import { providers, Signer, BigNumberish } from 'ethers'; |
| 3 | +import { erc20RecurringPaymentProxyArtifact } from '@requestnetwork/smart-contracts'; |
| 4 | +import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; |
| 5 | +import { getErc20Allowance } from './erc20'; |
| 6 | + |
| 7 | +/** |
| 8 | + * Retrieves the current ERC-20 allowance that a subscriber (`payerAddress`) has |
| 9 | + * granted to the `ERC20RecurringPaymentProxy` on a specific network. |
| 10 | + * |
| 11 | + * @param payerAddress - Address of the token owner (subscriber) whose allowance is queried. |
| 12 | + * @param tokenAddress - Address of the ERC-20 token involved in the recurring payment schedule. |
| 13 | + * @param provider - A Web3 provider or signer used to perform the on-chain call. |
| 14 | + * @param network - The EVM chain name (e.g. `'mainnet'`, `'goerli'`, `'matic'`). |
| 15 | + * |
| 16 | + * @returns A Promise that resolves to the allowance **as a decimal string** (same |
| 17 | + * units as `token.decimals`). An empty allowance is returned as `"0"`. |
| 18 | + * |
| 19 | + * @throws {Error} If the `ERC20RecurringPaymentProxy` has no known deployment |
| 20 | + * on the provided `network`.. |
| 21 | + */ |
| 22 | +export async function getPayerRecurringPaymentAllowance({ |
| 23 | + payerAddress, |
| 24 | + tokenAddress, |
| 25 | + provider, |
| 26 | + network, |
| 27 | +}: { |
| 28 | + payerAddress: string; |
| 29 | + tokenAddress: string; |
| 30 | + provider: Signer | providers.Provider; |
| 31 | + network: CurrencyTypes.EvmChainName; |
| 32 | +}): Promise<string> { |
| 33 | + const erc20RecurringPaymentProxy = erc20RecurringPaymentProxyArtifact.connect(network, provider); |
| 34 | + |
| 35 | + if (!erc20RecurringPaymentProxy.address) { |
| 36 | + throw new Error(`ERC20RecurringPaymentProxy not found on ${network}`); |
| 37 | + } |
| 38 | + |
| 39 | + const allowance = await getErc20Allowance( |
| 40 | + payerAddress, |
| 41 | + erc20RecurringPaymentProxy.address, |
| 42 | + provider, |
| 43 | + tokenAddress, |
| 44 | + ); |
| 45 | + |
| 46 | + return allowance.toString(); |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Encodes the transaction data to set the allowance for the ERC20RecurringPaymentProxy. |
| 51 | + * |
| 52 | + * @param tokenAddress - The ERC20 token contract address |
| 53 | + * @param amount - The amount to approve, as a BigNumberish value |
| 54 | + * @param provider - Web3 provider or signer to interact with the blockchain |
| 55 | + * @param network - The EVM chain name where the proxy is deployed |
| 56 | + * @param isUSDT - Flag to indicate if the token is USDT, which requires special handling |
| 57 | + * |
| 58 | + * @returns Array of transaction objects ready to be sent to the blockchain |
| 59 | + * |
| 60 | + * @throws {Error} If the ERC20RecurringPaymentProxy is not deployed on the specified network |
| 61 | + * |
| 62 | + * @remarks |
| 63 | + * • For USDT, it returns two transactions: approve(0) and then approve(amount) |
| 64 | + * • For other ERC20 tokens, it returns a single approve(amount) transaction |
| 65 | + */ |
| 66 | +export function encodeSetRecurringAllowance({ |
| 67 | + tokenAddress, |
| 68 | + amount, |
| 69 | + provider, |
| 70 | + network, |
| 71 | + isUSDT = false, |
| 72 | +}: { |
| 73 | + tokenAddress: string; |
| 74 | + amount: BigNumberish; |
| 75 | + provider: providers.Provider | Signer; |
| 76 | + network: CurrencyTypes.EvmChainName; |
| 77 | + isUSDT?: boolean; |
| 78 | +}): Array<{ to: string; data: string; value: number }> { |
| 79 | + const erc20RecurringPaymentProxy = erc20RecurringPaymentProxyArtifact.connect(network, provider); |
| 80 | + |
| 81 | + if (!erc20RecurringPaymentProxy.address) { |
| 82 | + throw new Error(`ERC20RecurringPaymentProxy not found on ${network}`); |
| 83 | + } |
| 84 | + |
| 85 | + const paymentTokenContract = ERC20__factory.connect(tokenAddress, provider); |
| 86 | + |
| 87 | + const transactions: Array<{ to: string; data: string; value: number }> = []; |
| 88 | + |
| 89 | + if (isUSDT) { |
| 90 | + const resetData = paymentTokenContract.interface.encodeFunctionData('approve', [ |
| 91 | + erc20RecurringPaymentProxy.address, |
| 92 | + 0, |
| 93 | + ]); |
| 94 | + transactions.push({ to: tokenAddress, data: resetData, value: 0 }); |
| 95 | + } |
| 96 | + |
| 97 | + const setData = paymentTokenContract.interface.encodeFunctionData('approve', [ |
| 98 | + erc20RecurringPaymentProxy.address, |
| 99 | + amount, |
| 100 | + ]); |
| 101 | + transactions.push({ to: tokenAddress, data: setData, value: 0 }); |
| 102 | + |
| 103 | + return transactions; |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Encodes the transaction data to trigger a recurring payment through the ERC20RecurringPaymentProxy. |
| 108 | + * |
| 109 | + * @param permitTuple - The SchedulePermit struct data |
| 110 | + * @param permitSignature - The signature authorizing the recurring payment schedule |
| 111 | + * @param paymentIndex - The index of the payment to trigger (1-based) |
| 112 | + * @param paymentReference - Reference data for the payment |
| 113 | + * @param network - The EVM chain name where the proxy is deployed |
| 114 | + * |
| 115 | + * @returns The encoded function data as a hex string, ready to be used in a transaction |
| 116 | + * |
| 117 | + * @throws {Error} If the ERC20RecurringPaymentProxy is not deployed on the specified network |
| 118 | + * |
| 119 | + * @remarks |
| 120 | + * • The function only encodes the transaction data without sending it |
| 121 | + * • The encoded data can be used with any web3 library or multisig wallet |
| 122 | + * • Make sure the paymentIndex matches the expected payment sequence |
| 123 | + */ |
| 124 | +export function encodeRecurringPaymentTrigger({ |
| 125 | + permitTuple, |
| 126 | + permitSignature, |
| 127 | + paymentIndex, |
| 128 | + paymentReference, |
| 129 | + network, |
| 130 | + provider, |
| 131 | +}: { |
| 132 | + permitTuple: PaymentTypes.SchedulePermit; |
| 133 | + permitSignature: string; |
| 134 | + paymentIndex: number; |
| 135 | + paymentReference: string; |
| 136 | + network: CurrencyTypes.EvmChainName; |
| 137 | + provider: providers.Provider | Signer; |
| 138 | +}): string { |
| 139 | + const proxyContract = erc20RecurringPaymentProxyArtifact.connect(network, provider); |
| 140 | + |
| 141 | + return proxyContract.interface.encodeFunctionData('triggerRecurringPayment', [ |
| 142 | + permitTuple, |
| 143 | + permitSignature, |
| 144 | + paymentIndex, |
| 145 | + paymentReference, |
| 146 | + ]); |
| 147 | +} |
| 148 | + |
| 149 | +/** |
| 150 | + * Triggers a recurring payment through the ERC20RecurringPaymentProxy. |
| 151 | + * |
| 152 | + * @param permitTuple - The SchedulePermit struct data |
| 153 | + * @param permitSignature - The signature authorizing the recurring payment schedule |
| 154 | + * @param paymentIndex - The index of the payment to trigger (1-based) |
| 155 | + * @param paymentReference - Reference data for the payment |
| 156 | + * @param signer - The signer that will trigger the transaction (must have RELAYER_ROLE) |
| 157 | + * @param network - The EVM chain name where the proxy is deployed |
| 158 | + * |
| 159 | + * @returns A Promise resolving to the transaction response (TransactionResponse) |
| 160 | + * |
| 161 | + * @throws {Error} If the ERC20RecurringPaymentProxy is not deployed on the specified network |
| 162 | + * @throws {Error} If the transaction fails (e.g. wrong index, expired permit, insufficient allowance) |
| 163 | + * |
| 164 | + * @remarks |
| 165 | + * • The function returns the transaction response immediately after sending |
| 166 | + * • The signer must have been granted RELAYER_ROLE by the proxy admin |
| 167 | + * • Make sure all preconditions are met (allowance, balance, timing) before calling |
| 168 | + * • To wait for confirmation, call tx.wait() on the returned TransactionResponse |
| 169 | + */ |
| 170 | +export async function triggerRecurringPayment({ |
| 171 | + permitTuple, |
| 172 | + permitSignature, |
| 173 | + paymentIndex, |
| 174 | + paymentReference, |
| 175 | + signer, |
| 176 | + network, |
| 177 | +}: { |
| 178 | + permitTuple: PaymentTypes.SchedulePermit; |
| 179 | + permitSignature: string; |
| 180 | + paymentIndex: number; |
| 181 | + paymentReference: string; |
| 182 | + signer: Signer; |
| 183 | + network: CurrencyTypes.EvmChainName; |
| 184 | +}): Promise<providers.TransactionResponse> { |
| 185 | + const proxyAddress = getRecurringPaymentProxyAddress(network); |
| 186 | + |
| 187 | + const data = encodeRecurringPaymentTrigger({ |
| 188 | + permitTuple, |
| 189 | + permitSignature, |
| 190 | + paymentIndex, |
| 191 | + paymentReference, |
| 192 | + network, |
| 193 | + provider: signer, |
| 194 | + }); |
| 195 | + |
| 196 | + const tx = await signer.sendTransaction({ |
| 197 | + to: proxyAddress, |
| 198 | + data, |
| 199 | + value: 0, |
| 200 | + }); |
| 201 | + |
| 202 | + return tx; |
| 203 | +} |
| 204 | + |
| 205 | +/** |
| 206 | + * Returns the deployed address of the ERC20RecurringPaymentProxy contract for a given network. |
| 207 | + * |
| 208 | + * @param network - The EVM chain name (e.g. 'mainnet', 'sepolia', 'matic') |
| 209 | + * |
| 210 | + * @returns The deployed proxy contract address for the specified network |
| 211 | + * |
| 212 | + * @throws {Error} If the ERC20RecurringPaymentProxy has no known deployment |
| 213 | + * on the provided network |
| 214 | + * |
| 215 | + * @remarks |
| 216 | + * • This is a pure helper that doesn't require a provider or make any network calls |
| 217 | + * • The address is looked up from the deployment artifacts maintained by the smart-contracts package |
| 218 | + * • Use this when you only need the address and don't need to interact with the contract |
| 219 | + */ |
| 220 | +export function getRecurringPaymentProxyAddress(network: CurrencyTypes.EvmChainName): string { |
| 221 | + const address = erc20RecurringPaymentProxyArtifact.getAddress(network); |
| 222 | + |
| 223 | + if (!address) { |
| 224 | + throw new Error(`ERC20RecurringPaymentProxy not found on ${network}`); |
| 225 | + } |
| 226 | + |
| 227 | + return address; |
| 228 | +} |
0 commit comments