diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index b415bdc12..fd3438da8 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: CurrencyTypes.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) { @@ -158,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/advanced-logic/src/extensions/abstract-extension.ts b/packages/advanced-logic/src/extensions/abstract-extension.ts index 1b545a454..b28bf23b3 100644 --- a/packages/advanced-logic/src/extensions/abstract-extension.ts +++ b/packages/advanced-logic/src/extensions/abstract-extension.ts @@ -1,6 +1,13 @@ 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 */ @@ -60,7 +67,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 +111,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..77e3a4af5 --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/meta.ts @@ -0,0 +1,239 @@ +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'; +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: CurrencyTypes.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); + 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); + } + }); + + 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, + 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); + + (parameters as any[]).forEach((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]; + + const pn = this.getExtension(extensionToActOn.id); + + 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]) => { + // Checks that the PN is supported + this.getExtension(pnId); + + 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); + } + }); + } 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}`); + } + + // Checks that the PN is supported + this.getExtension(subPnState.id); + } + } + + 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/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..1988d2d08 --- /dev/null +++ b/packages/advanced-logic/test/extensions/payment-network/meta.test.ts @@ -0,0 +1,377 @@ +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; +const otherBaseParams = { + ...baseParams, + salt: 'ea3bc7caf64110cb', +} 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, otherBaseParams], + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [baseParams, otherBaseParams], + }, + 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 }, + otherBaseParams, + ], + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.PAYMENT_NETWORK_ID.META, + parameters: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAddress: undefined, feeAmount: undefined }, + otherBaseParams, + ], + }, + version: '0.1.0', + }); + }); + + it('cannot createCreationAction with duplicated salt', () => { + 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', () => { + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, paymentAddress: 'not an ethereum address' }, + otherBaseParams, + ], + }); + }).toThrowError("paymentAddress 'not an ethereum address' is not a valid address"); + }); + + it('cannot createCreationAction with refund address not an ethereum address', () => { + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, refundAddress: 'not an ethereum address' }, + otherBaseParams, + ], + }); + }).toThrowError("refundAddress 'not an ethereum address' is not a valid address"); + }); + + it('cannot createCreationAction with fee address not an ethereum address', () => { + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAddress: 'not an ethereum address' }, + otherBaseParams, + ], + }); + }).toThrowError('feeAddress is not a valid address'); + }); + + it('cannot createCreationAction with invalid fee amount', () => { + expect(() => { + metaPn.createCreationAction({ + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: [ + { ...baseParams, feeAmount: '-2000' }, + otherBaseParams, + ], + }); + }).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.actionCreationMultipleAnyToErc20, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(MetaCreate.extensionFullStateMultipleAnyToErc20); + }); + + it('cannot applyActionToExtensions of creation with a previous state', () => { + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestFullStateCreated.extensions, + MetaCreate.actionCreationMultipleAnyToErc20, + 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', + }; + + expect(() => { + metaPn.applyActionToExtension( + TestData.requestCreatedNoExtension.extensions, + MetaCreate.actionCreationMultipleAnyToErc20, + 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.actionCreationMultipleAnyToErc20); + actionWithInvalidAddress.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].paymentAddress = DataConversionERC20FeeAddData.invalidAddress; + + 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.actionCreationMultipleAnyToErc20); + actionWithInvalidToken.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].acceptedTokens = []; + + 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.actionCreationMultipleAnyToErc20); + actionWithInvalidToken.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].acceptedTokens = ['invalid address']; + + 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.actionCreationMultipleAnyToErc20); + testnetRefundAddress.parameters[ + ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ][0].refundAddress = DataConversionERC20FeeAddData.invalidAddress; + + 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.actionCreationMultipleAnyToErc20, 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.actionCreationMultipleAnyToErc20, version: '' }, + MetaCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('version is required at creation'); + }); + }); + + describe('applyActionToExtension/applyApplyActionToExtension', () => { + it('can applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress', () => { + expect( + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + MetaCreate.actionApplyActionToPn, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(MetaCreate.extensionStateWithApplyAddPaymentAddressAfterCreation); + }); + + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress without a previous state', () => { + 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 applyApplyActionToExtension for addPaymentAddress without a payee', () => { + const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); + previousState.payee = undefined; + + expect(() => { + metaPn.applyActionToExtension( + previousState.extensions, + MetaCreate.actionApplyActionToPn, + previousState, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The request must have a payee`); + }); + + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress signed by someone else than the payee', () => { + const previousState = deepCopy(MetaCreate.requestStateCreatedMissingAddress); + + expect(() => { + metaPn.applyActionToExtension( + previousState.extensions, + MetaCreate.actionApplyActionToPn, + previousState, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payee`); + }); + + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address already given', () => { + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestFullStateCreated.extensions, + MetaCreate.actionApplyActionToPn, + MetaCreate.requestFullStateCreated, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Payment address already given`); + }); + + it('cannot applyActionToExtensions of applyApplyActionToExtension for addPaymentAddress with payment address not valid', () => { + const actionWithInvalidAddress = deepCopy(MetaCreate.actionApplyActionToPn); + actionWithInvalidAddress.parameters.parameters.paymentAddress = + DataConversionERC20FeeAddData.invalidAddress; + + 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 applyApplyActionToExtension when the pn identifier is wrong', () => { + const actionWithInvalidPnIdentifier = deepCopy(MetaCreate.actionApplyActionToPn); + actionWithInvalidPnIdentifier.parameters.pnIdentifier = 'wrongId'; + + expect(() => { + metaPn.applyActionToExtension( + MetaCreate.requestStateCreatedMissingAddress.extensions, + actionWithInvalidPnIdentifier, + MetaCreate.requestStateCreatedMissingAddress, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`No payment network with identifier wrongId`); + }); + + 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; + + 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..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 @@ -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,10 @@ export const extensionFullState = { feeAddress, feeAmount, network, - paymentAddress, + paymentAddress: + paymentAddressOverride === null ? undefined : paymentAddressOverride ?? paymentAddress, refundAddress, - salt, + salt: saltOverride || salt, acceptedTokens: [tokenAddress], maxRateTimespan: undefined, }, @@ -94,9 +98,10 @@ export const extensionFullState = { feeAddress, feeAmount, network, - paymentAddress, + paymentAddress: + paymentAddressOverride === null ? undefined : paymentAddressOverride ?? paymentAddress, refundAddress, - salt, + salt: saltOverride || salt, acceptedTokens: [tokenAddress], maxRateTimespan: undefined, payeeDelegate: undefined, @@ -110,7 +115,7 @@ export const extensionFullState = { }, version: '0.1.0', }, -}; +}); export const extensionStateCreatedEmpty = { [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY as string]: { events: [ @@ -228,7 +233,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..9c157a1bc --- /dev/null +++ b/packages/advanced-logic/test/utils/payment-network/meta-pn-data-generator.ts @@ -0,0 +1,419 @@ +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 actionCreationMultipleAnyToErc20 = { + 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 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 extensionFullStateMultipleAnyToErc20 = { + [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: extensionFullStateMultipleAnyToErc20, + extensionsData: [actionCreationMultipleAnyToErc20], + 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/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/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/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 74e57ebeb..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, @@ -28,6 +30,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 +52,7 @@ export { NearConversionNativeTokenPaymentDetector, EscrowERC20InfoRetriever, SuperFluidInfoRetriever, + MetaDetector, setProviderFactory, initPaymentDetectionApiKeys, getDefaultProvider, @@ -59,8 +63,10 @@ export { padAmountForChainlink, unpadAmountFromChainlink, calculateEscrowState, + flattenRequestByPnId, getPaymentNetworkExtension, getPaymentReference, + getPaymentReferencesForMetaPnRequest, hashReference, formatAddress, }; 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..1226d26fe --- /dev/null +++ b/packages/payment-detection/src/meta-payment-detector.ts @@ -0,0 +1,190 @@ +import { + AdvancedLogicTypes, + CurrencyTypes, + ExtensionTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { deepCopy } 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: (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, +]; + +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: 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', +}; + +/** + * Detect payment for the meta 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, + | PaymentTypes.ConversionPaymentNetworkEventParameters + | PaymentTypes.IDeclarativePaymentEventParameters +> { + private readonly advancedLogic: AdvancedLogicTypes.IAdvancedLogic; + private readonly currencyManager: CurrencyTypes.ICurrencyManager; + private readonly options: Partial; + + 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. + * + * @param paymentNetworkCreationParameters Parameters to create the extension + * @returns The extensionData object + */ + public async createExtensionsDataForCreation( + paymentNetworkCreationParameters: ExtensionTypes.PnMeta.ICreationParameters, + ): Promise { + // Do the same for each sub-extension + for (const [key, value] of Object.entries(paymentNetworkCreationParameters)) { + 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 = + this.advancedLogic.extensions[extensionKey as keyof typeof this.advancedLogic.extensions]; + + if (!detectorClass || !extension) { + throw new Error(`The payment network id: ${key} is not supported for meta-pn detection`); + } + + 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])).parameters; + } + } + } + return this.extension.createCreationAction({ + ...paymentNetworkCreationParameters, + }); + } + + /** + * Creates the extensions data to apply an action on a sub pn + * + * @param Parameters to apply an action on a sub pn + * @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[] = []; + + for (const value of Object.values( + paymentExtension.values as Record>, + )) { + 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 = + 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 for meta-pn detection`, + ); + } + + 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 cb85d23a6..fc2d4d505 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -13,6 +13,7 @@ import { } from './types'; import { BtcMainnetAddressBasedDetector, BtcTestnetAddressBasedDetector } from './btc'; import { DeclarativePaymentDetector } from './declarative'; +import { MetaDetector } from './meta-payment-detector'; import { ERC20AddressBasedPaymentDetector, ERC20FeeProxyPaymentDetector, @@ -82,6 +83,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 */ @@ -150,6 +152,7 @@ export class PaymentNetworkFactory { network, advancedLogic: this.advancedLogic, currencyManager: this.currencyManager, + options: this.options, ...this.options, }); 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-detection/test/meta-payment-network.test.ts b/packages/payment-detection/test/meta-payment-network.test.ts new file mode 100644 index 000000000..6e6f8cb0f --- /dev/null +++ b/packages/payment-detection/test/meta-payment-network.test.ts @@ -0,0 +1,380 @@ +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().mockImplementation((parameters) => { + return { + parameters, + }; +}); +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', + }, + ], + }); + + expect(spyMeta).toHaveBeenCalledTimes(1); + expect(spySubPn).toHaveBeenCalledTimes(2); + }); + + it('can createExtensionsDataForCreation without sub-pn 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'); + }); +}); diff --git a/packages/payment-processor/src/payment/encoder-approval.ts b/packages/payment-processor/src/payment/encoder-approval.ts index 4149f0e6c..b577b4ab4 100644 --- a/packages/payment-processor/src/payment/encoder-approval.ts +++ b/packages/payment-processor/src/payment/encoder-approval.ts @@ -12,7 +12,10 @@ import { hasErc20ApprovalForSwapWithConversion, prepareApprovalErc20ForSwapWithConversionToPay, } from './swap-conversion-erc20'; -import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; +import { + getPaymentNetworkExtension, + flattenRequestByPnId, +} from '@requestnetwork/payment-detection'; /** * For a given request and user, encode an approval transaction if it is needed. @@ -27,10 +30,11 @@ export async function encodeRequestErc20ApprovalIfNeeded( from: string, options?: IRequestPaymentOptions, ): Promise { + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); 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 +49,11 @@ export function encodeRequestErc20Approval( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction | void { + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); 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..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'; @@ -25,10 +28,11 @@ export function encodeRequestPayment( provider: providers.Provider, options?: IRequestPaymentOptions, ): IPreparedTransaction { + const formattedRequest = flattenRequestByPnId({ request, pnIdentifier: options?.pnIdentifier }); 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/types.ts b/packages/payment-processor/src/types.ts index 37947419f..19b589071 100644 --- a/packages/payment-processor/src/types.ts +++ b/packages/payment-processor/src/types.ts @@ -70,6 +70,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'); + }); + }); +}); diff --git a/packages/types/src/advanced-logic-types.ts b/packages/types/src/advanced-logic-types.ts index 34bfede9b..34bb0a499 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 */ @@ -36,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 5331df4e3..71fb8fa2e 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,6 +36,7 @@ export interface IExtension { actionSigner: Identity.IIdentity, timestamp: number, ) => RequestLogic.IExtensionStates; + createCreationAction: (parameters: TCreationParameters) => IAction; } export type ApplyAction = ( @@ -97,6 +100,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 = { diff --git a/packages/types/src/extensions/pn-meta.ts b/packages/types/src/extensions/pn-meta.ts new file mode 100644 index 000000000..7860e2ad5 --- /dev/null +++ b/packages/types/src/extensions/pn-meta.ts @@ -0,0 +1,37 @@ +// import * as Extension from '../extension-types'; +import { ExtensionTypes } from '..'; +import { PnAnyDeclarative, PnAnyToErc20, PnAnyToEth } 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.ANY_DECLARATIVE]?: PnAnyDeclarative.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]?: PnAnyToErc20.ICreationParameters[]; + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]?: PnAnyToEth.ICreationParameters[]; +} + +/** + * Parameters of of apply-action-to-pn action + * Supports all actions supported by sub payment networks + */ +export interface IApplyActionToPn { + pnIdentifier: string; + action: string; + parameters: any; +} + +/** Actions possible */ +export enum ACTION { + CREATE = 'create', + APPLY_ACTION_TO_PN = 'apply-action-to-pn', +} 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; }; /**