Skip to content

Commit fd2a103

Browse files
committed
feat: enhance cancel method to support allowance revocation for recurring payments
- Updated the cancel method to accept options for recurring payment cancellations. - Improved flexibility for payer/payee allowance revocation during cancellation.
1 parent 5210d8c commit fd2a103

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

packages/request-client.js/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@requestnetwork/epk-signature": "0.10.0",
5050
"@requestnetwork/multi-format": "0.28.0",
5151
"@requestnetwork/payment-detection": "0.54.0",
52+
"@requestnetwork/payment-processor": "0.57.0",
5253
"@requestnetwork/request-logic": "0.44.0",
5354
"@requestnetwork/smart-contracts": "0.48.0",
5455
"@requestnetwork/transaction-manager": "0.45.0",

packages/request-client.js/src/api/request.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
PaymentTypes,
1212
RequestLogicTypes,
1313
} from '@requestnetwork/types';
14+
import { getRecurringPaymentProxyAddress } from '@requestnetwork/payment-processor';
15+
import { ERC20__factory } from '@requestnetwork/smart-contracts/types';
16+
import { BigNumber } from 'ethers';
1417
import * as Types from '../types';
1518
import ContentDataExtension from './content-data-extension';
1619
import localUtils from './utils';
@@ -203,12 +206,24 @@ export default class Request {
203206
*
204207
* @param signerIdentity Identity of the signer. The identity type must be supported by the signature provider.
205208
* @param refundInformation refund information to add (any because it is specific to the payment network used by the request)
206-
* @returns The updated request
209+
* @param options Optional cancellation options for recurring payments
210+
* @param options.isRecurringPayment Whether this is a recurring payment cancellation
211+
* @param options.isPayerCancel Whether the payer is canceling (true) or payee (false). Only relevant for recurring payments.
212+
* @param options.recurringPaymentInfo Information needed to generate allowance revocation calldata for recurring payments
213+
* @returns The updated request and optional calldata for allowance revocation (if payer cancels recurring payment)
207214
*/
208215
public async cancel(
209216
signerIdentity: IdentityTypes.IIdentity,
210217
refundInformation?: any,
211-
): Promise<Types.IRequestDataWithEvents> {
218+
options?: {
219+
isRecurringPayment?: boolean;
220+
isPayerCancel?: boolean;
221+
recurringPaymentInfo?: {
222+
tokenAddress: string;
223+
network: CurrencyTypes.EvmChainName;
224+
};
225+
},
226+
): Promise<Types.IRequestDataWithEvents & { allowanceRevocationCalldata?: string }> {
212227
const extensionsData: any[] = [];
213228
if (refundInformation) {
214229
if (!this.paymentNetwork) {
@@ -225,8 +240,23 @@ export default class Request {
225240
};
226241

227242
const cancelResult = await this.requestLogic.cancelRequest(parameters, signerIdentity, true);
243+
const result = await this.handleRequestDataEvents(cancelResult);
244+
245+
// Generate allowance revocation calldata if payer is canceling a recurring payment
246+
let allowanceRevocationCalldata: string | undefined;
247+
if (options?.isRecurringPayment && options?.isPayerCancel && options?.recurringPaymentInfo) {
248+
const proxyAddress = getRecurringPaymentProxyAddress(options.recurringPaymentInfo.network);
249+
const erc20Interface = ERC20__factory.createInterface();
250+
allowanceRevocationCalldata = erc20Interface.encodeFunctionData('approve', [
251+
proxyAddress,
252+
BigNumber.from(0),
253+
]);
254+
}
228255

229-
return this.handleRequestDataEvents(cancelResult);
256+
return {
257+
...result,
258+
...(allowanceRevocationCalldata && { allowanceRevocationCalldata }),
259+
};
230260
}
231261

232262
/**

packages/request-client.js/test/api/request.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,57 @@ describe('api/request', () => {
219219
request.cancel(signatureIdentity, { refundAddress: bitcoinAddress }),
220220
).rejects.toThrowError('Cannot add refund information without payment network');
221221
});
222+
223+
it('returns allowance revocation calldata when payer cancels recurring payment', async () => {
224+
const request = new Request('1', mockRequestLogic, currencyManager);
225+
const result = await request.cancel(signatureIdentity, undefined, {
226+
isRecurringPayment: true,
227+
isPayerCancel: true,
228+
recurringPaymentInfo: {
229+
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
230+
network: 'private',
231+
},
232+
});
233+
234+
expect(result.allowanceRevocationCalldata).toBeDefined();
235+
expect(typeof result.allowanceRevocationCalldata).toBe('string');
236+
expect(result.allowanceRevocationCalldata?.startsWith('0x')).toBe(true);
237+
});
238+
239+
it('does not return allowance revocation calldata when payee cancels recurring payment', async () => {
240+
const request = new Request('1', mockRequestLogic, currencyManager);
241+
const result = await request.cancel(signatureIdentity, undefined, {
242+
isRecurringPayment: true,
243+
isPayerCancel: false,
244+
recurringPaymentInfo: {
245+
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
246+
network: 'private',
247+
},
248+
});
249+
250+
expect(result.allowanceRevocationCalldata).toBeUndefined();
251+
});
252+
253+
it('does not return allowance revocation calldata for non-recurring payment cancellation', async () => {
254+
const request = new Request('1', mockRequestLogic, currencyManager);
255+
const result = await request.cancel(signatureIdentity);
256+
257+
expect(result.allowanceRevocationCalldata).toBeUndefined();
258+
});
259+
260+
it('does not return allowance revocation calldata when isRecurringPayment is false', async () => {
261+
const request = new Request('1', mockRequestLogic, currencyManager);
262+
const result = await request.cancel(signatureIdentity, undefined, {
263+
isRecurringPayment: false,
264+
isPayerCancel: true,
265+
recurringPaymentInfo: {
266+
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
267+
network: 'private',
268+
},
269+
});
270+
271+
expect(result.allowanceRevocationCalldata).toBeUndefined();
272+
});
222273
});
223274

224275
describe('increaseExpectedAmountRequest', () => {

0 commit comments

Comments
 (0)