Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions packages/request-client.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@requestnetwork/epk-signature": "0.10.0",
"@requestnetwork/multi-format": "0.28.0",
"@requestnetwork/payment-detection": "0.54.0",
"@requestnetwork/payment-processor": "0.57.0",
"@requestnetwork/request-logic": "0.44.0",
"@requestnetwork/smart-contracts": "0.48.0",
"@requestnetwork/transaction-manager": "0.45.0",
Expand Down
36 changes: 33 additions & 3 deletions packages/request-client.js/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
PaymentTypes,
RequestLogicTypes,
} from '@requestnetwork/types';
import { getRecurringPaymentProxyAddress } from '@requestnetwork/payment-processor';
import { ERC20__factory } from '@requestnetwork/smart-contracts/types';
import { BigNumber } from 'ethers';
import * as Types from '../types';
import ContentDataExtension from './content-data-extension';
import localUtils from './utils';
Expand Down Expand Up @@ -203,12 +206,24 @@ export default class Request {
*
* @param signerIdentity Identity of the signer. The identity type must be supported by the signature provider.
* @param refundInformation refund information to add (any because it is specific to the payment network used by the request)
* @returns The updated request
* @param options Optional cancellation options for recurring payments
* @param options.isRecurringPayment Whether this is a recurring payment cancellation
* @param options.isPayerCancel Whether the payer is canceling (true) or payee (false). Only relevant for recurring payments.
* @param options.recurringPaymentInfo Information needed to generate allowance revocation calldata for recurring payments
* @returns The updated request and optional calldata for allowance revocation (if payer cancels recurring payment)
*/
public async cancel(
signerIdentity: IdentityTypes.IIdentity,
refundInformation?: any,
): Promise<Types.IRequestDataWithEvents> {
options?: {
isRecurringPayment?: boolean;
isPayerCancel?: boolean;
recurringPaymentInfo?: {
tokenAddress: string;
network: CurrencyTypes.EvmChainName;
};
},
): Promise<Types.IRequestDataWithEvents & { allowanceRevocationCalldata?: string }> {
const extensionsData: any[] = [];
if (refundInformation) {
if (!this.paymentNetwork) {
Expand All @@ -225,8 +240,23 @@ export default class Request {
};

const cancelResult = await this.requestLogic.cancelRequest(parameters, signerIdentity, true);
const result = await this.handleRequestDataEvents(cancelResult);

// Generate allowance revocation calldata if payer is canceling a recurring payment
let allowanceRevocationCalldata: string | undefined;
if (options?.isRecurringPayment && options?.isPayerCancel && options?.recurringPaymentInfo) {
const proxyAddress = getRecurringPaymentProxyAddress(options.recurringPaymentInfo.network);
const erc20Interface = ERC20__factory.createInterface();
allowanceRevocationCalldata = erc20Interface.encodeFunctionData('approve', [
proxyAddress,
BigNumber.from(0),
]);
}

return this.handleRequestDataEvents(cancelResult);
return {
...result,
...(allowanceRevocationCalldata && { allowanceRevocationCalldata }),
};
}

/**
Expand Down
51 changes: 51 additions & 0 deletions packages/request-client.js/test/api/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,57 @@ describe('api/request', () => {
request.cancel(signatureIdentity, { refundAddress: bitcoinAddress }),
).rejects.toThrowError('Cannot add refund information without payment network');
});

it('returns allowance revocation calldata when payer cancels recurring payment', async () => {
const request = new Request('1', mockRequestLogic, currencyManager);
const result = await request.cancel(signatureIdentity, undefined, {
isRecurringPayment: true,
isPayerCancel: true,
recurringPaymentInfo: {
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
network: 'private',
},
});

expect(result.allowanceRevocationCalldata).toBeDefined();
expect(typeof result.allowanceRevocationCalldata).toBe('string');
expect(result.allowanceRevocationCalldata?.startsWith('0x')).toBe(true);
});

it('does not return allowance revocation calldata when payee cancels recurring payment', async () => {
const request = new Request('1', mockRequestLogic, currencyManager);
const result = await request.cancel(signatureIdentity, undefined, {
isRecurringPayment: true,
isPayerCancel: false,
recurringPaymentInfo: {
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
network: 'private',
},
});

expect(result.allowanceRevocationCalldata).toBeUndefined();
});

it('does not return allowance revocation calldata for non-recurring payment cancellation', async () => {
const request = new Request('1', mockRequestLogic, currencyManager);
const result = await request.cancel(signatureIdentity);

expect(result.allowanceRevocationCalldata).toBeUndefined();
});

it('does not return allowance revocation calldata when isRecurringPayment is false', async () => {
const request = new Request('1', mockRequestLogic, currencyManager);
const result = await request.cancel(signatureIdentity, undefined, {
isRecurringPayment: false,
isPayerCancel: true,
recurringPaymentInfo: {
tokenAddress: '0x9FBDa871d559710256a2502A2517b794B482Db40',
network: 'private',
},
});

expect(result.allowanceRevocationCalldata).toBeUndefined();
});
});

describe('increaseExpectedAmountRequest', () => {
Expand Down