From cff4bbe0d472d3dd2d70c84a585bb3c24b5af4c6 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 04:45:09 +0200 Subject: [PATCH 01/20] feat: meta-pn --- .../src/meta-payment-detector.ts | 190 ++++++++ .../src/payment-network-factory.ts | 2 + .../test/meta-payment-network.test.ts | 411 ++++++++++++++++++ 3 files changed, 603 insertions(+) create mode 100644 packages/payment-detection/src/meta-payment-detector.ts create mode 100644 packages/payment-detection/test/meta-payment-network.test.ts diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts new file mode 100644 index 000000000..4486bef89 --- /dev/null +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -0,0 +1,190 @@ +import { + AdvancedLogicTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { ICurrencyManager } from '@requestnetwork/currency'; +import { deepCopy, generate8randomBytes } from '@requestnetwork/utils'; +import { AnyToERC20PaymentDetector, AnyToEthFeeProxyPaymentDetector } from './any'; +import { + IPaymentNetworkModuleByType, + PaymentNetworkOptions, + ReferenceBasedDetectorOptions, +} from './types'; +import { DeclarativePaymentDetector, DeclarativePaymentDetectorBase } from './declarative'; +import { BigNumber } from 'ethers'; + +const supportedPns = [ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE, + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, +]; + +const detectorMap: IPaymentNetworkModuleByType = { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: DeclarativePaymentDetector, + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: AnyToERC20PaymentDetector, + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: AnyToEthFeeProxyPaymentDetector, +}; + +const advancedLogicMap = { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: 'declarative', + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: 'anyToErc20Proxy', + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: 'anyToEthProxy', +}; + +/** + * Abstract class to extend to get the payment balance of reference based requests + */ +export class MetaDetector extends DeclarativePaymentDetectorBase< + ExtensionTypes.PnMeta.IMeta, + | PaymentTypes.ConversionPaymentNetworkEventParameters + | PaymentTypes.IDeclarativePaymentEventParameters +> { + private readonly advancedLogic: AdvancedLogicTypes.IAdvancedLogic; + private readonly currencyManager: ICurrencyManager; + private readonly options: Partial; + /** + * @param paymentNetworkId Example : ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA + * @param extension The advanced logic payment network extension, reference based + */ + public constructor({ + advancedLogic, + currencyManager, + options, + }: ReferenceBasedDetectorOptions & { options?: Partial }) { + super(ExtensionTypes.PAYMENT_NETWORK_ID.META, advancedLogic.extensions.metaPn); + this.options = options || {}; + this.currencyManager = currencyManager; + this.advancedLogic = advancedLogic; + } + + /** + * Creates the extensions data for the creation of this extension. + * Will set a salt if none is already given + * + * @param paymentNetworkCreationParameters Parameters to create the extension + * @returns The extensionData object + */ + public async createExtensionsDataForCreation( + paymentNetworkCreationParameters: ExtensionTypes.PnMeta.ICreationParameters, + ): Promise { + // If no salt is given, generate one + paymentNetworkCreationParameters.salt = + paymentNetworkCreationParameters.salt || (await generate8randomBytes()); + + // Do the same for each sub-extension + for (const [key, value] of Object.entries(paymentNetworkCreationParameters)) { + if (supportedPns.includes(key as ExtensionTypes.PAYMENT_NETWORK_ID)) { + const detectorClass = detectorMap[key as keyof typeof detectorMap]; + const extensionKey = advancedLogicMap[key as keyof typeof advancedLogicMap]; + const extension = + this.advancedLogic.extensions[extensionKey as keyof typeof this.advancedLogic.extensions]; + + if (!detectorClass || !extension) { + throw new Error(`the payment network id: ${key} is not supported`); + } + + const detector = new detectorClass({ + advancedLogic: this.advancedLogic, + paymentNetworkId: key as ExtensionTypes.PAYMENT_NETWORK_ID, + extension, + currencyManager: this.currencyManager, + ...this.options, + }); + + for (let index = 0; index < value.length; index++) { + paymentNetworkCreationParameters[key as keyof ExtensionTypes.PnMeta.ICreationParameters][ + index + ] = await detector.createExtensionsDataForCreation(value[index]); + } + } + } + return this.extension.createCreationAction({ + ...paymentNetworkCreationParameters, + }); + } + + /** + * Creates the extensions data to apply an action on a sub pn + * + * @param Parameters to add refund information + * @returns The extensionData object + */ + public createExtensionsDataForApplyActionOnPn( + parameters: ExtensionTypes.PnMeta.IApplyActionToPn, + ): ExtensionTypes.IAction { + return this.extension.createApplyActionToPn({ + pnIdentifier: parameters.pnIdentifier, + action: parameters.action, + parameters: parameters.parameters, + }); + } + + /** + * To retrieve all events, iterate over the sub payment networks and aggregate their balances + */ + protected async getEvents( + request: RequestLogicTypes.IRequest, + ): Promise< + PaymentTypes.AllNetworkEvents< + | PaymentTypes.ConversionPaymentNetworkEventParameters + | PaymentTypes.IDeclarativePaymentEventParameters + > + > { + const paymentExtension = this.getPaymentExtension(request); + const events: PaymentTypes.IBalanceWithEvents[] = []; + const feeBalances: PaymentTypes.IBalanceWithEvents[] = []; + this.checkRequiredParameter(paymentExtension.values.salt, 'salt'); + + for (const value of Object.values( + paymentExtension.values as Record>, + )) { + if (supportedPns.includes(value.id as ExtensionTypes.PAYMENT_NETWORK_ID)) { + const detectorClass = detectorMap[value.id as keyof typeof detectorMap]; + const extensionKey = advancedLogicMap[value.id as keyof typeof advancedLogicMap]; + const extension = + this.advancedLogic.extensions[extensionKey as keyof typeof this.advancedLogic.extensions]; + + if (!detectorClass || !extension) { + throw new Error(`the payment network id: ${value.id} is not supported`); + } + + const detector = new detectorClass({ + advancedLogic: this.advancedLogic, + paymentNetworkId: value.id as ExtensionTypes.PAYMENT_NETWORK_ID, + extension, + currencyManager: this.currencyManager, + ...this.options, + }); + + const partialRequest = deepCopy(request); + partialRequest.extensions = { + [value.id]: value, + }; + partialRequest.extensionsData = [value]; + + events.push(await detector.getBalance(partialRequest)); + const feeBalance = partialRequest.extensions[value.id].values.feeBalance; + if (feeBalance) { + feeBalances.push(feeBalance); + } + } + } + const declaredEvents = this.getDeclarativeEvents(request); + const allPaymentEvents = [...declaredEvents, ...events.map((event) => event.events).flat()]; + + // FIXME: should be at the same level as balance + const values: any = this.getPaymentExtension(request).values; + values.feeBalance = { + events: feeBalances.map((event) => event.events).flat(), + balance: feeBalances + .reduce((sum, curr) => sum.add(curr.balance || '0'), BigNumber.from(0)) + .toString(), + }; + + return { + paymentEvents: allPaymentEvents, + }; + } +} diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 9dc2a66a0..66b0b7a52 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -14,6 +14,7 @@ import { } from './types'; import { BtcMainnetAddressBasedDetector, BtcTestnetAddressBasedDetector } from './btc'; import { DeclarativePaymentDetector } from './declarative'; +import { MetaDetector } from './meta-payment-detector'; import { ERC20AddressBasedPaymentDetector, ERC20FeeProxyPaymentDetector, @@ -83,6 +84,7 @@ const anyCurrencyPaymentNetwork: IPaymentNetworkModuleByType = { [PN_ID.ANY_DECLARATIVE]: DeclarativePaymentDetector, [PN_ID.ANY_TO_ETH_PROXY]: AnyToEthFeeProxyPaymentDetector, [PN_ID.ANY_TO_NATIVE_TOKEN]: NearConversionNativeTokenPaymentDetector, + [PN_ID.META]: MetaDetector, }; /** Factory to create the payment network according to the currency and payment network type */ diff --git a/packages/payment-detection/test/meta-payment-network.test.ts b/packages/payment-detection/test/meta-payment-network.test.ts new file mode 100644 index 000000000..b2d6c26e0 --- /dev/null +++ b/packages/payment-detection/test/meta-payment-network.test.ts @@ -0,0 +1,411 @@ +import { + AdvancedLogicTypes, + ExtensionTypes, + IdentityTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; + +import { mockAdvancedLogicBase } from './utils'; +import { MetaDetector } from '../src/meta-payment-detector'; +import { AnyToERC20PaymentDetector, TheGraphClient } from '../src'; +import { CurrencyManager } from '@requestnetwork/currency'; + +jest.mock('../src/thegraph/client'); +const theGraphClientMock = { + GetAnyToFungiblePayments: jest.fn(), +} as jest.MockedObjectDeep; + +let detector: MetaDetector; + +const createCreationActionMeta = jest.fn(); +const createCreationActionAnyToErc20 = jest.fn(); +const createAddPaymentInstructionAction = jest.fn(); +const createAddRefundInstructionAction = jest.fn(); +const createDeclareReceivedPaymentAction = jest.fn(); +const createDeclareReceivedRefundAction = jest.fn(); +const createDeclareSentPaymentAction = jest.fn(); +const createDeclareSentRefundAction = jest.fn(); +const createApplyActionToPn = jest.fn(); + +const currencyManager = CurrencyManager.getDefault(); + +const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { + ...mockAdvancedLogicBase, + extensions: { + metaPn: { + createCreationAction: createCreationActionMeta, + createAddPaymentInstructionAction, + createAddRefundInstructionAction, + createDeclareReceivedPaymentAction, + createDeclareReceivedRefundAction, + createDeclareSentPaymentAction, + createDeclareSentRefundAction, + createApplyActionToPn, + }, + anyToErc20Proxy: { + createCreationAction: createCreationActionAnyToErc20, + // inherited from declarative + createAddPaymentInstructionAction, + createAddRefundInstructionAction, + }, + } as any as AdvancedLogicTypes.IAdvancedLogicExtensions, +}; + +const mainnetAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const maticAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b733'; + +const requestMock: RequestLogicTypes.IRequest = { + requestId: '0x1', + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: '', + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'USD', + }, + events: [], + expectedAmount: '100', + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.META]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + abcd: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: maticAddress, + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'matic', + salt: 'abcd', + }, + }, + efgh: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: mainnetAddress, + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'mainnet', + salt: 'efgh', + }, + }, + salt: 'main-salt', + }, + version: '0', + }, + }, + extensionsData: [], + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '', +}; + +// Most of the tests are done as integration tests in ../index.test.ts +/* eslint-disable @typescript-eslint/no-unused-expressions */ +describe('api/meta-payment-network', () => { + beforeEach(() => { + detector = new MetaDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + options: { + getSubgraphClient: () => theGraphClientMock, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('can createExtensionsDataForCreation', async () => { + const spyMeta = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createCreationAction'); + const spySubPn = jest.spyOn( + mockAdvancedLogic.extensions.anyToErc20Proxy, + 'createCreationAction', + ); + + await detector.createExtensionsDataForCreation({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'matic', + salt: 'abcd', + }, + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'mainnet', + salt: 'efgh', + }, + ], + salt: 'anySalt', + }); + + expect(spyMeta).toHaveBeenCalledTimes(1); + expect(spySubPn).toHaveBeenCalledTimes(2); + }); + + it('can createExtensionsDataForCreation without salt', async () => { + const spyMeta = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createCreationAction'); + const spySubPn = jest.spyOn( + mockAdvancedLogic.extensions.anyToErc20Proxy, + 'createCreationAction', + ); + + await detector.createExtensionsDataForCreation({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'matic', + salt: 'abcd', + }, + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'mainnet', + salt: 'efgh', + }, + ], + }); + + expect(spyMeta).toHaveBeenCalledTimes(1); + expect(spySubPn).toHaveBeenCalledTimes(2); + }); + + it('can createExtensionsDataForCreation without sub-salt', async () => { + const spyMeta = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createCreationAction'); + const spySubPn = jest.spyOn( + mockAdvancedLogic.extensions.anyToErc20Proxy, + 'createCreationAction', + ); + + await detector.createExtensionsDataForCreation({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'matic', + salt: 'abcd', + }, + { + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', + refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', + acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + network: 'mainnet', + }, + ], + salt: 'anySalt', + }); + + expect(spyMeta).toHaveBeenCalledTimes(1); + expect(spySubPn).toHaveBeenCalledTimes(2); + }); + + it('can createExtensionsDataForAddPaymentInformation', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.metaPn, + 'createAddPaymentInstructionAction', + ); + + detector.createExtensionsDataForAddPaymentInformation({ + paymentInfo: 'payment instruction', + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForAddRefundInformation', async () => { + const spy = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createAddRefundInstructionAction'); + + detector.createExtensionsDataForAddRefundInformation({ refundInfo: 'refund instruction' }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForDeclareSentPayment', async () => { + const spy = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createDeclareSentPaymentAction'); + + detector.createExtensionsDataForDeclareSentPayment({ amount: '1000', note: 'payment sent' }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForDeclareSentRefund', async () => { + const spy = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createDeclareSentRefundAction'); + + detector.createExtensionsDataForDeclareSentRefund({ amount: '1000', note: 'refund sent' }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForDeclareReceivedPayment', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.metaPn, + 'createDeclareReceivedPaymentAction', + ); + + detector.createExtensionsDataForDeclareReceivedPayment({ + amount: '1000', + note: 'payment received', + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForDeclareReceivedRefund', async () => { + const spy = jest.spyOn( + mockAdvancedLogic.extensions.metaPn, + 'createDeclareReceivedRefundAction', + ); + + detector.createExtensionsDataForDeclareReceivedRefund({ + amount: '1000', + note: 'refund received', + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('can createExtensionsDataForApplyActionOnPn', async () => { + const spy = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createApplyActionToPn'); + + detector.createExtensionsDataForApplyActionOnPn({ + pnIdentifier: 'abcd', + action: 'addPaymentAddress', + parameters: { + paymentAddress: 'any-address', + }, + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should not throw when getBalance fail', async () => { + expect(await detector.getBalance({ extensions: {} } as RequestLogicTypes.IRequest)).toEqual({ + balance: null, + error: { + code: PaymentTypes.BALANCE_ERROR_CODE.WRONG_EXTENSION, + message: 'The request does not have the extension: pn-meta', + }, + events: [], + }); + }); + + it('can compute the balance', async () => { + const mockExtractEvents = (eventName: PaymentTypes.EVENTS_NAMES, address: string) => { + if (eventName === PaymentTypes.EVENTS_NAMES.PAYMENT) { + if (address === mainnetAddress) { + return Promise.resolve({ + paymentEvents: [ + // Wrong fee address + { + amount: '100', + name: PaymentTypes.EVENTS_NAMES.PAYMENT, + parameters: { + block: 1, + feeAddress: 'fee address', + feeAmount: '5', + to: mainnetAddress, + txHash: '0xABC', + }, + timestamp: 10, + }, + // Correct fee address and a fee value + { + amount: '500', + name: PaymentTypes.EVENTS_NAMES.PAYMENT, + parameters: { + block: 1, + feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', + feeAmount: '5', + to: mainnetAddress, + txHash: '0xABCD', + }, + timestamp: 11, + }, + // No fee + { + amount: '500', + name: PaymentTypes.EVENTS_NAMES.PAYMENT, + parameters: { + block: 1, + feeAddress: '', + feeAmount: '0', + to: mainnetAddress, + txHash: '0xABCDE', + }, + timestamp: 12, + }, + ], + }); + } else if (address == maticAddress) { + return Promise.resolve({ + paymentEvents: [ + // Wrong fee address + { + amount: '100', + name: PaymentTypes.EVENTS_NAMES.PAYMENT, + parameters: { + block: 1, + feeAddress: 'fee address', + feeAmount: '5', + to: maticAddress, + txHash: '0xABC', + }, + timestamp: 10, + }, + ], + }); + } + } + return { + paymentEvents: [], + }; + }; + jest + .spyOn(AnyToERC20PaymentDetector.prototype as any, 'extractEvents') + .mockImplementation(mockExtractEvents as any); + + const balance = await detector.getBalance(requestMock); + expect(balance.error).toBeUndefined(); + // Mainnet Payments: 100 + 500 + 500 + // Matic Payments: 100 + expect(balance.balance).toBe('1200'); + // Fee Payment Mainnet: 5 + // Fee Payments Matic: 0 + expect( + requestMock.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META].values.feeBalance.balance, + ).toBe('5'); + }); +}); From 957a81b8cec867530f825198673f95cb733b86fc Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 04:46:15 +0200 Subject: [PATCH 02/20] feat: meta-pn --- packages/advanced-logic/src/advanced-logic.ts | 4 + .../src/extensions/abstract-extension.ts | 38 +- .../src/extensions/payment-network/meta.ts | 244 ++++++++++ .../any-to-erc20-proxy.test.ts | 4 +- .../extensions/payment-network/meta.test.ts | 372 +++++++++++++++ ...ny-to-erc20-proxy-create-data-generator.ts | 25 +- .../payment-network/meta-pn-data-generator.ts | 448 ++++++++++++++++++ packages/types/src/advanced-logic-types.ts | 1 + packages/types/src/extension-types.ts | 9 + packages/types/src/extensions/pn-meta.ts | 45 ++ 10 files changed, 1180 insertions(+), 10 deletions(-) create mode 100644 packages/advanced-logic/src/extensions/payment-network/meta.ts create mode 100644 packages/advanced-logic/test/extensions/payment-network/meta.test.ts create mode 100644 packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts create mode 100644 packages/types/src/extensions/pn-meta.ts diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index 4784ee017..c30f25bde 100644 --- a/packages/advanced-logic/src/advanced-logic.ts +++ b/packages/advanced-logic/src/advanced-logic.ts @@ -26,6 +26,7 @@ import AnyToNearTestnet from './extensions/payment-network/near/any-to-near-test import NativeToken from './extensions/payment-network/native-token'; import AnyToNative from './extensions/payment-network/any-to-native'; import Erc20TransferableReceivablePaymentNetwork from './extensions/payment-network/erc20/transferable-receivable'; +import MetaPaymentNetwork from './extensions/payment-network/meta'; /** * Module to manage Advanced logic extensions @@ -49,6 +50,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic anyToEthProxy: AnyToEthProxy; anyToNativeToken: AnyToNative[]; erc20TransferableReceivable: Erc20TransferableReceivablePaymentNetwork; + metaPn: MetaPaymentNetwork; }; private currencyManager: ICurrencyManager; @@ -71,6 +73,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic nativeToken: [new NearNative(currencyManager), new NearTestnetNative(currencyManager)], anyToNativeToken: [new AnyToNear(currencyManager), new AnyToNearTestnet(currencyManager)], erc20TransferableReceivable: new Erc20TransferableReceivablePaymentNetwork(currencyManager), + metaPn: new MetaPaymentNetwork(currencyManager), }; } @@ -131,6 +134,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic this.getAnyToNativeTokenExtensionForNetwork(network), [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: this.extensions.erc20TransferableReceivable, + [ExtensionTypes.PAYMENT_NETWORK_ID.META]: this.extensions.metaPn, }[id]; if (!extension) { diff --git a/packages/advanced-logic/src/extensions/abstract-extension.ts b/packages/advanced-logic/src/extensions/abstract-extension.ts index 1b545a454..bc47292af 100644 --- a/packages/advanced-logic/src/extensions/abstract-extension.ts +++ b/packages/advanced-logic/src/extensions/abstract-extension.ts @@ -1,11 +1,19 @@ import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; import { deepCopy } from '@requestnetwork/utils'; +export interface ICreationContext { + extensionsState: RequestLogicTypes.IExtensionStates; + extensionAction: ExtensionTypes.IAction; + requestState: RequestLogicTypes.IRequest; + actionSigner: IdentityTypes.IIdentity; +} + /** * Abstract class to create extension */ export abstract class AbstractExtension implements ExtensionTypes.IExtension { protected actions: ExtensionTypes.SupportedActions; + protected createActions: ExtensionTypes.SupportedActionsToCreate; protected constructor( public readonly extensionType: ExtensionTypes.TYPE, @@ -13,6 +21,9 @@ export abstract class AbstractExtension implements Extensio public readonly currentVersion: string, ) { this.actions = {}; + this.createActions = { + [ExtensionTypes.PnAnyDeclarative.ACTION.CREATE]: this.createCreationAction.bind(this), + }; } /** @@ -33,6 +44,24 @@ export abstract class AbstractExtension implements Extensio }; } + /** + * Create an action to apply to an extension + * + * @param action The action to create + * @param parameters Action parameters + * + * @returns The created Action + */ + public createExtensionAction(action: string, parameters: any): ExtensionTypes.IAction { + const actionCreator: ExtensionTypes.CreateAction = this.createActions[action]; + + if (!actionCreator) { + throw Error(`Unknown action: ${action}`); + } + + return actionCreator(parameters); + } + /** * Applies the extension action to the request * Is called to interpret the extensions data when applying the transaction @@ -60,7 +89,12 @@ export abstract class AbstractExtension implements Extensio throw Error(`This extension has already been created`); } - copiedExtensionState[extensionAction.id] = this.applyCreation(extensionAction, timestamp); + copiedExtensionState[extensionAction.id] = this.applyCreation(extensionAction, timestamp, { + extensionsState, + extensionAction, + requestState, + actionSigner, + }); return copiedExtensionState; } @@ -99,6 +133,8 @@ export abstract class AbstractExtension implements Extensio extensionAction: ExtensionTypes.IAction, // eslint-disable-next-line @typescript-eslint/no-unused-vars _timestamp: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context?: ICreationContext, ): ExtensionTypes.IState { if (!extensionAction.version) { throw Error('version is required at creation'); diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts new file mode 100644 index 000000000..7a412220e --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -0,0 +1,244 @@ +import { ICurrencyManager } from '@requestnetwork/currency'; +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { ICreationContext } from '../abstract-extension'; +import AnyToErc20ProxyPaymentNetwork from './any-to-erc20-proxy'; +import AnyToEthProxyPaymentNetwork from './any-to-eth-proxy'; +import { deepCopy } from '@requestnetwork/utils'; +import DeclarativePaymentNetwork from './declarative'; + +const CURRENT_VERSION = '0.1.0'; + +export default class MetaPaymentNetwork< + TCreationParameters extends + ExtensionTypes.PnMeta.ICreationParameters = ExtensionTypes.PnMeta.ICreationParameters, +> extends DeclarativePaymentNetwork { + public constructor( + protected currencyManager: ICurrencyManager, + public extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID.META, + public currentVersion: string = CURRENT_VERSION, + ) { + super(extensionId, currentVersion); + this.actions = { + ...this.actions, + [ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN]: + this.applyApplyActionToExtension.bind(this), + }; + } + + /** + * Creates the extensionsData to create the meta extension payment detection + * + * @param creationParameters extensions parameters to create + * + * @returns IExtensionCreationAction the extensionsData to be stored in the request + */ + public createCreationAction( + creationParameters: TCreationParameters, + ): ExtensionTypes.IAction { + Object.entries(creationParameters).forEach(([pnId, creationParameters]) => { + const pn = this.getExtension(pnId); + + if (!pn) throw new Error('Invalid PN'); + + // This is to perform validations on each input + for (const param of creationParameters) { + pn.createExtensionAction('create', param); + } + }); + + return super.createCreationAction(creationParameters); + } + + /** + * Creates the extensionsData to perform an action on a sub-pn + * + * @param parameters parameters to create the action to perform + * + * @returns IAction the extensionsData to be stored in the request + */ + public createApplyActionToPn( + parameters: ExtensionTypes.PnMeta.IApplyActionToPn, + ): ExtensionTypes.IAction { + return { + action: ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN, + id: this.extensionId, + parameters: { + pnIdentifier: parameters.pnIdentifier, + action: parameters.action, + parameters: parameters.parameters, + }, + }; + } + + /** + * Applies a creation extension action + * + * @param extensionAction action to apply + * @param timestamp action timestamp + * + * @returns state of the extension created + */ + protected applyCreation( + extensionAction: ExtensionTypes.IAction, + timestamp: number, + ): ExtensionTypes.IState; + protected applyCreation( + extensionAction: ExtensionTypes.IAction, + timestamp: number, + context: ICreationContext, + ): ExtensionTypes.IState; + protected applyCreation( + extensionAction: ExtensionTypes.IAction, + timestamp: number, + context?: ICreationContext, + ): ExtensionTypes.IState { + if (!context) { + throw new Error('Context is required'); + } + const values: Record = {}; + Object.entries(extensionAction.parameters).forEach(([pnId, parameters]) => { + const pn = this.getExtension(pnId); + if (!pn) throw new Error('Invalid PN'); + + (parameters as any[]).forEach((params) => { + console.log(params); + values[params.salt] = pn.applyActionToExtension( + {}, + { + action: 'create', + id: pnId as ExtensionTypes.PAYMENT_NETWORK_ID, + parameters: params, + version: pn.currentVersion, + }, + context.requestState, + context.actionSigner, + timestamp, + )[pnId]; + }); + }); + + return { + ...super.applyCreation(extensionAction, timestamp), + events: [ + { + name: 'create', + parameters: { + ...extensionAction.parameters, + }, + timestamp, + }, + ], + values, + }; + } + + /** Applies an action on a sub-payment network + * + * @param extensionsState previous state of the extensions + * @param extensionAction action to apply + * @param requestState request state read-only + * @param actionSigner identity of the signer + * @param timestamp timestamp of the action + * + * @returns state of the extension created + */ + protected applyApplyActionToExtension( + extensionState: ExtensionTypes.IState, + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + actionSigner: IdentityTypes.IIdentity, + timestamp: number, + ): ExtensionTypes.IState { + const copiedExtensionState: ExtensionTypes.IState = deepCopy(extensionState); + const { pnIdentifier, action, parameters } = extensionAction.parameters; + const extensionToActOn: ExtensionTypes.IState = copiedExtensionState.values[pnIdentifier]; + // increment sentPaymentAmount + + const pn = this.getExtension(extensionToActOn.id); + if (!pn) throw new Error('Invalid PN'); + + const subExtensionState = { + [extensionToActOn.id]: extensionToActOn, + }; + + copiedExtensionState.values[pnIdentifier] = pn.applyActionToExtension( + subExtensionState, + { + id: extensionToActOn.id, + action, + parameters, + }, + requestState, + actionSigner, + timestamp, + )[extensionToActOn.id]; + + // update events + copiedExtensionState.events.push({ + name: ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN, + parameters: { + pnIdentifier, + action, + parameters, + }, + timestamp, + from: actionSigner, + }); + return copiedExtensionState; + } + + /** + * Validate the extension action regarding the currency and network + * It must throw in case of error + */ + protected validate( + request: RequestLogicTypes.IRequest, + extensionAction: ExtensionTypes.IAction, + ): void { + const pnIdentifiers: string[] = []; + if (extensionAction.action === ExtensionTypes.PnMeta.ACTION.CREATE) { + Object.entries(extensionAction.parameters).forEach(([pnId, parameters]: [string, any]) => { + const pn = this.getExtension(pnId); + if (!pn) throw new Error('Invalid PN'); + + if (parameters.action) { + throw new Error('Invalid action'); + } + + for (const param of parameters) { + if (pnIdentifiers.includes(param.salt)) { + throw new Error('Duplicate payment network identifier'); + } + pnIdentifiers.push(param.salt); + } + + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META]?.values; + }); + } else if (extensionAction.action === ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN) { + const { pnIdentifier } = extensionAction.parameters; + + const subPnState: ExtensionTypes.IState = + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META]?.values?.[pnIdentifier]; + if (!subPnState) { + throw new Error(`No payment network with identifier ${pnIdentifier}`); + } + + const pn = this.getExtension(subPnState.id); + if (!pn) { + throw new Error(`Payment network ${subPnState.id} not supported`); + } + } + } + + private getExtension(pnId: string): ExtensionTypes.IExtension | undefined { + return { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: new DeclarativePaymentNetwork(), + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: new AnyToErc20ProxyPaymentNetwork( + this.currencyManager, + ), + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: new AnyToEthProxyPaymentNetwork( + this.currencyManager, + ), + }[pnId]; + } +} diff --git a/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts b/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts index 04ffc62e7..75c345639 100644 --- a/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts @@ -353,7 +353,7 @@ describe('extensions/payment-network/erc20/any-to-erc20-fee-proxy-contract', () TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, ), - ).toEqual(DataConversionERC20FeeCreate.extensionFullState); + ).toEqual(DataConversionERC20FeeCreate.extensionFullState()); }); it('can applyActionToExtensions of creation when address is checksumed', () => { @@ -372,7 +372,7 @@ describe('extensions/payment-network/erc20/any-to-erc20-fee-proxy-contract', () TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, ), - ).toEqual(DataConversionERC20FeeCreate.extensionFullState); + ).toEqual(DataConversionERC20FeeCreate.extensionFullState()); }); it('cannot applyActionToExtensions of creation with a previous state', () => { diff --git a/packages/advanced-logic/test/extensions/payment-network/meta.test.ts b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts new file mode 100644 index 000000000..b849cf712 --- /dev/null +++ b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts @@ -0,0 +1,372 @@ +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { deepCopy } from '@requestnetwork/utils'; +import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; + +import * as DataConversionERC20FeeAddData from '../../utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator'; +import * as MetaCreate from '../../utils/payment-network/meta-pn-data-generator'; +import * as TestData from '../../utils/test-data-generator'; +import MetaPaymentNetwork from '../../../src/extensions/payment-network/meta'; + +const metaPn = new MetaPaymentNetwork(CurrencyManager.getDefault()); +const baseParams = { + feeAddress: '0x0000000000000000000000000000000000000001', + feeAmount: '0', + paymentAddress: '0x0000000000000000000000000000000000000002', + refundAddress: '0x0000000000000000000000000000000000000003', + salt: 'ea3bc7caf64110ca', + network: 'rinkeby', + acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + maxRateTimespan: 1000000, +} as ExtensionTypes.PnAnyToErc20.ICreationParameters; + +/* eslint-disable @typescript-eslint/no-unused-expressions */ +describe('extensions/payment-network/meta', () => { + describe('createCreationAction', () => { + it('can create a create action with all parameters', () => { + expect( + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], + }, + version: '0.1.0', + }); + }); + + it('can create a create action without fee parameters', () => { + expect( + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAddress: undefined, feeAmount: undefined }, + baseParams, + ], + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAddress: undefined, feeAmount: undefined }, + baseParams, + ], + }, + version: '0.1.0', + }); + }); + + it('cannot createCreationAction with payment address not an ethereum address', () => { + // 'must throw' + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, paymentAddress: 'not an ethereum address' }, + baseParams, + ], + }); + }).toThrowError("paymentAddress 'not an ethereum address' is not a valid address"); + }); + + it('cannot createCreationAction with refund address not an ethereum address', () => { + // 'must throw' + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, refundAddress: 'not an ethereum address' }, + baseParams, + ], + }); + }).toThrowError("refundAddress 'not an ethereum address' is not a valid address"); + }); + + it('cannot createCreationAction with fee address not an ethereum address', () => { + // 'must throw' + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAddress: 'not an ethereum address' }, + baseParams, + ], + }); + }).toThrowError('feeAddress is not a valid address'); + }); + + it('cannot createCreationAction with invalid fee amount', () => { + // 'must throw' + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAmount: '-2000' }, + baseParams, + ], + }); + }).toThrowError('feeAmount is not a valid amount'); + }); + }); + + describe('applyActionToExtension', () => { + describe('applyActionToExtension/create', () => { + it('can applyActionToExtensions of creation', () => { + // 'new extension state wrong' + expect( + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + MetaCreate.actionCreationFull, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(MetaCreate.extensionFullState); + }); + + it('cannot applyActionToExtensions of creation with a previous state', () => { + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestFullStateCreated.extensions, + MetaCreate.actionCreationFull, + MetaCreate.requestFullStateCreated, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('This extension has already been created'); + }); + + it('cannot applyActionToExtensions of creation on a non supported currency', () => { + const requestCreatedNoExtension: RequestLogicTypes.IRequest = deepCopy( + TestData.requestCreatedNoExtension, + ); + requestCreatedNoExtension.currency = { + type: RequestLogicTypes.CURRENCY.BTC, + value: 'BTC', + }; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + TestData.requestCreatedNoExtension.extensions, + MetaCreate.actionCreationFull, + requestCreatedNoExtension, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + 'The currency (BTC-mainnet, 0x03049758a18d1589388d7a74fb71c3fcce11d286) of the request is not supported for this payment network.', + ); + }); + + it('cannot applyActionToExtensions of creation with payment address not valid', () => { + const actionWithInvalidAddress = deepCopy(MetaCreate.actionCreationFull); + actionWithInvalidAddress.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].paymentAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + actionWithInvalidAddress, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + `paymentAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`, + ); + }); + + it('cannot applyActionToExtensions of creation with no tokens accepted', () => { + const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationFull); + actionWithInvalidToken.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].acceptedTokens = []; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + actionWithInvalidToken, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('acceptedTokens is required'); + }); + + it('cannot applyActionToExtensions of creation with token address not valid', () => { + const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationFull); + actionWithInvalidToken.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].acceptedTokens = ['invalid address']; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + actionWithInvalidToken, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('acceptedTokens must contains only valid ethereum addresses'); + }); + + it('cannot applyActionToExtensions of creation with refund address not valid', () => { + const testnetRefundAddress = deepCopy(MetaCreate.actionCreationFull); + testnetRefundAddress.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].refundAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + testnetRefundAddress, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + `refundAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`, + ); + }); + it('keeps the version used at creation', () => { + const newState = metaPn.applyActionToExtension( + {}, + { ...MetaCreate.actionCreationFull, version: 'ABCD' }, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + expect(newState[metaPn.extensionId].version).toBe('ABCD'); + }); + + it('requires a version at creation', () => { + expect(() => { + metaPn.applyActionToExtension( + {}, + { ...MetaCreate.actionCreationFull, version: '' }, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('version is required at creation'); + }); + }); + + describe('applyActionToExtension/applyApplyActionToExtension', () => { + it('can applyActionToExtensions of applyApplyActionToExtension addPaymentAddress', () => { + expect( + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + MetaCreate.actionApplyActionToPn, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(MetaCreate.extensionStateWithApplyAddPaymentAddressAfterCreation); + }); + + it('cannot applyActionToExtensions of addPaymentAddress without a previous state', () => { + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateNoExtensions.extensions, + MetaCreate.actionApplyActionToPn, + MetaCreate.requestStateNoExtensions, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`No payment network with identifier ${MetaCreate.salt2}`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress without a payee', () => { + const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); + previousState.payee = undefined; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + previousState.extensions, + MetaCreate.actionApplyActionToPn, + previousState, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The request must have a payee`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress signed by someone else than the payee', () => { + const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + previousState.extensions, + MetaCreate.actionApplyActionToPn, + previousState, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payee`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress with payment address already given', () => { + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestFullStateCreated.extensions, + MetaCreate.actionApplyActionToPn, + MetaCreate.requestFullStateCreated, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Payment address already given`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress with payment address not valid', () => { + const actionWithInvalidAddress = deepCopy(MetaCreate.actionApplyActionToPn); + actionWithInvalidAddress.parameters.parameters.paymentAddress = + DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + actionWithInvalidAddress, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + `paymentAddress '${DataConversionERC20FeeAddData.invalidAddress}' is not a valid address`, + ); + }); + + it('cannot applyActionToExtensions when the pn identifier is wrong', () => { + const actionWithInvalidPnIdentifier = deepCopy(MetaCreate.actionApplyActionToPn); + actionWithInvalidPnIdentifier.parameters.pnIdentifier = 'wrongId'; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + actionWithInvalidPnIdentifier, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`No payment network with identifier wrongId`); + }); + + it('cannot applyActionToExtensions when the action does not exists on the sub pn', () => { + const actionWithInvalidPnAction = deepCopy(MetaCreate.actionApplyActionToPn); + actionWithInvalidPnAction.parameters.action = 'wrongAction' as ExtensionTypes.ACTION; + // 'must throw' + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + actionWithInvalidPnAction, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Unknown action: wrongAction`); + }); + }); + }); +}); diff --git a/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts index 14f972048..9ff99fd90 100644 --- a/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts +++ b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts @@ -70,7 +70,10 @@ export const actionCreationEmpty = { // --------------------------------------------------------------------- // extensions states -export const extensionFullState = { +export const extensionFullState = ( + saltOverride?: string, + paymentAddressOverride?: string | null, +) => ({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY as string]: { events: [ { @@ -79,9 +82,13 @@ export const extensionFullState = { feeAddress, feeAmount, network, - paymentAddress, + paymentAddress: paymentAddressOverride + ? paymentAddressOverride + : paymentAddressOverride === null + ? undefined + : paymentAddress, refundAddress, - salt, + salt: saltOverride || salt, acceptedTokens: [tokenAddress], maxRateTimespan: undefined, }, @@ -94,9 +101,13 @@ export const extensionFullState = { feeAddress, feeAmount, network, - paymentAddress, + paymentAddress: paymentAddressOverride + ? paymentAddressOverride + : paymentAddressOverride === null + ? undefined + : paymentAddress, refundAddress, - salt, + salt: saltOverride || salt, acceptedTokens: [tokenAddress], maxRateTimespan: undefined, payeeDelegate: undefined, @@ -110,7 +121,7 @@ export const extensionFullState = { }, version: '0.1.0', }, -}; +}); export const extensionStateCreatedEmpty = { [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY as string]: { events: [ @@ -228,7 +239,7 @@ export const requestFullStateCreated: RequestLogicTypes.IRequest = { }, ], expectedAmount: TestData.arbitraryExpectedAmount, - extensions: extensionFullState, + extensions: extensionFullState(), extensionsData: [actionCreationFull], payee: { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, diff --git a/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts new file mode 100644 index 000000000..3b71c05cc --- /dev/null +++ b/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts @@ -0,0 +1,448 @@ +import * as TestData from '../test-data-generator'; + +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; +import * as AnyToErc20Create from './erc20/any-to-erc20-proxy-create-data-generator'; +import * as AnyToErc20Add from './erc20/any-to-erc20-proxy-add-data-generator'; + +export const arbitraryTimestamp = 1544426030; + +// --------------------------------------------------------------------- +// Mock addresses for testing ETH payment networks +export const paymentAddress = '0x627306090abaB3A6e1400e9345bC60c78a8BEf57'; +export const refundAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +export const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +export const feeAmount = '2000000000000000000'; +export const invalidAddress = '0x not and address'; +export const tokenAddress = '0x6b175474e89094c44da98b954eedeac495271d0f'; +export const network = 'mainnet'; +export const saltMain = 'ea3bc7caf64110ca'; +export const salt1 = 'ea3bc7caf64110cb'; +export const salt2 = 'ea3bc7caf64110cc'; +export const salt3 = 'ea3bc7caf64110cd'; +export const salt4 = 'ea3bc7caf64110ce'; +// --------------------------------------------------------------------- +export const baseParams = (salt: string) => ({ + feeAddress, + feeAmount, + paymentAddress, + refundAddress, + salt, + acceptedTokens: [tokenAddress], + network, +}); +export const extendedParams = (salt: string) => ({ + ...baseParams(salt), + maxRateTimespan: undefined, + payeeDelegate: undefined, + payerDelegate: undefined, + paymentInfo: undefined, + receivedPaymentAmount: '0', + receivedRefundAmount: '0', + refundInfo: undefined, + sentPaymentAmount: '0', + sentRefundAmount: '0', +}); +// actions +export const actionCreationFull = { + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams(salt1), baseParams(salt2)], + }, + version: '0.1.0', +}; +export const actionCreationOnlyPayment = { + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + parameters: { + paymentAddress, + acceptedTokens: [tokenAddress], + network, + }, + version: '0.1.0', +}; +export const actionCreationOnlyRefund = { + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + parameters: { + refundAddress, + acceptedTokens: [tokenAddress], + network, + }, + version: '0.1.0', +}; +export const actionCreationOnlyFee = { + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + parameters: { + feeAddress, + feeAmount, + acceptedTokens: [tokenAddress], + network, + }, + version: '0.1.0', +}; +export const actionCreationEmpty = { + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: {}, + version: '0.1.0', +}; +export const actionApplyActionToPn = { + action: ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN, + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + action: ExtensionTypes.PnAddressBased.ACTION.ADD_PAYMENT_ADDRESS, + pnIdentifier: salt2, + parameters: { + paymentAddress, + }, + }, +}; + +// --------------------------------------------------------------------- +// extensions states +export const extensionFullState = { + [ExtensionTypes.PAYMENT_NETWORK_ID.META as string]: { + events: [ + { + name: 'create', + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + baseParams(salt1), + baseParams(salt2), + ], + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + [salt1]: + AnyToErc20Create.extensionFullState(salt1)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + [salt2]: + AnyToErc20Create.extensionFullState(salt2)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + }, + version: '0.1.0', + }, +}; +export const extensionStateCreatedEmpty = { + [ExtensionTypes.PAYMENT_NETWORK_ID.META as string]: { + events: [ + { + name: 'create', + parameters: {}, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: {}, + version: '0.1.0', + }, +}; + +export const extensionStateCreatedMissingAddress = { + [ExtensionTypes.PAYMENT_NETWORK_ID.META as string]: { + events: [ + { + name: 'create', + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + baseParams(salt1), + { ...baseParams(salt2), paymentAddress: undefined }, + ], + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + [salt1]: + AnyToErc20Create.extensionFullState(salt1)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + [salt2]: AnyToErc20Create.extensionFullState(salt2, null)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + }, + version: '0.1.0', + }, +}; + +export const extensionStateWithApplyAddPaymentAddressAfterCreation = { + [ExtensionTypes.PAYMENT_NETWORK_ID.META as string]: { + events: [ + { + name: 'create', + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + baseParams(salt1), + { ...baseParams(salt2), paymentAddress: undefined }, + ], + }, + timestamp: arbitraryTimestamp, + }, + { + from: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN, + parameters: { + pnIdentifier: salt2, + action: ExtensionTypes.PnAddressBased.ACTION.ADD_PAYMENT_ADDRESS, + parameters: { + paymentAddress, + }, + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + [salt1]: + AnyToErc20Create.extensionFullState(salt1)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + [salt2]: { + ...AnyToErc20Create.extensionFullState(salt2, null)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ], + events: [ + ...AnyToErc20Create.extensionFullState(salt2, null)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ].events, + { + name: ExtensionTypes.PnAddressBased.ACTION.ADD_PAYMENT_ADDRESS, + parameters: { + paymentAddress, + }, + timestamp: 1544426030, + }, + ], + values: { + ...AnyToErc20Create.extensionFullState(salt2, null)[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ].values, + paymentAddress, + }, + }, + }, + version: '0.1.0', + }, +}; + +// --------------------------------------------------------------------- +// request states +export const requestStateNoExtensions: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 0, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: {}, + extensionsData: [], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestFullStateCreated: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionFullState, + extensionsData: [actionCreationFull], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestStateCreatedEmpty: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateCreatedEmpty, + extensionsData: [actionCreationEmpty], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestStateCreatedMissingAddress: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateCreatedMissingAddress, + extensionsData: [extensionStateCreatedMissingAddress], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestStateCreatedAfterApplyAddAddress: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateWithApplyAddPaymentAddressAfterCreation, + extensionsData: [extensionStateWithApplyAddPaymentAddressAfterCreation], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; diff --git a/packages/types/src/advanced-logic-types.ts b/packages/types/src/advanced-logic-types.ts index 34bfede9b..2b4ffeab5 100644 --- a/packages/types/src/advanced-logic-types.ts +++ b/packages/types/src/advanced-logic-types.ts @@ -21,6 +21,7 @@ export interface IAdvancedLogicExtensions { anyToEthProxy: Extension.PnFeeReferenceBased.IFeeReferenceBased; anyToNativeToken: Extension.PnFeeReferenceBased.IFeeReferenceBased[]; erc20TransferableReceivable: Extension.PnFeeReferenceBased.IFeeReferenceBased; + metaPn: Extension.PnMeta.IMeta; } /** Advanced Logic layer */ diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index 5331df4e3..149d2cc73 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -6,6 +6,7 @@ import * as PnFeeReferenceBased from './extensions/pn-any-fee-reference-based-ty import * as PnReferenceBased from './extensions/pn-any-reference-based-types'; import * as PnAnyToErc20 from './extensions/pn-any-to-erc20-types'; import * as PnAnyToEth from './extensions/pn-any-to-eth-types'; +import * as PnMeta from './extensions/pn-meta'; import * as PnAnyToAnyConversion from './extensions/pn-any-to-any-conversion-types'; import * as Identity from './identity-types'; import * as RequestLogic from './request-logic-types'; @@ -20,6 +21,7 @@ export { PnAnyToErc20, PnAnyToEth, PnAnyToAnyConversion, + PnMeta, }; /** Extension interface is extended by the extensions implementation */ @@ -34,8 +36,11 @@ export interface IExtension { actionSigner: Identity.IIdentity, timestamp: number, ) => RequestLogic.IExtensionStates; + createExtensionAction: (action: string, parameters: any) => IAction; } +export type CreateAction = (parameters: any) => IAction; + export type ApplyAction = ( extensionState: IState, extensionAction: IAction, @@ -97,6 +102,7 @@ export enum PAYMENT_NETWORK_ID { ANY_TO_ERC20_PROXY = 'pn-any-to-erc20-proxy', ANY_TO_ETH_PROXY = 'pn-any-to-eth-proxy', ERC20_TRANSFERABLE_RECEIVABLE = 'pn-erc20-transferable-receivable', + META = 'pn-meta', } export const ID = { @@ -119,3 +125,6 @@ export enum ACTION { } export type SupportedActions = { [actionId: string]: ApplyAction }; +export type SupportedActionsToCreate = { + [actionId: string]: CreateAction; +}; diff --git a/packages/types/src/extensions/pn-meta.ts b/packages/types/src/extensions/pn-meta.ts new file mode 100644 index 000000000..3e7667f49 --- /dev/null +++ b/packages/types/src/extensions/pn-meta.ts @@ -0,0 +1,45 @@ +// import * as Extension from '../extension-types'; +import { ExtensionTypes } from '..'; +import { + PnAnyDeclarative, + PnAnyToAnyConversion, + PnAnyToErc20, + PnAnyToEth, + PnFeeReferenceBased, +} from '../extension-types'; + +/** Manager of the extension */ +export interface IMeta + extends PnAnyDeclarative.IAnyDeclarative { + createCreationAction: ( + parameters: TCreationParameters, + ) => ExtensionTypes.IAction; + createApplyActionToPn: ( + parameters: IApplyActionToPn, + ) => ExtensionTypes.IAction; +} + +/** Parameters of creation action */ +export interface ICreationParameters extends PnAnyDeclarative.ICreationParameters { + [ExtensionTypes.PAYMENT_NETWORK_ID + .ERC20_FEE_PROXY_CONTRACT]?: PnFeeReferenceBased.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]?: PnAnyToErc20.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID + .ETH_FEE_PROXY_CONTRACT]?: PnFeeReferenceBased.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]?: PnAnyToEth.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID + .ANY_TO_NATIVE_TOKEN]?: PnAnyToAnyConversion.ICreationParameters[]; +} + +/** Parameters of declareSentPayment and declareSentRefund action */ +export interface IApplyActionToPn { + pnIdentifier: string; + action: string; + parameters: any; +} + +/** Actions possible */ +export enum ACTION { + CREATE = 'create', + APPLY_ACTION_TO_PN = 'apply-action-to-pn', +} From af03f65c807eabd24166991d346c5fa684650519 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 04:50:16 +0200 Subject: [PATCH 03/20] revert change --- packages/types/src/extension-types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index 149d2cc73..ef1ba255a 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -36,7 +36,6 @@ export interface IExtension { actionSigner: Identity.IIdentity, timestamp: number, ) => RequestLogic.IExtensionStates; - createExtensionAction: (action: string, parameters: any) => IAction; } export type CreateAction = (parameters: any) => IAction; From c8f847ada6a58714d3849bf5d8849660d7a8ec1c Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 04:50:31 +0200 Subject: [PATCH 04/20] revert change --- .../src/extensions/abstract-extension.ts | 22 ------------------- .../src/extensions/payment-network/meta.ts | 7 +----- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/advanced-logic/src/extensions/abstract-extension.ts b/packages/advanced-logic/src/extensions/abstract-extension.ts index bc47292af..b28bf23b3 100644 --- a/packages/advanced-logic/src/extensions/abstract-extension.ts +++ b/packages/advanced-logic/src/extensions/abstract-extension.ts @@ -13,7 +13,6 @@ export interface ICreationContext { */ export abstract class AbstractExtension implements ExtensionTypes.IExtension { protected actions: ExtensionTypes.SupportedActions; - protected createActions: ExtensionTypes.SupportedActionsToCreate; protected constructor( public readonly extensionType: ExtensionTypes.TYPE, @@ -21,9 +20,6 @@ export abstract class AbstractExtension implements Extensio public readonly currentVersion: string, ) { this.actions = {}; - this.createActions = { - [ExtensionTypes.PnAnyDeclarative.ACTION.CREATE]: this.createCreationAction.bind(this), - }; } /** @@ -44,24 +40,6 @@ export abstract class AbstractExtension implements Extensio }; } - /** - * Create an action to apply to an extension - * - * @param action The action to create - * @param parameters Action parameters - * - * @returns The created Action - */ - public createExtensionAction(action: string, parameters: any): ExtensionTypes.IAction { - const actionCreator: ExtensionTypes.CreateAction = this.createActions[action]; - - if (!actionCreator) { - throw Error(`Unknown action: ${action}`); - } - - return actionCreator(parameters); - } - /** * Applies the extension action to the request * Is called to interpret the extensions data when applying the transaction diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index 7a412220e..29ee76067 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -35,15 +35,10 @@ export default class MetaPaymentNetwork< public createCreationAction( creationParameters: TCreationParameters, ): ExtensionTypes.IAction { - Object.entries(creationParameters).forEach(([pnId, creationParameters]) => { + Object.entries(creationParameters).forEach(([pnId]) => { const pn = this.getExtension(pnId); if (!pn) throw new Error('Invalid PN'); - - // This is to perform validations on each input - for (const param of creationParameters) { - pn.createExtensionAction('create', param); - } }); return super.createCreationAction(creationParameters); From 326c79430a25bf45af4487dfbb986f2653e1227f Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 04:54:54 +0200 Subject: [PATCH 05/20] remove log --- packages/advanced-logic/src/extensions/payment-network/meta.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index 29ee76067..6bcda43d2 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -96,7 +96,6 @@ export default class MetaPaymentNetwork< if (!pn) throw new Error('Invalid PN'); (parameters as any[]).forEach((params) => { - console.log(params); values[params.salt] = pn.applyActionToExtension( {}, { From e440a3eaefaaab5e3ae0a838df9b00e56becafaa Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 05:28:07 +0200 Subject: [PATCH 06/20] fixes --- packages/integration-test/test/scheduled/mocks.ts | 5 +++++ packages/types/src/extensions/pn-meta.ts | 15 ++------------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/integration-test/test/scheduled/mocks.ts b/packages/integration-test/test/scheduled/mocks.ts index ba3dd0bbe..acc2e8620 100644 --- a/packages/integration-test/test/scheduled/mocks.ts +++ b/packages/integration-test/test/scheduled/mocks.ts @@ -7,6 +7,7 @@ const createCreationAction = jest.fn(); const createAddFeeAction = jest.fn(); const createAddPaymentInstructionAction = jest.fn(); const createAddRefundInstructionAction = jest.fn(); +const createApplyActionToPn = jest.fn(); export const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { applyActionToExtensions: jest.fn(), @@ -79,5 +80,9 @@ export const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { createAddPaymentInstructionAction, createAddRefundInstructionAction, } as any as Extension.PnFeeReferenceBased.IFeeReferenceBased, + metaPn: { + createCreationAction, + createApplyActionToPn, + } as any as Extension.PnMeta.IMeta, }, }; diff --git a/packages/types/src/extensions/pn-meta.ts b/packages/types/src/extensions/pn-meta.ts index 3e7667f49..435b9a442 100644 --- a/packages/types/src/extensions/pn-meta.ts +++ b/packages/types/src/extensions/pn-meta.ts @@ -1,12 +1,6 @@ // import * as Extension from '../extension-types'; import { ExtensionTypes } from '..'; -import { - PnAnyDeclarative, - PnAnyToAnyConversion, - PnAnyToErc20, - PnAnyToEth, - PnFeeReferenceBased, -} from '../extension-types'; +import { PnAnyDeclarative, PnAnyToErc20, PnAnyToEth } from '../extension-types'; /** Manager of the extension */ export interface IMeta @@ -21,14 +15,9 @@ export interface IMeta /** Parameters of creation action */ export interface ICreationParameters extends PnAnyDeclarative.ICreationParameters { - [ExtensionTypes.PAYMENT_NETWORK_ID - .ERC20_FEE_PROXY_CONTRACT]?: PnFeeReferenceBased.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]?: PnAnyDeclarative.ICreationParameters[]; [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]?: PnAnyToErc20.ICreationParameters[]; - [ExtensionTypes.PAYMENT_NETWORK_ID - .ETH_FEE_PROXY_CONTRACT]?: PnFeeReferenceBased.ICreationParameters[]; [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]?: PnAnyToEth.ICreationParameters[]; - [ExtensionTypes.PAYMENT_NETWORK_ID - .ANY_TO_NATIVE_TOKEN]?: PnAnyToAnyConversion.ICreationParameters[]; } /** Parameters of declareSentPayment and declareSentRefund action */ From e2cf57f5be9c8bba55c0a9d22c50156ccc1a7121 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 05:40:45 +0200 Subject: [PATCH 07/20] fix meta validation --- .../advanced-logic/src/extensions/payment-network/meta.ts | 7 ++++++- packages/types/src/extension-types.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index 6bcda43d2..39861e317 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -35,10 +35,15 @@ export default class MetaPaymentNetwork< public createCreationAction( creationParameters: TCreationParameters, ): ExtensionTypes.IAction { - Object.entries(creationParameters).forEach(([pnId]) => { + Object.entries(creationParameters).forEach(([pnId, creationParameters]) => { const pn = this.getExtension(pnId); if (!pn) throw new Error('Invalid PN'); + + // Perform validation on sub-pn creation parameters + for (const param of creationParameters) { + pn.createCreationAction(param); + } }); return super.createCreationAction(creationParameters); diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index ef1ba255a..bf7751d53 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -36,6 +36,7 @@ export interface IExtension { actionSigner: Identity.IIdentity, timestamp: number, ) => RequestLogic.IExtensionStates; + createCreationAction: (parameters: TCreationParameters) => IAction; } export type CreateAction = (parameters: any) => IAction; From 57120894e9a49e14dfdbccc09cb4f365d6e819db Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 06:11:44 +0200 Subject: [PATCH 08/20] fix types --- packages/advanced-logic/src/advanced-logic.ts | 4 +++- packages/types/src/advanced-logic-types.ts | 4 ++-- packages/types/src/extension-types.ts | 5 ----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index c30f25bde..cfe6fad6c 100644 --- a/packages/advanced-logic/src/advanced-logic.ts +++ b/packages/advanced-logic/src/advanced-logic.ts @@ -162,7 +162,9 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic public getAnyToNativeTokenExtensionForNetwork( network?: CurrencyTypes.ChainName, - ): AnyToNative | undefined { + ): + | ExtensionTypes.IExtension + | undefined { return network ? this.extensions.anyToNativeToken.find((anyToNativeTokenExtension) => anyToNativeTokenExtension.supportedNetworks.includes(network), diff --git a/packages/types/src/advanced-logic-types.ts b/packages/types/src/advanced-logic-types.ts index 2b4ffeab5..34bb0a499 100644 --- a/packages/types/src/advanced-logic-types.ts +++ b/packages/types/src/advanced-logic-types.ts @@ -37,8 +37,8 @@ export interface IAdvancedLogic { network: ChainName, ) => Extension.IExtension | undefined; getAnyToNativeTokenExtensionForNetwork: ( - network: ChainName, - ) => Extension.IExtension | undefined; + network?: ChainName, + ) => Extension.IExtension | undefined; getFeeProxyContractErc20ForNetwork: ( network?: ChainName, ) => Extension.PnFeeReferenceBased.IFeeReferenceBased | undefined; diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index bf7751d53..71fb8fa2e 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -39,8 +39,6 @@ export interface IExtension { createCreationAction: (parameters: TCreationParameters) => IAction; } -export type CreateAction = (parameters: any) => IAction; - export type ApplyAction = ( extensionState: IState, extensionAction: IAction, @@ -125,6 +123,3 @@ export enum ACTION { } export type SupportedActions = { [actionId: string]: ApplyAction }; -export type SupportedActionsToCreate = { - [actionId: string]: CreateAction; -}; From a5231f292a4e7c1e9558f320bc0c8efc5f99753f Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 20:14:37 +0200 Subject: [PATCH 09/20] feat: meta-pn payment processor --- packages/payment-detection/src/index.ts | 2 + .../src/payment/encoder-approval.ts | 11 +- .../src/payment/encoder-payment.ts | 6 +- .../payment-processor/src/payment/utils.ts | 30 +++ packages/payment-processor/src/types.ts | 2 + .../test/payment/encoder-approval.test.ts | 176 ++++++++++++++++++ .../test/payment/encoder-payment.test.ts | 143 ++++++++++++++ 7 files changed, 364 insertions(+), 6 deletions(-) diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 74e57ebeb..2d9175aad 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -28,6 +28,7 @@ import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever'; import { SuperFluidInfoRetriever } from './erc777/superfluid-retriever'; import { PaymentNetworkOptions } from './types'; import { ERC20TransferableReceivablePaymentDetector } from './erc20'; +import { MetaDetector } from './meta-payment-detector'; export type { TheGraphClient } from './thegraph'; @@ -49,6 +50,7 @@ export { NearConversionNativeTokenPaymentDetector, EscrowERC20InfoRetriever, SuperFluidInfoRetriever, + MetaDetector, setProviderFactory, initPaymentDetectionApiKeys, getDefaultProvider, diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index 4149f0e6c..d5369482f 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -13,6 +13,7 @@ import { prepareApprovalErc20ForSwapWithConversionToPay, } from './swap-conversion-erc20'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; +import { getFormattedRequest } from './utils'; /** * For a given request and user, encode an approval transaction if it is needed. @@ -27,10 +28,11 @@ export async function encodeRequestErc20ApprovalIfNeeded( from: string, options?: IRequestPaymentOptions, ): Promise { + const formattedRequest = getFormattedRequest({ request, options }); if (options && options.swap) { - return encodeRequestErc20ApprovalWithSwapIfNeeded(request, provider, from, options); + return encodeRequestErc20ApprovalWithSwapIfNeeded(formattedRequest, provider, from, options); } else { - return encodeRequestErc20ApprovalWithoutSwapIfNeeded(request, provider, from, options); + return encodeRequestErc20ApprovalWithoutSwapIfNeeded(formattedRequest, provider, from, options); } } @@ -45,10 +47,11 @@ export function encodeRequestErc20Approval( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction | void { + const formattedRequest = getFormattedRequest({ request, options }); if (options && options.swap) { - return encodeRequestErc20ApprovalWithSwap(request, provider, options); + return encodeRequestErc20ApprovalWithSwap(formattedRequest, provider, options); } else { - return encodeRequestErc20ApprovalWithoutSwap(request, provider, options); + return encodeRequestErc20ApprovalWithoutSwap(formattedRequest, provider, options); } } diff --git a/packages/payment-processor/src/payment/encoder-payment.ts b/packages/payment-processor/src/payment/encoder-payment.ts index bee279094..bfc239caa 100644 --- a/packages/payment-processor/src/payment/encoder-payment.ts +++ b/packages/payment-processor/src/payment/encoder-payment.ts @@ -13,6 +13,7 @@ import { prepareEthFeeProxyPaymentTransaction } from './eth-fee-proxy'; import { prepareAnyToEthProxyPaymentTransaction } from './any-to-eth-proxy'; import { IConversionPaymentSettings } from '.'; import { prepareErc777StreamPaymentTransaction } from './erc777-stream'; +import { getFormattedRequest } from './utils'; /** * Encodes a transaction to pay a Request in generic way. ERC777 stream excepted. @@ -25,10 +26,11 @@ export function encodeRequestPayment( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction { + const formattedRequest = getFormattedRequest({ request, options }); if (options && options.swap) { - return encodeRequestPaymentWithSwap(request, provider, options); + return encodeRequestPaymentWithSwap(formattedRequest, provider, options); } else { - return encodeRequestPaymentWithoutSwap(request, options); + return encodeRequestPaymentWithoutSwap(formattedRequest, options); } } diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index a340c6fc6..2adaaa35c 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -11,6 +11,8 @@ import { EvmChains, getCurrencyHash } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; import { getReceivableTokenIdForRequest } from './erc20-transferable-receivable'; +import { IRequestPaymentOptions } from '../types'; +import { deepCopy } from '@requestnetwork/utils'; /** @constant MAX_ALLOWANCE set to the max uint256 value */ export const MAX_ALLOWANCE = BigNumber.from(2).pow(256).sub(1); @@ -420,3 +422,31 @@ export async function revokeErc20Approval( const tx = await signer.sendTransaction(preparedTx); return tx; } + +/** + * Format a request we wish to build a payment for. + * If the request does not use the meta-pn, returns it as it. + * Otherwise, returns the request formatted with the pn of interest + */ +export function getFormattedRequest({ + request, + options, +}: { + request: ClientTypes.IRequestData; + options?: IRequestPaymentOptions; +}): ClientTypes.IRequestData { + const pn = getPaymentNetworkExtension(request); + if (!pn?.id || pn.id !== ExtensionTypes.PAYMENT_NETWORK_ID.META) return request; + if (!options?.pnIdentifier) throw new Error('Missing pn identifier'); + + const extensionOfInterest: ExtensionTypes.IState | undefined = pn.values[options.pnIdentifier]; + if (!extensionOfInterest) throw new Error('Invalid pn identifier'); + + const formattedRequest = { + ...deepCopy(request), + extensions: { + [extensionOfInterest.id]: extensionOfInterest, + }, + }; + return formattedRequest; +} diff --git a/packages/payment-processor/src/types.ts b/packages/payment-processor/src/types.ts index c82e94872..a04bb5714 100644 --- a/packages/payment-processor/src/types.ts +++ b/packages/payment-processor/src/types.ts @@ -66,6 +66,8 @@ export interface IRequestPaymentOptions { skipFeeUSDLimit?: boolean; /** Optional, only for batch payment to define the proxy to use. */ version?: string; + /** Payment network identifier - used for MetaPn */ + pnIdentifier?: string; } export type BatchPaymentNetworks = diff --git a/packages/payment-processor/test/payment/encoder-approval.test.ts b/packages/payment-processor/test/payment/encoder-approval.test.ts index 69f6d606b..10385946a 100644 --- a/packages/payment-processor/test/payment/encoder-approval.test.ts +++ b/packages/payment-processor/test/payment/encoder-approval.test.ts @@ -55,6 +55,7 @@ const approvalSettings = { const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; const mnemonicPath = `m/44'/60'/0'/0/19`; const paymentAddress = '0x821aEa9a577a9b44299B9c15c88cf3087F3b5544'; +const otherPaymentAddress = '0x821aEa9a577a9b44299B9c15c88cf3087F3b5545'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); let wallet = Wallet.fromMnemonic(mnemonic, mnemonicPath).connect(provider); const erc20ApprovalData = (proxy: string, approvedHexValue?: BigNumber) => { @@ -228,6 +229,61 @@ const validRequestEthConversionProxy: ClientTypes.IRequestData = { }, }; +export const validMetaRequest: ClientTypes.IRequestData = { + ...validRequestEthConversionProxy, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.META]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + salt1: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress, + salt: 'salt1', + network: 'private', + acceptedTokens: [erc20ContractAddress], + }, + version: '0.1.0', + }, + salt2: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress: otherPaymentAddress, + salt: 'salt2', + network: 'private', + }, + version: '0.1.0', + }, + salt3: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress: otherPaymentAddress, + salt: 'salt3', + network: 'mainnet', + acceptedTokens: [erc20ContractAddress], + }, + version: '0.1.0', + }, + }, + version: '0.1.0', + }, + }, +}; + beforeAll(async () => { const mainAddress = wallet.address; wallet = Wallet.fromMnemonic(mnemonic).connect(provider); @@ -595,3 +651,123 @@ describe('Approval encoder handles Eth Requests', () => { expect(approvalTransaction).toBeUndefined(); }); }); + +describe('Approval encoder handles Meta PN', () => { + describe('Error cases', () => { + it('Should not be possible to encode a transaction when passing an invalid pn identifier', async () => { + await expect( + encodeRequestErc20ApprovalIfNeeded(validMetaRequest, provider, wallet.address, { + conversion: alphaConversionSettings, + pnIdentifier: 'unknown', + }), + ).rejects.toThrowError('Invalid pn identifier'); + }); + + it('Should not be possible to encode a transaction without passing a pn identifier', async () => { + await expect( + encodeRequestErc20ApprovalIfNeeded(validMetaRequest, provider, wallet.address, { + conversion: alphaConversionSettings, + }), + ).rejects.toThrowError('Missing pn identifier'); + }); + + it('Should not be possible to encode a conversion transaction without passing conversion options', async () => { + await expect( + encodeRequestErc20ApprovalIfNeeded( + validRequestERC20ConversionProxy, + provider, + wallet.address, + { + pnIdentifier: 'salt1', + }, + ), + ).rejects.toThrowError('Conversion settings missing'); + }); + }); + + describe('Approval encoder handles Sub pn for ERC20 Conversion Proxy', () => { + beforeEach(async () => { + proxyERC20Conv = getProxyAddress( + validRequestERC20ConversionProxy, + AnyToERC20PaymentDetector.getDeploymentInformation, + ); + await revokeErc20Approval(proxyERC20Conv, alphaContractAddress, wallet); + + proxyERC20SwapConv = erc20SwapConversionArtifact.getAddress( + validRequestERC20FeeProxy.currencyInfo.network! as CurrencyTypes.EvmChainName, + ); + await revokeErc20Approval(proxyERC20SwapConv, alphaContractAddress, wallet); + }); + + it('Should return a valid transaction', async () => { + let approvalTransaction = await encodeRequestErc20ApprovalIfNeeded( + validMetaRequest, + provider, + wallet.address, + { + conversion: alphaConversionSettings, + pnIdentifier: 'salt1', + }, + ); + + expect(approvalTransaction).toEqual({ + data: erc20ApprovalData(proxyERC20Conv), + to: alphaContractAddress, + value: 0, + }); + }); + it('Should return a valid transaction with specific approval value', async () => { + let approvalTransaction = await encodeRequestErc20ApprovalIfNeeded( + validMetaRequest, + provider, + wallet.address, + { + conversion: alphaConversionSettings, + approval: approvalSettings, + pnIdentifier: 'salt1', + }, + ); + + expect(approvalTransaction).toEqual({ + data: erc20ApprovalData(proxyERC20Conv, arbitraryApprovalValue), + to: alphaContractAddress, + value: 0, + }); + }); + it('Should return undefined - approval already made', async () => { + let approvalTransaction = await encodeRequestErc20ApprovalIfNeeded( + validMetaRequest, + provider, + wallet.address, + { + conversion: alphaConversionSettings, + pnIdentifier: 'salt1', + }, + ); + await wallet.sendTransaction(approvalTransaction as IPreparedTransaction); + + approvalTransaction = await encodeRequestErc20ApprovalIfNeeded( + validMetaRequest, + provider, + wallet.address, + { + conversion: alphaConversionSettings, + pnIdentifier: 'salt1', + }, + ); + expect(approvalTransaction).toBeUndefined(); + }); + + it('Should not return anything - native pn', async () => { + const approvalTransaction = await encodeRequestErc20ApprovalIfNeeded( + validMetaRequest, + provider, + wallet.address, + { + pnIdentifier: 'salt2', + }, + ); + expect(approvalTransaction).toBeUndefined(); + }); + }); +}); diff --git a/packages/payment-processor/test/payment/encoder-payment.test.ts b/packages/payment-processor/test/payment/encoder-payment.test.ts index 58f1ceed1..a322b9163 100644 --- a/packages/payment-processor/test/payment/encoder-payment.test.ts +++ b/packages/payment-processor/test/payment/encoder-payment.test.ts @@ -65,6 +65,7 @@ const alphaSwapConversionSettings = { const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; +const otherPaymentAddress = '0xfA9154CAA55c83a6941696785B1cae6386611721'; const expectedFlowRate = '100000'; const expectedStartDate = '1643041225'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); @@ -251,6 +252,61 @@ const validRequestEthConversionProxy: ClientTypes.IRequestData = { }, }; +export const validMetaRequest: ClientTypes.IRequestData = { + ...validRequestEthConversionProxy, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.META]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + salt: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress, + salt: 'salt', + network: 'private', + acceptedTokens: [alphaContractAddress], + }, + version: '0.1.0', + }, + salt2: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress: otherPaymentAddress, + salt: 'salt2', + network: 'private', + }, + version: '0.1.0', + }, + salt3: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '2', + paymentAddress: otherPaymentAddress, + salt: 'salt3', + network: 'mainnet', + acceptedTokens: [erc20ContractAddress], + }, + version: '0.1.0', + }, + }, + version: '0.1.0', + }, + }, +}; + describe('Payment encoder handles ERC20 Proxy', () => { it('Should return a valid transaction', async () => { const paymentTransaction = encodeRequestPayment(baseValidRequest, provider); @@ -446,3 +502,90 @@ describe('Payment encoder handles ERC777 Stream', () => { }); }); }); + +describe('Payment encoder handles Meta PN', () => { + describe('Error cases', () => { + it('Should not be possible to encode a transaction when passing an invalid pn identifier', () => { + expect(() => + encodeRequestPayment(validMetaRequest, provider, { + conversion: alphaConversionSettings, + pnIdentifier: 'unknown', + }), + ).toThrowError('Invalid pn identifier'); + }); + + it('Should not be possible to encode a transaction without passing a pn identifier', () => { + expect(() => + encodeRequestPayment(validMetaRequest, provider, { + conversion: alphaConversionSettings, + }), + ).toThrowError('Missing pn identifier'); + }); + + it('Should not be possible to encode a conversion transaction without passing conversion options', () => { + expect(() => + encodeRequestPayment(validRequestERC20ConversionProxy, provider, { + pnIdentifier: 'salt1', + }), + ).toThrowError('Conversion settings missing'); + }); + }); + + describe('Payment encoder handles Sub pn for ERC20 Conversion Proxy', () => { + it('Should return a valid transaction', async () => { + const paymentTransaction = encodeRequestPayment(validMetaRequest, provider, { + conversion: alphaConversionSettings, + pnIdentifier: 'salt', + }); + + const proxyAddress = getProxyAddress( + validRequestERC20ConversionProxy, + AnyToERC20PaymentDetector.getDeploymentInformation, + ); + + // The data payload is the same in the ERC20 Conversion proxy case (standalone PN) as requestId, salt and paymentAddress are the same + expect(paymentTransaction).toEqual({ + data: '0x3af2c012000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000', + to: proxyAddress, + value: 0, + }); + }); + + it('Should not be possible to encode a conversion transaction without passing conversion options', () => { + expect(() => + encodeRequestPayment(validRequestERC20ConversionProxy, provider, { + pnIdentifier: 'salt', + }), + ).toThrowError('Conversion settings missing'); + }); + }); + + describe('Payment encoder handles Sub pn for ETH Conversion Proxy', () => { + it('Should return a valid transaction', async () => { + let paymentTransaction = await encodeRequestPayment(validMetaRequest, provider, { + conversion: ethConversionSettings, + pnIdentifier: 'salt2', + }); + + const proxyAddress = getProxyAddress( + validRequestEthConversionProxy, + AnyToEthFeeProxyPaymentDetector.getDeploymentInformation, + ); + + // The data payload is not the same in the ETH Conversion proxy case (standalone PN) as salt and paymentAddress are different + expect(paymentTransaction).toEqual({ + data: '0xac473c8a000000000000000000000000fa9154caa55c83a6941696785b1cae63866117210000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e8480000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b000000000000000000000000a65ded58a0afee8241e788c5115ca53ef3925fd2000000000000000000000000000000000000000000000000000000000000000863033c01472fcb07000000000000000000000000000000000000000000000000', + to: proxyAddress, + value: BigNumber.from(ethConversionSettings.maxToSpend), + }); + }); + + it('Should not be possible to encode a conversion transaction without passing conversion options', () => { + expect(() => + encodeRequestPayment(validMetaRequest, provider, { + pnIdentifier: 'salt2', + }), + ).toThrowError('Conversion settings missing'); + }); + }); +}); From 6dae29f11757bbf685b6fa637a428527927d3fbe Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 20:58:19 +0200 Subject: [PATCH 10/20] clean --- packages/advanced-logic/src/extensions/payment-network/meta.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index 39861e317..c13405a81 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -151,7 +151,6 @@ export default class MetaPaymentNetwork< const copiedExtensionState: ExtensionTypes.IState = deepCopy(extensionState); const { pnIdentifier, action, parameters } = extensionAction.parameters; const extensionToActOn: ExtensionTypes.IState = copiedExtensionState.values[pnIdentifier]; - // increment sentPaymentAmount const pn = this.getExtension(extensionToActOn.id); if (!pn) throw new Error('Invalid PN'); @@ -210,8 +209,6 @@ export default class MetaPaymentNetwork< } pnIdentifiers.push(param.salt); } - - request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META]?.values; }); } else if (extensionAction.action === ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN) { const { pnIdentifier } = extensionAction.parameters; From 4a15e87a37856a1b7aae24bb13c1d4e5d7223c34 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 4 Jun 2024 21:38:58 +0200 Subject: [PATCH 11/20] fix currency types --- .../src/extensions/payment-network/meta.ts | 10 +++++++--- .../payment-detection/src/meta-payment-detector.ts | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index c13405a81..11bd70552 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -1,5 +1,9 @@ -import { ICurrencyManager } from '@requestnetwork/currency'; -import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { + CurrencyTypes, + ExtensionTypes, + IdentityTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; import { ICreationContext } from '../abstract-extension'; import AnyToErc20ProxyPaymentNetwork from './any-to-erc20-proxy'; import AnyToEthProxyPaymentNetwork from './any-to-eth-proxy'; @@ -13,7 +17,7 @@ export default class MetaPaymentNetwork< ExtensionTypes.PnMeta.ICreationParameters = ExtensionTypes.PnMeta.ICreationParameters, > extends DeclarativePaymentNetwork { public constructor( - protected currencyManager: ICurrencyManager, + protected currencyManager: CurrencyTypes.ICurrencyManager, public extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID.META, public currentVersion: string = CURRENT_VERSION, ) { diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index 4486bef89..cec50f587 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -1,10 +1,10 @@ import { AdvancedLogicTypes, + CurrencyTypes, ExtensionTypes, PaymentTypes, RequestLogicTypes, } from '@requestnetwork/types'; -import { ICurrencyManager } from '@requestnetwork/currency'; import { deepCopy, generate8randomBytes } from '@requestnetwork/utils'; import { AnyToERC20PaymentDetector, AnyToEthFeeProxyPaymentDetector } from './any'; import { @@ -42,7 +42,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< | PaymentTypes.IDeclarativePaymentEventParameters > { private readonly advancedLogic: AdvancedLogicTypes.IAdvancedLogic; - private readonly currencyManager: ICurrencyManager; + private readonly currencyManager: CurrencyTypes.ICurrencyManager; private readonly options: Partial; /** * @param paymentNetworkId Example : ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA From d94e7c3379208c115c6f973475e04abe08ec1576 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 11 Jun 2024 03:28:28 +0200 Subject: [PATCH 12/20] integration tests --- .../integration-test/test/node-client.test.ts | 192 +++++++++++++++--- .../src/meta-payment-detector.ts | 9 +- .../src/payment-network-factory.ts | 1 + packages/types/src/payment-types.ts | 4 + 4 files changed, 166 insertions(+), 40 deletions(-) diff --git a/packages/integration-test/test/node-client.test.ts b/packages/integration-test/test/node-client.test.ts index be84ca486..d3ffe5d9b 100644 --- a/packages/integration-test/test/node-client.test.ts +++ b/packages/integration-test/test/node-client.test.ts @@ -12,6 +12,8 @@ import { import { payRequest, approveErc20ForProxyConversionIfNeeded, + encodeRequestErc20Approval, + encodeRequestPayment, } from '@requestnetwork/payment-processor'; import { CurrencyManager } from '@requestnetwork/currency'; @@ -54,6 +56,37 @@ const encryptionData = { '299708c07399c9b28e9870c4e643742f65c94683f35d1b3fc05d0478344ee0cc5a6a5e23f78b5ff8c93a04254232b32350c8672d2873677060d5095184dad422', }; +// Currencies and Currency Manager +const tokenContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; +const localDai = { + type: Types.RequestLogic.CURRENCY.ERC20, + value: tokenContractAddress, + network: 'private' as CurrencyTypes.ChainName, +}; +const localEth = { + type: Types.RequestLogic.CURRENCY.ETH, + value: 'ETH', + network: 'private' as CurrencyTypes.ChainName, +}; + +const currencies: CurrencyTypes.CurrencyInput[] = [ + ...CurrencyManager.getDefaultList(), + { + network: 'private', + symbol: 'ETH', + decimals: 18, + type: RequestLogicTypes.CURRENCY.ETH, + }, + { + address: tokenContractAddress, + decimals: 18, + network: 'private', + symbol: 'localDAI', + type: RequestLogicTypes.CURRENCY.ERC20, + }, +]; +const currencyManager = new CurrencyManager(currencies); + // Decryption provider setup const decryptionProvider = new EthereumPrivateKeyDecryptionProvider( encryptionData.decryptionParams, @@ -560,22 +593,10 @@ describe('ERC20 localhost request creation and detection test', () => { }); it('can create ERC20 requests with any to erc20 proxy', async () => { - const tokenContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; - - const currencies: CurrencyTypes.CurrencyInput[] = [ - ...CurrencyManager.getDefaultList(), - { - address: tokenContractAddress, - decimals: 18, - network: 'private', - symbol: 'localDAI', - type: RequestLogicTypes.CURRENCY.ERC20, - }, - ]; const requestNetwork = new RequestNetwork({ signatureProvider, useMockStorage: true, - currencyManager: new CurrencyManager(currencies), + currencyManager, }); const paymentNetworkAnyToERC20: PaymentTypes.PaymentNetworkCreateParameters = { @@ -612,13 +633,9 @@ describe('ERC20 localhost request creation and detection test', () => { // USD => token const maxToSpend = BigNumber.from(2).pow(255); const paymentTx = await payRequest(data, wallet, undefined, undefined, { - currency: { - type: Types.RequestLogic.CURRENCY.ERC20, - value: tokenContractAddress, - network: 'private', - }, + currency: localDai, maxToSpend, - currencyManager: new CurrencyManager(currencies), + currencyManager, }); await paymentTx.wait(); @@ -655,11 +672,10 @@ describe('ETH localhost request creation and detection test', () => { }; it('can create ETH requests and pay with ETH Fee proxy and cancel after paid', async () => { - const currencies = [...CurrencyManager.getDefaultList()]; const requestNetwork = new RequestNetwork({ signatureProvider, useMockStorage: true, - currencyManager: new CurrencyManager(currencies), + currencyManager, }); const paymentNetworkETHFeeProxy: PaymentTypes.PaymentNetworkCreateParameters = { @@ -703,20 +719,10 @@ describe('ETH localhost request creation and detection test', () => { }); it('can create & pay a request with any to eth proxy', async () => { - const currencies: CurrencyTypes.CurrencyInput[] = [ - ...CurrencyManager.getDefaultList(), - { - network: 'private', - symbol: 'ETH', - decimals: 18, - type: RequestLogicTypes.CURRENCY.ETH, - }, - ]; - const requestNetwork = new RequestNetwork({ signatureProvider, useMockStorage: true, - currencyManager: new CurrencyManager(currencies), + currencyManager, }); const paymentNetworkAnyToETH: PaymentTypes.PaymentNetworkCreateParameters = { @@ -743,7 +749,7 @@ describe('ETH localhost request creation and detection test', () => { const maxToSpend = '30000000000000000'; const paymentTx = await payRequest(data, wallet, undefined, undefined, { maxToSpend, - currencyManager: new CurrencyManager(currencies), + currencyManager, }); await paymentTx.wait(); @@ -773,3 +779,123 @@ describe('ETH localhost request creation and detection test', () => { expect(event?.parameters?.maxRateTimespan).toBe('1000000'); }); }); + +describe('Localhost Meta request creation and detection test', () => { + const anyToErc20Salt = 'ea3bc7caf64110cb'; + const anyToEthSalt = 'ea3bc7caf64110ca'; + const anyToErc20PaymentAddress = '0xfA9154CAA55c83a6941696785B1cae6386611721'; + const anyToEthPaymentAddress = '0xc12F17Da12cd01a9CDBB216949BA0b41A6Ffc4EB'; + const metaPaymentNetworkParameters: PaymentTypes.PaymentNetworkCreateParameters = { + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: [ + { + paymentAddress: anyToEthPaymentAddress, + feeAddress: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2', + feeAmount: '200', + network: 'private', + salt: anyToEthSalt, + }, + ], + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { + paymentAddress: anyToErc20PaymentAddress, + refundAddress: '0x821aEa9a577a9b44299B9c15c88cf3087F3b5544', + feeAddress: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2', + feeAmount: '100', + network: 'private', + acceptedTokens: [tokenContractAddress], + maxRateTimespan: 1000000, + salt: anyToErc20Salt, + }, + ], + }, + }; + + it('can create meta-pn requests and pay with any-to-erc20', async () => { + const requestNetwork = new RequestNetwork({ + signatureProvider, + useMockStorage: true, + currencyManager, + }); + + const request = await requestNetwork.createRequest({ + paymentNetwork: metaPaymentNetworkParameters, + requestInfo: requestCreationHashUSD, + signer: payeeIdentity, + }); + + let data = await request.refresh(); + + const maxToSpend = BigNumber.from(2).pow(255); + const settings = { + conversion: { + maxToSpend, + currencyManager, + currency: localDai, + }, + pnIdentifier: anyToErc20Salt, + }; + + const approvalTx = encodeRequestErc20Approval(data, provider, settings); + if (approvalTx) { + await wallet.sendTransaction(approvalTx); + } + + const paymentTx = await encodeRequestPayment(data, provider, settings); + await wallet.sendTransaction(paymentTx); + + data = await request.refresh(); + expect(data.balance?.error).toBeUndefined(); + expect(data.balance?.balance).toBe('1000'); + expect(data.balance?.events.length).toBe(1); + const event = data.balance?.events[0]; + expect(event?.amount).toBe('1000'); + expect(event?.name).toBe('payment'); + + expect(event?.parameters?.feeAmount).toBe('100'); + expect(event?.parameters?.feeAddress).toBe('0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2'); + expect(event?.parameters?.to).toBe(anyToErc20PaymentAddress); + }); + + it('can create meta-pn requests and pay with any-to-eth', async () => { + const requestNetwork = new RequestNetwork({ + signatureProvider, + useMockStorage: true, + currencyManager, + }); + + const request = await requestNetwork.createRequest({ + paymentNetwork: metaPaymentNetworkParameters, + requestInfo: requestCreationHashUSD, + signer: payeeIdentity, + }); + + let data = await request.refresh(); + + const maxToSpend = '30000000000000000'; + const settings = { + conversion: { + maxToSpend, + currencyManager, + currency: localEth, + }, + pnIdentifier: anyToEthSalt, + }; + + const paymentTx = await encodeRequestPayment(data, provider, settings); + await wallet.sendTransaction(paymentTx); + + data = await request.refresh(); + expect(data.balance?.error).toBeUndefined(); + expect(data.balance?.balance).toBe('1000'); + expect(data.balance?.events.length).toBe(1); + const event = data.balance?.events[0]; + expect(event?.amount).toBe('1000'); + expect(event?.name).toBe('payment'); + + expect(event?.parameters?.feeAmount).toBe('200'); + expect(event?.parameters?.feeAddress).toBe('0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2'); + expect(event?.parameters?.to).toBe(anyToEthPaymentAddress); + }); +}); diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index cec50f587..890487997 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -5,7 +5,7 @@ import { PaymentTypes, RequestLogicTypes, } from '@requestnetwork/types'; -import { deepCopy, generate8randomBytes } from '@requestnetwork/utils'; +import { deepCopy } from '@requestnetwork/utils'; import { AnyToERC20PaymentDetector, AnyToEthFeeProxyPaymentDetector } from './any'; import { IPaymentNetworkModuleByType, @@ -69,10 +69,6 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< public async createExtensionsDataForCreation( paymentNetworkCreationParameters: ExtensionTypes.PnMeta.ICreationParameters, ): Promise { - // If no salt is given, generate one - paymentNetworkCreationParameters.salt = - paymentNetworkCreationParameters.salt || (await generate8randomBytes()); - // Do the same for each sub-extension for (const [key, value] of Object.entries(paymentNetworkCreationParameters)) { if (supportedPns.includes(key as ExtensionTypes.PAYMENT_NETWORK_ID)) { @@ -96,7 +92,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< for (let index = 0; index < value.length; index++) { paymentNetworkCreationParameters[key as keyof ExtensionTypes.PnMeta.ICreationParameters][ index - ] = await detector.createExtensionsDataForCreation(value[index]); + ] = (await detector.createExtensionsDataForCreation(value[index])).parameters; } } } @@ -135,7 +131,6 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< const paymentExtension = this.getPaymentExtension(request); const events: PaymentTypes.IBalanceWithEvents[] = []; const feeBalances: PaymentTypes.IBalanceWithEvents[] = []; - this.checkRequiredParameter(paymentExtension.values.salt, 'salt'); for (const value of Object.values( paymentExtension.values as Record>, diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 9a7f279f9..fc2d4d505 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -152,6 +152,7 @@ export class PaymentNetworkFactory { network, advancedLogic: this.advancedLogic, currencyManager: this.currencyManager, + options: this.options, ...this.options, }); diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index d73d1f728..807dfb80c 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -100,6 +100,10 @@ export type PaymentNetworkCreateParameters = | { id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_ADDRESS_BASED; parameters: ExtensionTypes.PnAddressBased.ICreationParameters; + } + | { + id: ExtensionTypes.PAYMENT_NETWORK_ID.META; + parameters: ExtensionTypes.PnMeta.ICreationParameters; }; /** From 06ff3ba66b2660953fe3a283dc5735e3b0c782b6 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 11 Jun 2024 03:54:56 +0200 Subject: [PATCH 13/20] fix payment-detection tests --- .../src/extensions/payment-network/meta.ts | 39 +++++++++--------- .../test/meta-payment-network.test.ts | 41 +++---------------- 2 files changed, 24 insertions(+), 56 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index 11bd70552..f8806416a 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -42,8 +42,6 @@ export default class MetaPaymentNetwork< Object.entries(creationParameters).forEach(([pnId, creationParameters]) => { const pn = this.getExtension(pnId); - if (!pn) throw new Error('Invalid PN'); - // Perform validation on sub-pn creation parameters for (const param of creationParameters) { pn.createCreationAction(param); @@ -102,7 +100,6 @@ export default class MetaPaymentNetwork< const values: Record = {}; Object.entries(extensionAction.parameters).forEach(([pnId, parameters]) => { const pn = this.getExtension(pnId); - if (!pn) throw new Error('Invalid PN'); (parameters as any[]).forEach((params) => { values[params.salt] = pn.applyActionToExtension( @@ -157,7 +154,6 @@ export default class MetaPaymentNetwork< const extensionToActOn: ExtensionTypes.IState = copiedExtensionState.values[pnIdentifier]; const pn = this.getExtension(extensionToActOn.id); - if (!pn) throw new Error('Invalid PN'); const subExtensionState = { [extensionToActOn.id]: extensionToActOn, @@ -200,8 +196,8 @@ export default class MetaPaymentNetwork< const pnIdentifiers: string[] = []; if (extensionAction.action === ExtensionTypes.PnMeta.ACTION.CREATE) { Object.entries(extensionAction.parameters).forEach(([pnId, parameters]: [string, any]) => { - const pn = this.getExtension(pnId); - if (!pn) throw new Error('Invalid PN'); + // Checks that the PN is supported + this.getExtension(pnId); if (parameters.action) { throw new Error('Invalid action'); @@ -223,22 +219,25 @@ export default class MetaPaymentNetwork< throw new Error(`No payment network with identifier ${pnIdentifier}`); } - const pn = this.getExtension(subPnState.id); - if (!pn) { - throw new Error(`Payment network ${subPnState.id} not supported`); - } + // Checks that the PN is supported + this.getExtension(subPnState.id); } } - private getExtension(pnId: string): ExtensionTypes.IExtension | undefined { - return { - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: new DeclarativePaymentNetwork(), - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: new AnyToErc20ProxyPaymentNetwork( - this.currencyManager, - ), - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: new AnyToEthProxyPaymentNetwork( - this.currencyManager, - ), - }[pnId]; + private getExtension(pnId: string): ExtensionTypes.IExtension { + switch (pnId) { + case ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE: { + return new DeclarativePaymentNetwork(); + } + case ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY: { + return new AnyToErc20ProxyPaymentNetwork(this.currencyManager); + } + case ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY: { + return new AnyToEthProxyPaymentNetwork(this.currencyManager); + } + default: { + throw new Error(`Invalid PN: ${pnId}`); + } + } } } diff --git a/packages/payment-detection/test/meta-payment-network.test.ts b/packages/payment-detection/test/meta-payment-network.test.ts index b2d6c26e0..8f5b08158 100644 --- a/packages/payment-detection/test/meta-payment-network.test.ts +++ b/packages/payment-detection/test/meta-payment-network.test.ts @@ -19,7 +19,11 @@ const theGraphClientMock = { let detector: MetaDetector; const createCreationActionMeta = jest.fn(); -const createCreationActionAnyToErc20 = jest.fn(); +const createCreationActionAnyToErc20 = jest.fn().mockImplementation((parameters) => { + return { + parameters, + }; +}); const createAddPaymentInstructionAction = jest.fn(); const createAddRefundInstructionAction = jest.fn(); const createDeclareReceivedPaymentAction = jest.fn(); @@ -136,41 +140,6 @@ describe('api/meta-payment-network', () => { 'createCreationAction', ); - await detector.createExtensionsDataForCreation({ - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ - { - feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', - feeAmount: '5', - paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', - refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', - acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], - network: 'matic', - salt: 'abcd', - }, - { - feeAddress: '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', - feeAmount: '5', - paymentAddress: '0xf17f52151EbEF6C7334FAD080c5704D77216b732', - refundAddress: '0x666666151EbEF6C7334FAD080c5704D77216b732', - acceptedTokens: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], - network: 'mainnet', - salt: 'efgh', - }, - ], - salt: 'anySalt', - }); - - expect(spyMeta).toHaveBeenCalledTimes(1); - expect(spySubPn).toHaveBeenCalledTimes(2); - }); - - it('can createExtensionsDataForCreation without salt', async () => { - const spyMeta = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createCreationAction'); - const spySubPn = jest.spyOn( - mockAdvancedLogic.extensions.anyToErc20Proxy, - 'createCreationAction', - ); - await detector.createExtensionsDataForCreation({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { From 10934ba98186871905020fccc8f2edef636ef030 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 11 Jun 2024 03:58:24 +0200 Subject: [PATCH 14/20] improve mock --- .../any-to-erc20-proxy-create-data-generator.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts index 9ff99fd90..2a1beea7d 100644 --- a/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts +++ b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts @@ -82,11 +82,8 @@ export const extensionFullState = ( feeAddress, feeAmount, network, - paymentAddress: paymentAddressOverride - ? paymentAddressOverride - : paymentAddressOverride === null - ? undefined - : paymentAddress, + paymentAddress: + paymentAddressOverride === null ? undefined : paymentAddressOverride ?? paymentAddress, refundAddress, salt: saltOverride || salt, acceptedTokens: [tokenAddress], @@ -101,11 +98,8 @@ export const extensionFullState = ( feeAddress, feeAmount, network, - paymentAddress: paymentAddressOverride - ? paymentAddressOverride - : paymentAddressOverride === null - ? undefined - : paymentAddress, + paymentAddress: + paymentAddressOverride === null ? undefined : paymentAddressOverride ?? paymentAddress, refundAddress, salt: saltOverride || salt, acceptedTokens: [tokenAddress], From 5542c701c382b070b015d2fdf2941b66bc4f59df Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Tue, 11 Jun 2024 04:06:48 +0200 Subject: [PATCH 15/20] fix comments and error messages --- .../payment-detection/src/meta-payment-detector.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index 890487997..1d9be7791 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -34,7 +34,8 @@ const advancedLogicMap = { }; /** - * Abstract class to extend to get the payment balance of reference based requests + * Detect payment for the meta payment network. + * Recursively detects payments on each sub payment network */ export class MetaDetector extends DeclarativePaymentDetectorBase< ExtensionTypes.PnMeta.IMeta, @@ -45,8 +46,8 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< private readonly currencyManager: CurrencyTypes.ICurrencyManager; private readonly options: Partial; /** - * @param paymentNetworkId Example : ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA - * @param extension The advanced logic payment network extension, reference based + * @param paymentNetworkId + * @param extension */ public constructor({ advancedLogic, @@ -61,7 +62,6 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< /** * Creates the extensions data for the creation of this extension. - * Will set a salt if none is already given * * @param paymentNetworkCreationParameters Parameters to create the extension * @returns The extensionData object @@ -78,7 +78,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< this.advancedLogic.extensions[extensionKey as keyof typeof this.advancedLogic.extensions]; if (!detectorClass || !extension) { - throw new Error(`the payment network id: ${key} is not supported`); + throw new Error(`The payment network id: ${key} is not supported for meta-pn detection`); } const detector = new detectorClass({ @@ -142,7 +142,9 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< this.advancedLogic.extensions[extensionKey as keyof typeof this.advancedLogic.extensions]; if (!detectorClass || !extension) { - throw new Error(`the payment network id: ${value.id} is not supported`); + throw new Error( + `The payment network id: ${value.id} is not supported for meta-pn detection`, + ); } const detector = new detectorClass({ From d4656250f6d91e32dc25d3aa3026157868d9efae Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Wed, 12 Jun 2024 15:08:51 +0200 Subject: [PATCH 16/20] fix: small fixes --- .../src/extensions/payment-network/meta.ts | 5 ++++ .../extensions/payment-network/meta.test.ts | 29 ++++++++++++++----- .../src/meta-payment-detector.ts | 13 ++++++--- .../payment-processor/src/payment/utils.ts | 2 +- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index f8806416a..ca04f5862 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -41,10 +41,15 @@ export default class MetaPaymentNetwork< ): ExtensionTypes.IAction { Object.entries(creationParameters).forEach(([pnId, creationParameters]) => { const pn = this.getExtension(pnId); + const subPnIdentifiers: string[] = []; // Perform validation on sub-pn creation parameters for (const param of creationParameters) { pn.createCreationAction(param); + if (subPnIdentifiers.includes(param.salt)) { + throw new Error('Duplicate payment network identifier (salt)'); + } + subPnIdentifiers.push(param.salt); } }); diff --git a/packages/advanced-logic/test/extensions/payment-network/meta.test.ts b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts index b849cf712..b0d243e96 100644 --- a/packages/advanced-logic/test/extensions/payment-network/meta.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts @@ -18,6 +18,10 @@ const baseParams = { acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], maxRateTimespan: 1000000, } as ExtensionTypes.PnAnyToErc20.ICreationParameters; +const otherBaseParams = { + ...baseParams, + salt: 'ea3bc7caf64110cb', +} as ExtensionTypes.PnAnyToErc20.ICreationParameters; /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('extensions/payment-network/meta', () => { @@ -25,13 +29,13 @@ describe('extensions/payment-network/meta', () => { it('can create a create action with all parameters', () => { expect( metaPn.createCreationAction({ - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, otherBaseParams], }), ).toEqual({ action: 'create', id: ExtensionTypes.PAYMENT_NETWORK_ID.META, parameters: { - [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, otherBaseParams], }, version: '0.1.0', }); @@ -42,7 +46,7 @@ describe('extensions/payment-network/meta', () => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, feeAddress: undefined, feeAmount: undefined }, - baseParams, + otherBaseParams, ], }), ).toEqual({ @@ -51,20 +55,29 @@ describe('extensions/payment-network/meta', () => { parameters: { [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, feeAddress: undefined, feeAmount: undefined }, - baseParams, + otherBaseParams, ], }, version: '0.1.0', }); }); + it('cannot createCreationAction with duplicated salt', () => { + // 'must throw' + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], + }); + }).toThrowError('Duplicate payment network identifier (salt)'); + }); + it('cannot createCreationAction with payment address not an ethereum address', () => { // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, paymentAddress: 'not an ethereum address' }, - baseParams, + otherBaseParams, ], }); }).toThrowError("paymentAddress 'not an ethereum address' is not a valid address"); @@ -76,7 +89,7 @@ describe('extensions/payment-network/meta', () => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, refundAddress: 'not an ethereum address' }, - baseParams, + otherBaseParams, ], }); }).toThrowError("refundAddress 'not an ethereum address' is not a valid address"); @@ -88,7 +101,7 @@ describe('extensions/payment-network/meta', () => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, feeAddress: 'not an ethereum address' }, - baseParams, + otherBaseParams, ], }); }).toThrowError('feeAddress is not a valid address'); @@ -100,7 +113,7 @@ describe('extensions/payment-network/meta', () => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ { ...baseParams, feeAmount: '-2000' }, - baseParams, + otherBaseParams, ], }); }).toThrowError('feeAmount is not a valid amount'); diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index 1d9be7791..d3ac5371d 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -15,7 +15,7 @@ import { import { DeclarativePaymentDetector, DeclarativePaymentDetectorBase } from './declarative'; import { BigNumber } from 'ethers'; -const supportedPns = [ +const supportedPns: (keyof ExtensionTypes.PnMeta.ICreationParameters)[] = [ ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE, ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY, @@ -27,7 +27,12 @@ const detectorMap: IPaymentNetworkModuleByType = { [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: AnyToEthFeeProxyPaymentDetector, }; -const advancedLogicMap = { +const advancedLogicMap: Partial< + Record< + keyof ExtensionTypes.PnMeta.ICreationParameters, + keyof AdvancedLogicTypes.IAdvancedLogicExtensions + > +> = { [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_DECLARATIVE]: 'declarative', [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: 'anyToErc20Proxy', [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: 'anyToEthProxy', @@ -71,7 +76,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< ): Promise { // Do the same for each sub-extension for (const [key, value] of Object.entries(paymentNetworkCreationParameters)) { - if (supportedPns.includes(key as ExtensionTypes.PAYMENT_NETWORK_ID)) { + if (supportedPns.includes(key as keyof ExtensionTypes.PnMeta.ICreationParameters)) { const detectorClass = detectorMap[key as keyof typeof detectorMap]; const extensionKey = advancedLogicMap[key as keyof typeof advancedLogicMap]; const extension = @@ -135,7 +140,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< for (const value of Object.values( paymentExtension.values as Record>, )) { - if (supportedPns.includes(value.id as ExtensionTypes.PAYMENT_NETWORK_ID)) { + if (supportedPns.includes(value.id as keyof ExtensionTypes.PnMeta.ICreationParameters)) { const detectorClass = detectorMap[value.id as keyof typeof detectorMap]; const extensionKey = advancedLogicMap[value.id as keyof typeof advancedLogicMap]; const extension = diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 2adaaa35c..8269adcc9 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -425,7 +425,7 @@ export async function revokeErc20Approval( /** * Format a request we wish to build a payment for. - * If the request does not use the meta-pn, returns it as it. + * If the request does not use the meta-pn, it returns it as is. * Otherwise, returns the request formatted with the pn of interest */ export function getFormattedRequest({ From ee3520ce473daa2235b0a0b687c417388d861012 Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Fri, 14 Jun 2024 11:31:29 +0200 Subject: [PATCH 17/20] fix comments --- .../src/extensions/payment-network/meta.ts | 9 --------- packages/payment-detection/src/meta-payment-detector.ts | 9 +++------ .../payment-processor/src/payment/encoder-approval.ts | 4 ++-- .../payment-processor/src/payment/encoder-payment.ts | 2 +- packages/payment-processor/src/payment/utils.ts | 9 ++++----- packages/types/src/extensions/pn-meta.ts | 5 ++++- 6 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/meta.ts b/packages/advanced-logic/src/extensions/payment-network/meta.ts index ca04f5862..77e3a4af5 100644 --- a/packages/advanced-logic/src/extensions/payment-network/meta.ts +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -85,15 +85,6 @@ export default class MetaPaymentNetwork< * * @returns state of the extension created */ - protected applyCreation( - extensionAction: ExtensionTypes.IAction, - timestamp: number, - ): ExtensionTypes.IState; - protected applyCreation( - extensionAction: ExtensionTypes.IAction, - timestamp: number, - context: ICreationContext, - ): ExtensionTypes.IState; protected applyCreation( extensionAction: ExtensionTypes.IAction, timestamp: number, diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index d3ac5371d..3802f1bb6 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -50,15 +50,12 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< private readonly advancedLogic: AdvancedLogicTypes.IAdvancedLogic; private readonly currencyManager: CurrencyTypes.ICurrencyManager; private readonly options: Partial; - /** - * @param paymentNetworkId - * @param extension - */ + public constructor({ advancedLogic, currencyManager, options, - }: ReferenceBasedDetectorOptions & { options?: Partial }) { + }: ReferenceBasedDetectorOptions & { options: Partial }) { super(ExtensionTypes.PAYMENT_NETWORK_ID.META, advancedLogic.extensions.metaPn); this.options = options || {}; this.currencyManager = currencyManager; @@ -109,7 +106,7 @@ export class MetaDetector extends DeclarativePaymentDetectorBase< /** * Creates the extensions data to apply an action on a sub pn * - * @param Parameters to add refund information + * @param Parameters to apply an action on a sub pn * @returns The extensionData object */ public createExtensionsDataForApplyActionOnPn( diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index d5369482f..20553a78d 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -28,7 +28,7 @@ export async function encodeRequestErc20ApprovalIfNeeded( from: string, options?: IRequestPaymentOptions, ): Promise { - const formattedRequest = getFormattedRequest({ request, options }); + const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestErc20ApprovalWithSwapIfNeeded(formattedRequest, provider, from, options); } else { @@ -47,7 +47,7 @@ export function encodeRequestErc20Approval( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction | void { - const formattedRequest = getFormattedRequest({ request, options }); + const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestErc20ApprovalWithSwap(formattedRequest, provider, options); } else { diff --git a/packages/payment-processor/src/payment/encoder-payment.ts b/packages/payment-processor/src/payment/encoder-payment.ts index bfc239caa..554065a39 100644 --- a/packages/payment-processor/src/payment/encoder-payment.ts +++ b/packages/payment-processor/src/payment/encoder-payment.ts @@ -26,7 +26,7 @@ export function encodeRequestPayment( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction { - const formattedRequest = getFormattedRequest({ request, options }); + const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestPaymentWithSwap(formattedRequest, provider, options); } else { diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 8269adcc9..c34453736 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -11,7 +11,6 @@ import { EvmChains, getCurrencyHash } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; import { getReceivableTokenIdForRequest } from './erc20-transferable-receivable'; -import { IRequestPaymentOptions } from '../types'; import { deepCopy } from '@requestnetwork/utils'; /** @constant MAX_ALLOWANCE set to the max uint256 value */ @@ -430,16 +429,16 @@ export async function revokeErc20Approval( */ export function getFormattedRequest({ request, - options, + pnIdentifier, }: { request: ClientTypes.IRequestData; - options?: IRequestPaymentOptions; + pnIdentifier?: string; }): ClientTypes.IRequestData { const pn = getPaymentNetworkExtension(request); if (!pn?.id || pn.id !== ExtensionTypes.PAYMENT_NETWORK_ID.META) return request; - if (!options?.pnIdentifier) throw new Error('Missing pn identifier'); + if (!pnIdentifier) throw new Error('Missing pn identifier'); - const extensionOfInterest: ExtensionTypes.IState | undefined = pn.values[options.pnIdentifier]; + const extensionOfInterest: ExtensionTypes.IState | undefined = pn.values[pnIdentifier]; if (!extensionOfInterest) throw new Error('Invalid pn identifier'); const formattedRequest = { diff --git a/packages/types/src/extensions/pn-meta.ts b/packages/types/src/extensions/pn-meta.ts index 435b9a442..7860e2ad5 100644 --- a/packages/types/src/extensions/pn-meta.ts +++ b/packages/types/src/extensions/pn-meta.ts @@ -20,7 +20,10 @@ export interface ICreationParameters extends PnAnyDeclarative.ICreationParameter [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]?: PnAnyToEth.ICreationParameters[]; } -/** Parameters of declareSentPayment and declareSentRefund action */ +/** + * Parameters of of apply-action-to-pn action + * Supports all actions supported by sub payment networks + */ export interface IApplyActionToPn { pnIdentifier: string; action: string; From 3dcb5483d483592e6b11146f29415291c26615fc Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Fri, 14 Jun 2024 17:27:15 +0200 Subject: [PATCH 18/20] update naming --- .../extensions/payment-network/meta.test.ts | 64 ++++++++----------- .../payment-network/meta-pn-data-generator.ts | 41 ++---------- .../test/meta-payment-network.test.ts | 2 +- .../src/payment/encoder-approval.ts | 6 +- .../src/payment/encoder-payment.ts | 4 +- .../payment-processor/src/payment/utils.ts | 2 +- 6 files changed, 41 insertions(+), 78 deletions(-) diff --git a/packages/advanced-logic/test/extensions/payment-network/meta.test.ts b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts index b0d243e96..1988d2d08 100644 --- a/packages/advanced-logic/test/extensions/payment-network/meta.test.ts +++ b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts @@ -63,7 +63,6 @@ describe('extensions/payment-network/meta', () => { }); it('cannot createCreationAction with duplicated salt', () => { - // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, baseParams], @@ -72,7 +71,6 @@ describe('extensions/payment-network/meta', () => { }); it('cannot createCreationAction with payment address not an ethereum address', () => { - // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ @@ -84,7 +82,6 @@ describe('extensions/payment-network/meta', () => { }); it('cannot createCreationAction with refund address not an ethereum address', () => { - // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ @@ -96,7 +93,6 @@ describe('extensions/payment-network/meta', () => { }); it('cannot createCreationAction with fee address not an ethereum address', () => { - // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ @@ -108,7 +104,6 @@ describe('extensions/payment-network/meta', () => { }); it('cannot createCreationAction with invalid fee amount', () => { - // 'must throw' expect(() => { metaPn.createCreationAction({ [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ @@ -127,20 +122,19 @@ describe('extensions/payment-network/meta', () => { expect( metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, - MetaCreate.actionCreationFull, + MetaCreate.actionCreationMultipleAnyToErc20, MetaCreate.requestStateNoExtensions, TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, ), - ).toEqual(MetaCreate.extensionFullState); + ).toEqual(MetaCreate.extensionFullStateMultipleAnyToErc20); }); it('cannot applyActionToExtensions of creation with a previous state', () => { - // 'must throw' expect(() => { metaPn.applyActionToExtension( MetaCreate.requestFullStateCreated.extensions, - MetaCreate.actionCreationFull, + MetaCreate.actionCreationMultipleAnyToErc20, MetaCreate.requestFullStateCreated, TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, @@ -156,11 +150,11 @@ describe('extensions/payment-network/meta', () => { type: RequestLogicTypes.CURRENCY.BTC, value: 'BTC', }; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( TestData.requestCreatedNoExtension.extensions, - MetaCreate.actionCreationFull, + MetaCreate.actionCreationMultipleAnyToErc20, requestCreatedNoExtension, TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, @@ -171,11 +165,11 @@ describe('extensions/payment-network/meta', () => { }); it('cannot applyActionToExtensions of creation with payment address not valid', () => { - const actionWithInvalidAddress = deepCopy(MetaCreate.actionCreationFull); + const actionWithInvalidAddress = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20); actionWithInvalidAddress.parameters[ ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY ][0].paymentAddress = DataConversionERC20FeeAddData.invalidAddress; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, @@ -190,11 +184,11 @@ describe('extensions/payment-network/meta', () => { }); it('cannot applyActionToExtensions of creation with no tokens accepted', () => { - const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationFull); + const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20); actionWithInvalidToken.parameters[ ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY ][0].acceptedTokens = []; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, @@ -207,11 +201,11 @@ describe('extensions/payment-network/meta', () => { }); it('cannot applyActionToExtensions of creation with token address not valid', () => { - const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationFull); + const actionWithInvalidToken = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20); actionWithInvalidToken.parameters[ ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY ][0].acceptedTokens = ['invalid address']; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, @@ -224,11 +218,11 @@ describe('extensions/payment-network/meta', () => { }); it('cannot applyActionToExtensions of creation with refund address not valid', () => { - const testnetRefundAddress = deepCopy(MetaCreate.actionCreationFull); + const testnetRefundAddress = deepCopy(MetaCreate.actionCreationMultipleAnyToErc20); testnetRefundAddress.parameters[ ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY ][0].refundAddress = DataConversionERC20FeeAddData.invalidAddress; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, @@ -244,7 +238,7 @@ describe('extensions/payment-network/meta', () => { it('keeps the version used at creation', () => { const newState = metaPn.applyActionToExtension( {}, - { ...MetaCreate.actionCreationFull, version: 'ABCD' }, + { ...MetaCreate.actionCreationMultipleAnyToErc20, version: 'ABCD' }, MetaCreate.requestStateNoExtensions, TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, @@ -256,7 +250,7 @@ describe('extensions/payment-network/meta', () => { expect(() => { metaPn.applyActionToExtension( {}, - { ...MetaCreate.actionCreationFull, version: '' }, + { ...MetaCreate.actionCreationMultipleAnyToErc20, version: '' }, MetaCreate.requestStateNoExtensions, TestData.otherIdRaw.identity, TestData.arbitraryTimestamp, @@ -266,7 +260,7 @@ describe('extensions/payment-network/meta', () => { }); describe('applyActionToExtension/applyApplyActionToExtension', () => { - it('can applyActionToExtensions of applyApplyActionToExtension addPaymentAddress', () => { + it('can applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress', () => { expect( metaPn.applyActionToExtension( MetaCreate.requestStateCreatedMissingAddress.extensions, @@ -278,8 +272,7 @@ describe('extensions/payment-network/meta', () => { ).toEqual(MetaCreate.extensionStateWithApplyAddPaymentAddressAfterCreation); }); - it('cannot applyActionToExtensions of addPaymentAddress without a previous state', () => { - // 'must throw' + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress without a previous state', () => { expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateNoExtensions.extensions, @@ -291,10 +284,10 @@ describe('extensions/payment-network/meta', () => { }).toThrowError(`No payment network with identifier ${MetaCreate.salt2}`); }); - it('cannot applyActionToExtensions of addPaymentAddress without a payee', () => { + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress without a payee', () => { const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); previousState.payee = undefined; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( previousState.extensions, @@ -306,9 +299,9 @@ describe('extensions/payment-network/meta', () => { }).toThrowError(`The request must have a payee`); }); - it('cannot applyActionToExtensions of addPaymentAddress signed by someone else than the payee', () => { + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress signed by someone else than the payee', () => { const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); - // 'must throw' + expect(() => { metaPn.applyActionToExtension( previousState.extensions, @@ -320,8 +313,7 @@ describe('extensions/payment-network/meta', () => { }).toThrowError(`The signer must be the payee`); }); - it('cannot applyActionToExtensions of addPaymentAddress with payment address already given', () => { - // 'must throw' + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address already given', () => { expect(() => { metaPn.applyActionToExtension( MetaCreate.requestFullStateCreated.extensions, @@ -333,11 +325,11 @@ describe('extensions/payment-network/meta', () => { }).toThrowError(`Payment address already given`); }); - it('cannot applyActionToExtensions of addPaymentAddress with payment address not valid', () => { + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address not valid', () => { const actionWithInvalidAddress = deepCopy(MetaCreate.actionApplyActionToPn); actionWithInvalidAddress.parameters.parameters.paymentAddress = DataConversionERC20FeeAddData.invalidAddress; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateCreatedMissingAddress.extensions, @@ -351,10 +343,10 @@ describe('extensions/payment-network/meta', () => { ); }); - it('cannot applyActionToExtensions when the pn identifier is wrong', () => { + it('cannot applyActionToExtensions applyApplyActionToExtension when the pn identifier is wrong', () => { const actionWithInvalidPnIdentifier = deepCopy(MetaCreate.actionApplyActionToPn); actionWithInvalidPnIdentifier.parameters.pnIdentifier = 'wrongId'; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateCreatedMissingAddress.extensions, @@ -366,10 +358,10 @@ describe('extensions/payment-network/meta', () => { }).toThrowError(`No payment network with identifier wrongId`); }); - it('cannot applyActionToExtensions when the action does not exists on the sub pn', () => { + it('cannot applyActionToExtensions applyApplyActionToExtension when the action does not exists on the sub pn', () => { const actionWithInvalidPnAction = deepCopy(MetaCreate.actionApplyActionToPn); actionWithInvalidPnAction.parameters.action = 'wrongAction' as ExtensionTypes.ACTION; - // 'must throw' + expect(() => { metaPn.applyActionToExtension( MetaCreate.requestStateCreatedMissingAddress.extensions, diff --git a/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts index 3b71c05cc..9c157a1bc 100644 --- a/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts +++ b/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts @@ -43,7 +43,7 @@ export const extendedParams = (salt: string) => ({ sentRefundAmount: '0', }); // actions -export const actionCreationFull = { +export const actionCreationMultipleAnyToErc20 = { action: 'create', id: ExtensionTypes.PAYMENT_NETWORK_ID.META, parameters: { @@ -51,43 +51,14 @@ export const actionCreationFull = { }, version: '0.1.0', }; -export const actionCreationOnlyPayment = { - action: 'create', - id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - parameters: { - paymentAddress, - acceptedTokens: [tokenAddress], - network, - }, - version: '0.1.0', -}; -export const actionCreationOnlyRefund = { - action: 'create', - id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - parameters: { - refundAddress, - acceptedTokens: [tokenAddress], - network, - }, - version: '0.1.0', -}; -export const actionCreationOnlyFee = { - action: 'create', - id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, - parameters: { - feeAddress, - feeAmount, - acceptedTokens: [tokenAddress], - network, - }, - version: '0.1.0', -}; + export const actionCreationEmpty = { action: 'create', id: ExtensionTypes.PAYMENT_NETWORK_ID.META, parameters: {}, version: '0.1.0', }; + export const actionApplyActionToPn = { action: ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN, id: ExtensionTypes.PAYMENT_NETWORK_ID.META, @@ -102,7 +73,7 @@ export const actionApplyActionToPn = { // --------------------------------------------------------------------- // extensions states -export const extensionFullState = { +export const extensionFullStateMultipleAnyToErc20 = { [ExtensionTypes.PAYMENT_NETWORK_ID.META as string]: { events: [ { @@ -308,8 +279,8 @@ export const requestFullStateCreated: RequestLogicTypes.IRequest = { }, ], expectedAmount: TestData.arbitraryExpectedAmount, - extensions: extensionFullState, - extensionsData: [actionCreationFull], + extensions: extensionFullStateMultipleAnyToErc20, + extensionsData: [actionCreationMultipleAnyToErc20], payee: { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: TestData.payeeRaw.address, diff --git a/packages/payment-detection/test/meta-payment-network.test.ts b/packages/payment-detection/test/meta-payment-network.test.ts index 8f5b08158..6e6f8cb0f 100644 --- a/packages/payment-detection/test/meta-payment-network.test.ts +++ b/packages/payment-detection/test/meta-payment-network.test.ts @@ -167,7 +167,7 @@ describe('api/meta-payment-network', () => { expect(spySubPn).toHaveBeenCalledTimes(2); }); - it('can createExtensionsDataForCreation without sub-salt', async () => { + it('can createExtensionsDataForCreation without sub-pn salt', async () => { const spyMeta = jest.spyOn(mockAdvancedLogic.extensions.metaPn, 'createCreationAction'); const spySubPn = jest.spyOn( mockAdvancedLogic.extensions.anyToErc20Proxy, diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index 20553a78d..bfd31de26 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -13,7 +13,7 @@ import { prepareApprovalErc20ForSwapWithConversionToPay, } from './swap-conversion-erc20'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; -import { getFormattedRequest } from './utils'; +import { flattenRequestByPnId } from './utils'; /** * For a given request and user, encode an approval transaction if it is needed. @@ -28,7 +28,7 @@ export async function encodeRequestErc20ApprovalIfNeeded( from: string, options?: IRequestPaymentOptions, ): Promise { - const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestErc20ApprovalWithSwapIfNeeded(formattedRequest, provider, from, options); } else { @@ -47,7 +47,7 @@ export function encodeRequestErc20Approval( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction | void { - const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestErc20ApprovalWithSwap(formattedRequest, provider, options); } else { diff --git a/packages/payment-processor/src/payment/encoder-payment.ts b/packages/payment-processor/src/payment/encoder-payment.ts index 554065a39..e8218fec7 100644 --- a/packages/payment-processor/src/payment/encoder-payment.ts +++ b/packages/payment-processor/src/payment/encoder-payment.ts @@ -13,7 +13,7 @@ import { prepareEthFeeProxyPaymentTransaction } from './eth-fee-proxy'; import { prepareAnyToEthProxyPaymentTransaction } from './any-to-eth-proxy'; import { IConversionPaymentSettings } from '.'; import { prepareErc777StreamPaymentTransaction } from './erc777-stream'; -import { getFormattedRequest } from './utils'; +import { flattenRequestByPnId } from './utils'; /** * Encodes a transaction to pay a Request in generic way. ERC777 stream excepted. @@ -26,7 +26,7 @@ export function encodeRequestPayment( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction { - const formattedRequest = getFormattedRequest({ request, pnIdentifier: options?.pnIdentifier }); + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); if (options && options.swap) { return encodeRequestPaymentWithSwap(formattedRequest, provider, options); } else { diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index c34453736..a8614adae 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -427,7 +427,7 @@ export async function revokeErc20Approval( * If the request does not use the meta-pn, it returns it as is. * Otherwise, returns the request formatted with the pn of interest */ -export function getFormattedRequest({ +export function flattenRequestByPnId({ request, pnIdentifier, }: { From 6bcd9dcdbe98911536332beda1313ed33440cf5b Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Mon, 1 Jul 2024 10:21:34 +0200 Subject: [PATCH 19/20] additional utility method to compute payment references for meta pn --- packages/payment-detection/src/index.ts | 4 ++ packages/payment-detection/src/utils.ts | 47 +++++++++++++++++++ .../src/payment/encoder-approval.ts | 6 ++- .../src/payment/encoder-payment.ts | 6 ++- .../payment-processor/src/payment/utils.ts | 29 ------------ 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 2d9175aad..5e8e177f1 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -16,6 +16,8 @@ import { formatAddress, getPaymentNetworkExtension, getPaymentReference, + getPaymentReferencesForMetaPnRequest, + flattenRequestByPnId, hashReference, padAmountForChainlink, parseLogArgs, @@ -61,8 +63,10 @@ export { padAmountForChainlink, unpadAmountFromChainlink, calculateEscrowState, + flattenRequestByPnId, getPaymentNetworkExtension, getPaymentReference, + getPaymentReferencesForMetaPnRequest, hashReference, formatAddress, }; diff --git a/packages/payment-detection/src/utils.ts b/packages/payment-detection/src/utils.ts index 5e7611dc1..36e37bd27 100644 --- a/packages/payment-detection/src/utils.ts +++ b/packages/payment-detection/src/utils.ts @@ -1,5 +1,6 @@ import { isValidNearAddress } from '@requestnetwork/currency'; import { + ClientTypes, CurrencyTypes, ExtensionTypes, PaymentTypes, @@ -10,6 +11,7 @@ import { getAddress, keccak256, LogDescription } from 'ethers/lib/utils'; import { ContractArtifact, DeploymentInformation } from '@requestnetwork/smart-contracts'; import { NetworkNotSupported, VersionNotSupported } from './balance-error'; import * as PaymentReferenceCalculator from './payment-reference-calculator'; +import { deepCopy } from '@requestnetwork/utils'; /** * Converts the Log's args from array to an object with keys being the name of the arguments @@ -173,6 +175,51 @@ export function getPaymentReference( return PaymentReferenceCalculator.calculate(requestId, salt, info); } +/** + * Format a request we wish to build a payment for. + * If the request does not use the meta-pn, it returns it as is. + * Otherwise, returns the request formatted with the pn of interest + */ +export function flattenRequestByPnId({ + request, + pnIdentifier, +}: { + request: ClientTypes.IRequestData; + pnIdentifier?: string; +}): ClientTypes.IRequestData { + const pn = getPaymentNetworkExtension(request); + if (!pn?.id || pn.id !== ExtensionTypes.PAYMENT_NETWORK_ID.META) return request; + if (!pnIdentifier) throw new Error('Missing pn identifier'); + + const extensionOfInterest: ExtensionTypes.IState | undefined = pn.values[pnIdentifier]; + if (!extensionOfInterest) throw new Error('Invalid pn identifier'); + + const formattedRequest = { + ...deepCopy(request), + extensions: { + [extensionOfInterest.id]: extensionOfInterest, + }, + }; + return formattedRequest; +} + +/** Gets all payment references associated to a request using meta-pn */ +export const getPaymentReferencesForMetaPnRequest = (request: ClientTypes.IRequestData) => { + if (!request?.extensions?.[ExtensionTypes.PAYMENT_NETWORK_ID.META]) + throw new Error('This request does not have a meta-pn extension'); + + const pnKeys = Object.keys(request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META].values); + const pnIdentifiers = pnKeys.filter( + (key) => + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META].values[key].type === + ExtensionTypes.TYPE.PAYMENT_NETWORK, + ); + + return pnIdentifiers.map((pnIdentifier) => + getPaymentReference(flattenRequestByPnId({ request, pnIdentifier })), + ); +}; + /** * Returns the hash of a payment reference. * @see getPaymentReference diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index bfd31de26..b577b4ab4 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -12,8 +12,10 @@ import { hasErc20ApprovalForSwapWithConversion, prepareApprovalErc20ForSwapWithConversionToPay, } from './swap-conversion-erc20'; -import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; -import { flattenRequestByPnId } from './utils'; +import { + getPaymentNetworkExtension, + flattenRequestByPnId, +} from '@requestnetwork/payment-detection'; /** * For a given request and user, encode an approval transaction if it is needed. diff --git a/packages/payment-processor/src/payment/encoder-payment.ts b/packages/payment-processor/src/payment/encoder-payment.ts index e8218fec7..d5de88499 100644 --- a/packages/payment-processor/src/payment/encoder-payment.ts +++ b/packages/payment-processor/src/payment/encoder-payment.ts @@ -2,7 +2,10 @@ import { IRequestPaymentOptions } from '../types'; import { IPreparedTransaction } from './prepared-transaction'; import { providers } from 'ethers'; import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; -import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; +import { + getPaymentNetworkExtension, + flattenRequestByPnId, +} from '@requestnetwork/payment-detection'; import { prepareErc20ProxyPaymentTransaction } from './erc20-proxy'; import { prepareErc20FeeProxyPaymentTransaction } from './erc20-fee-proxy'; import { prepareAnyToErc20ProxyPaymentTransaction } from './any-to-erc20-proxy'; @@ -13,7 +16,6 @@ import { prepareEthFeeProxyPaymentTransaction } from './eth-fee-proxy'; import { prepareAnyToEthProxyPaymentTransaction } from './any-to-eth-proxy'; import { IConversionPaymentSettings } from '.'; import { prepareErc777StreamPaymentTransaction } from './erc777-stream'; -import { flattenRequestByPnId } from './utils'; /** * Encodes a transaction to pay a Request in generic way. ERC777 stream excepted. diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index a8614adae..a340c6fc6 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -11,7 +11,6 @@ import { EvmChains, getCurrencyHash } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; import { getReceivableTokenIdForRequest } from './erc20-transferable-receivable'; -import { deepCopy } from '@requestnetwork/utils'; /** @constant MAX_ALLOWANCE set to the max uint256 value */ export const MAX_ALLOWANCE = BigNumber.from(2).pow(256).sub(1); @@ -421,31 +420,3 @@ export async function revokeErc20Approval( const tx = await signer.sendTransaction(preparedTx); return tx; } - -/** - * Format a request we wish to build a payment for. - * If the request does not use the meta-pn, it returns it as is. - * Otherwise, returns the request formatted with the pn of interest - */ -export function flattenRequestByPnId({ - request, - pnIdentifier, -}: { - request: ClientTypes.IRequestData; - pnIdentifier?: string; -}): ClientTypes.IRequestData { - const pn = getPaymentNetworkExtension(request); - if (!pn?.id || pn.id !== ExtensionTypes.PAYMENT_NETWORK_ID.META) return request; - if (!pnIdentifier) throw new Error('Missing pn identifier'); - - const extensionOfInterest: ExtensionTypes.IState | undefined = pn.values[pnIdentifier]; - if (!extensionOfInterest) throw new Error('Invalid pn identifier'); - - const formattedRequest = { - ...deepCopy(request), - extensions: { - [extensionOfInterest.id]: extensionOfInterest, - }, - }; - return formattedRequest; -} From 77a3016f2d1c7fa4a49d7ae0511076011ad2c9eb Mon Sep 17 00:00:00 2001 From: Leo Sollier Date: Mon, 1 Jul 2024 10:58:46 +0200 Subject: [PATCH 20/20] update comment --- packages/payment-detection/src/meta-payment-detector.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/payment-detection/src/meta-payment-detector.ts b/packages/payment-detection/src/meta-payment-detector.ts index 3802f1bb6..1226d26fe 100644 --- a/packages/payment-detection/src/meta-payment-detector.ts +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -40,7 +40,8 @@ const advancedLogicMap: Partial< /** * Detect payment for the meta payment network. - * Recursively detects payments on each sub payment network + * Recursively detects payments on each sub payment network. + * For each sub payment network we check for payments with the paymentReference associated to the sub payment network. */ export class MetaDetector extends DeclarativePaymentDetectorBase< ExtensionTypes.PnMeta.IMeta,