Skip to content

Commit 0ebcb25

Browse files
authored
feat: add recurring payment smart contract (#1633)
1 parent 43a72ea commit 0ebcb25

File tree

19 files changed

+1855
-79
lines changed

19 files changed

+1855
-79
lines changed

packages/payment-processor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export * as Escrow from './payment/erc20-escrow-payment';
2929
export * from './payment/prepared-transaction';
3030
export * from './payment/utils-near';
3131
export * from './payment/single-request-forwarder';
32+
export * from './payment/erc20-recurring-payment-proxy';
3233

3334
import * as utils from './payment/utils';
3435

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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

Comments
 (0)