From 45f09f9ee5693378722559d414b07e887fb3c63c Mon Sep 17 00:00:00 2001 From: Vincent <4611986+vrolland@users.noreply.github.com> Date: Thu, 25 Feb 2021 11:33:14 +0100 Subject: [PATCH] feat: payment network any to erc20 in advanced logic (#414) --- packages/advanced-logic/src/advanced-logic.ts | 96 +-- .../payment-network/any-to-erc20-proxy.ts | 444 +++++++++++ .../any-to-erc20-proxy.test.ts | 716 ++++++++++++++++++ .../any-to-erc20-proxy-add-data-generator.ts | 204 +++++ ...ny-to-erc20-proxy-create-data-generator.ts | 236 ++++++ packages/types/src/extension-types.ts | 4 +- .../src/extensions/pn-any-to-er20-types.ts | 14 + packages/types/src/payment-types.ts | 17 +- 8 files changed, 1653 insertions(+), 78 deletions(-) create mode 100644 packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts create mode 100644 packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts create mode 100644 packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator.ts create mode 100644 packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts create mode 100644 packages/types/src/extensions/pn-any-to-er20-types.ts diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index bd1a0d47aa..f251a30e7b 100644 --- a/packages/advanced-logic/src/advanced-logic.ts +++ b/packages/advanced-logic/src/advanced-logic.ts @@ -13,6 +13,7 @@ import addressBasedErc20 from './extensions/payment-network/erc20/address-based' import feeProxyContractErc20 from './extensions/payment-network/erc20/fee-proxy-contract'; import proxyContractErc20 from './extensions/payment-network/erc20/proxy-contract'; import ethereumInputData from './extensions/payment-network/ethereum/input-data'; +import anyToErc20Proxy from './extensions/payment-network/any-to-erc20-proxy'; /** * Module to manage Advanced logic extensions @@ -20,11 +21,12 @@ import ethereumInputData from './extensions/payment-network/ethereum/input-data' */ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic { /** Give access to the functions specific of the extensions supported */ - public extensions: any = { + public extensions = { addressBasedBtc, addressBasedErc20, addressBasedTestnetBtc, contentData, + anyToErc20Proxy, declarative, ethereumInputData, feeProxyContractErc20, @@ -50,80 +52,28 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic timestamp: number, ): RequestLogicTypes.IExtensionStates { const id: ExtensionTypes.ID = extensionAction.id; + const extension: ExtensionTypes.IExtension | undefined = { + [ExtensionTypes.ID.CONTENT_DATA]: contentData, + [ExtensionTypes.ID.PAYMENT_NETWORK_BITCOIN_ADDRESS_BASED]: addressBasedBtc, + [ExtensionTypes.ID.PAYMENT_NETWORK_TESTNET_BITCOIN_ADDRESS_BASED]: addressBasedTestnetBtc, + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_DECLARATIVE]: declarative, + [ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_ADDRESS_BASED]: addressBasedErc20, + [ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_PROXY_CONTRACT]: proxyContractErc20, + [ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT]: feeProxyContractErc20, + [ExtensionTypes.ID.PAYMENT_NETWORK_ETH_INPUT_DATA]: ethereumInputData, + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY]: anyToErc20Proxy, + } [id]; - if (id === ExtensionTypes.ID.CONTENT_DATA) { - return contentData.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_BITCOIN_ADDRESS_BASED) { - return addressBasedBtc.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_TESTNET_BITCOIN_ADDRESS_BASED) { - return addressBasedTestnetBtc.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_ANY_DECLARATIVE) { - return declarative.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_ADDRESS_BASED) { - return addressBasedErc20.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_PROXY_CONTRACT) { - return proxyContractErc20.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT) { - return feeProxyContractErc20.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); - } - if (id === ExtensionTypes.ID.PAYMENT_NETWORK_ETH_INPUT_DATA) { - return ethereumInputData.applyActionToExtension( - extensionsState, - extensionAction, - requestState, - actionSigner, - timestamp, - ); + if(!extension) { + throw Error(`extension not recognized, id: ${id}`); } - throw Error(`extension not recognized, id: ${id}`); + return extension.applyActionToExtension( + extensionsState, + extensionAction, + requestState, + actionSigner, + timestamp, + ); } } diff --git a/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts b/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts new file mode 100644 index 0000000000..df4d042def --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/any-to-erc20-proxy.ts @@ -0,0 +1,444 @@ +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; +import Utils from '@requestnetwork/utils'; +import ReferenceBased from './reference-based'; + +const CURRENT_VERSION = '0.1.0'; + +const walletAddressValidator = require('wallet-address-validator'); + +/** + * Implementation of the payment network to pay in ERC20, including third-party fees payment, based on a reference provided to a proxy contract. + * With this extension, one request can have three Ethereum addresses (one for payment, one for fees payment, and one for refund) + * Every ERC20 ethereum transaction that reaches these addresses through the proxy contract and has the correct reference will be interpreted as a payment or a refund. + * The value to give as input data is the last 8 bytes of a salted hash of the requestId and the address: `last8Bytes(hash(requestId + salt + address))`: + * The salt should have at least 8 bytes of randomness. A way to generate it is: + * `Math.floor(Math.random() * Math.pow(2, 4 * 8)).toString(16) + Math.floor(Math.random() * Math.pow(2, 4 * 8)).toString(16)` + */ +const conversionErc20FeeProxyContract: ExtensionTypes.PnAnyToErc20.IAnyToERC20 = { + applyActionToExtension, + createAddFeeAction, + createAddPaymentAddressAction, + createAddRefundAddressAction, + createCreationAction, + isValidAddress, +}; + +/** + * These currencies are supported by Chainlink for conversion. + * Only ERC20 is supported as accepted token by the payment proxy. + */ +const supportedCurrencies: Record> = { + private: { + [RequestLogicTypes.CURRENCY.ISO4217]: ['USD', 'EUR'], + [RequestLogicTypes.CURRENCY.ERC20]: ['0x9FBDa871d559710256a2502A2517b794B482Db40'], + [RequestLogicTypes.CURRENCY.ETH]: ['ETH'], + [RequestLogicTypes.CURRENCY.BTC]: [], + }, + rinkeby: { + [RequestLogicTypes.CURRENCY.ISO4217]: ['USD', 'EUR'], + [RequestLogicTypes.CURRENCY.ERC20]: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + [RequestLogicTypes.CURRENCY.ETH]: ['ETH'], + [RequestLogicTypes.CURRENCY.BTC]: [], + }, + mainnet: { + [RequestLogicTypes.CURRENCY.ISO4217]: [], + [RequestLogicTypes.CURRENCY.ERC20]: [], + [RequestLogicTypes.CURRENCY.ETH]: [], + [RequestLogicTypes.CURRENCY.BTC]: [], + }, +}; + +/** + * Creates the extensionsData to create the extension ERC20 fee proxy contract payment detection + * + * @param creationParameters extensions parameters to create + * + * @returns IExtensionCreationAction the extensionsData to be stored in the request + */ +function createCreationAction( + creationParameters: ExtensionTypes.PnAnyToErc20.ICreationParameters, +): ExtensionTypes.IAction { + if (creationParameters.paymentAddress && !isValidAddress(creationParameters.paymentAddress)) { + throw Error('paymentAddress is not a valid ethereum address'); + } + + if (creationParameters.refundAddress && !isValidAddress(creationParameters.refundAddress)) { + throw Error('refundAddress is not a valid ethereum address'); + } + + if (creationParameters.feeAddress && !isValidAddress(creationParameters.feeAddress)) { + throw Error('feeAddress is not a valid ethereum address'); + } + + if (creationParameters.feeAmount && !Utils.amount.isValid(creationParameters.feeAmount)) { + throw Error('feeAmount is not a valid amount'); + } + + if (creationParameters.feeAmount && !creationParameters.feeAddress) { + throw Error('feeAmount requires feeAddress'); + } + if (creationParameters.feeAddress && !creationParameters.feeAmount) { + throw Error('feeAddress requires feeAmount'); + } + if (!creationParameters.acceptedTokens || creationParameters.acceptedTokens.length === 0) { + throw Error('acceptedTokens is required'); + } + if (creationParameters.acceptedTokens.some((address) => !isValidAddress(address))) { + throw Error('acceptedTokens must contains only valid ethereum addresses'); + } + + const network = creationParameters.network || 'mainnet'; + if (!supportedCurrencies[network]) { + throw Error('network not supported'); + } + const supportedErc20: string[] = supportedCurrencies[network][RequestLogicTypes.CURRENCY.ERC20]; + if (creationParameters.acceptedTokens.some((address) => !supportedErc20.includes(address))) { + throw Error('acceptedTokens must contain only supported token addresses (ERC20 only)'); + } + + return ReferenceBased.createCreationAction( + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + creationParameters, + CURRENT_VERSION, + ); +} + +/** + * Creates the extensionsData to add a payment address + * + * @param addPaymentAddressParameters extensions parameters to create + * + * @returns IAction the extensionsData to be stored in the request + */ +function createAddPaymentAddressAction( + addPaymentAddressParameters: ExtensionTypes.PnReferenceBased.IAddPaymentAddressParameters, +): ExtensionTypes.IAction { + if ( + addPaymentAddressParameters.paymentAddress && + !isValidAddress(addPaymentAddressParameters.paymentAddress) + ) { + throw Error('paymentAddress is not a valid ethereum address'); + } + + return ReferenceBased.createAddPaymentAddressAction( + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + addPaymentAddressParameters, + ); +} + +/** + * Creates the extensionsData to add a refund address + * + * @param addRefundAddressParameters extensions parameters to create + * + * @returns IAction the extensionsData to be stored in the request + */ +function createAddRefundAddressAction( + addRefundAddressParameters: ExtensionTypes.PnReferenceBased.IAddRefundAddressParameters, +): ExtensionTypes.IAction { + if ( + addRefundAddressParameters.refundAddress && + !isValidAddress(addRefundAddressParameters.refundAddress) + ) { + throw Error('refundAddress is not a valid ethereum address'); + } + + return ReferenceBased.createAddRefundAddressAction( + ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + addRefundAddressParameters, + ); +} + +/** + * Creates the extensionsData to add a fee address + * + * @param addFeeParameters extensions parameters to create + * + * @returns IAction the extensionsData to be stored in the request + */ +function createAddFeeAction( + addFeeParameters: ExtensionTypes.PnFeeReferenceBased.IAddFeeParameters, +): ExtensionTypes.IAction { + if (addFeeParameters.feeAddress && !isValidAddress(addFeeParameters.feeAddress)) { + throw Error('feeAddress is not a valid ethereum address'); + } + + if (addFeeParameters.feeAmount && !Utils.amount.isValid(addFeeParameters.feeAmount)) { + throw Error('feeAmount is not a valid amount'); + } + + if (!addFeeParameters.feeAmount && addFeeParameters.feeAddress) { + throw Error('feeAmount requires feeAddress'); + } + if (addFeeParameters.feeAmount && !addFeeParameters.feeAddress) { + throw Error('feeAddress requires feeAmount'); + } + + return { + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: addFeeParameters, + }; +} +/** + * Applies the extension action to the request state + * Is called to interpret the extensions data when applying the transaction + * + * @param extensionsState previous state of the extensions + * @param extensionAction action to apply + * @param requestState request state read-only + * @param actionSigner identity of the signer + * + * @returns state of the request updated + */ +function applyActionToExtension( + extensionsState: RequestLogicTypes.IExtensionStates, + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + actionSigner: IdentityTypes.IIdentity, + timestamp: number, +): RequestLogicTypes.IExtensionStates { + checkSupportedCurrency(requestState.currency, extensionAction.parameters.network || 'rinkeby'); + + const copiedExtensionState: RequestLogicTypes.IExtensionStates = Utils.deepCopy(extensionsState); + + if (extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE) { + if (requestState.extensions[extensionAction.id]) { + throw Error(`This extension has already been created`); + } + + copiedExtensionState[extensionAction.id] = applyCreation(extensionAction, timestamp); + + return copiedExtensionState; + } + + // if the action is not "create", the state must have been created before + if (!requestState.extensions[extensionAction.id]) { + throw Error(`The extension should be created before receiving any other action`); + } + + if (extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_PAYMENT_ADDRESS) { + copiedExtensionState[extensionAction.id] = ReferenceBased.applyAddPaymentAddress( + isValidAddress, + copiedExtensionState[extensionAction.id], + extensionAction, + requestState, + actionSigner, + timestamp, + ); + + return copiedExtensionState; + } + + if (extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_REFUND_ADDRESS) { + copiedExtensionState[extensionAction.id] = ReferenceBased.applyAddRefundAddress( + isValidAddress, + copiedExtensionState[extensionAction.id], + extensionAction, + requestState, + actionSigner, + timestamp, + ); + + return copiedExtensionState; + } + + if (extensionAction.action === ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE) { + copiedExtensionState[extensionAction.id] = applyAddFee( + copiedExtensionState[extensionAction.id], + extensionAction, + requestState, + actionSigner, + timestamp, + ); + + return copiedExtensionState; + } + + throw Error(`Unknown action: ${extensionAction.action}`); +} + +/** + * Applies a creation extension action + * + * @param extensionAction action to apply + * @param timestamp action timestamp + * + * @returns state of the extension created + */ +function applyCreation( + extensionAction: ExtensionTypes.IAction, + timestamp: number, +): ExtensionTypes.IState { + if (!extensionAction.version) { + throw Error('version is missing'); + } + if (!extensionAction.parameters.paymentAddress) { + throw Error('salt is missing'); + } + if ( + extensionAction.parameters.paymentAddress && + !isValidAddress(extensionAction.parameters.paymentAddress) + ) { + throw Error('paymentAddress is not a valid address'); + } + if ( + extensionAction.parameters.refundAddress && + !isValidAddress(extensionAction.parameters.refundAddress) + ) { + throw Error('refundAddress is not a valid address'); + } + if ( + extensionAction.parameters.feeAddress && + !isValidAddress(extensionAction.parameters.feeAddress) + ) { + throw Error('feeAddress is not a valid address'); + } + if ( + extensionAction.parameters.feeAmount && + !Utils.amount.isValid(extensionAction.parameters.feeAmount) + ) { + throw Error('feeAmount is not a valid amount'); + } + if ( + !extensionAction.parameters.acceptedTokens || + extensionAction.parameters.acceptedTokens.length === 0 + ) { + throw Error('acceptedTokens is required'); + } + if ( + extensionAction.parameters.acceptedTokens.some((address: string) => !isValidAddress(address)) + ) { + throw Error('acceptedTokens must contains only valid ethereum addresses'); + } + + return { + events: [ + { + name: 'create', + parameters: { + feeAddress: extensionAction.parameters.feeAddress, + feeAmount: extensionAction.parameters.feeAmount, + paymentAddress: extensionAction.parameters.paymentAddress, + refundAddress: extensionAction.parameters.refundAddress, + salt: extensionAction.parameters.salt, + network: extensionAction.parameters.network, + acceptedTokens: extensionAction.parameters.acceptedTokens, + maxRateTimespan: extensionAction.parameters.maxRateTimespan, + }, + timestamp, + }, + ], + id: extensionAction.id, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress: extensionAction.parameters.feeAddress, + feeAmount: extensionAction.parameters.feeAmount, + paymentAddress: extensionAction.parameters.paymentAddress, + refundAddress: extensionAction.parameters.refundAddress, + salt: extensionAction.parameters.salt, + network: extensionAction.parameters.network, + acceptedTokens: extensionAction.parameters.acceptedTokens, + maxRateTimespan: extensionAction.parameters.maxRateTimespan, + }, + version: extensionAction.version, + }; +} + +/** + * Applies an add fee address and amount extension action + * + * @param extensionState previous state of the extension + * @param extensionAction action to apply + * @param requestState request state read-only + * @param actionSigner identity of the signer + * @param timestamp action timestamp + * + * @returns state of the extension updated + */ +function applyAddFee( + extensionState: ExtensionTypes.IState, + extensionAction: ExtensionTypes.IAction, + requestState: RequestLogicTypes.IRequest, + actionSigner: IdentityTypes.IIdentity, + timestamp: number, +): ExtensionTypes.IState { + if ( + extensionAction.parameters.feeAddress && + !isValidAddress(extensionAction.parameters.feeAddress) + ) { + throw Error('feeAddress is not a valid address'); + } + if (extensionState.values.feeAddress) { + throw Error(`Fee address already given`); + } + if ( + extensionAction.parameters.feeAmount && + !Utils.amount.isValid(extensionAction.parameters.feeAmount) + ) { + throw Error('feeAmount is not a valid amount'); + } + if (extensionState.values.feeAmount) { + throw Error(`Fee amount already given`); + } + if (!requestState.payee) { + throw Error(`The request must have a payee`); + } + if (!Utils.identity.areEqual(actionSigner, requestState.payee)) { + throw Error(`The signer must be the payee`); + } + + const copiedExtensionState: ExtensionTypes.IState = Utils.deepCopy(extensionState); + + // update fee address and amount + copiedExtensionState.values.feeAddress = extensionAction.parameters.feeAddress; + copiedExtensionState.values.feeAmount = extensionAction.parameters.feeAmount; + + // update events + copiedExtensionState.events.push({ + name: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + parameters: { + feeAddress: extensionAction.parameters.feeAddress, + feeAmount: extensionAction.parameters.feeAmount, + }, + timestamp, + }); + + return copiedExtensionState; +} + +/** + * Check if an ethereum address is valid + * + * @param {string} address address to check + * @returns {boolean} true if address is valid + */ +function isValidAddress(address: string): boolean { + return walletAddressValidator.validate(address, 'ethereum'); +} + +/** + * Throw if a currency is not supported + * + * @param currency currency to check + * @param network network of the payment + */ +function checkSupportedCurrency(currency: RequestLogicTypes.ICurrency, network: string): void { + if (!supportedCurrencies[network]) { + throw new Error(`The network (${network}) is not supported for this payment network.`); + } + + if (!supportedCurrencies[network][currency.type]) { + throw new Error( + `The currency type (${currency.type}) of the request is not supported for this payment network.`, + ); + } + + if (!supportedCurrencies[network][currency.type].includes(currency.value)) { + throw new Error( + `The currency (${currency.value}) of the request is not supported for this payment network.`, + ); + } +} + +export default conversionErc20FeeProxyContract; 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 new file mode 100644 index 0000000000..33e6928e04 --- /dev/null +++ b/packages/advanced-logic/test/extensions/payment-network/any-to-erc20-proxy.test.ts @@ -0,0 +1,716 @@ +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import Utils from '@requestnetwork/utils'; + +import anyToErc20Proxy from '../../../src/extensions/payment-network/any-to-erc20-proxy' +import * as DataConversionERC20FeeAddData from '../../utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator'; +import * as DataConversionERC20FeeCreate from '../../utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator'; +import * as TestData from '../../utils/test-data-generator'; + +/* tslint:disable:no-unused-expression */ +describe('extensions/payment-network/erc20/any-to-erc20-fee-proxy-contract', () => { + describe('createCreationAction', () => { + it('can create a create action with all parameters', () => { + // 'extension data is wrong' + expect( + anyToErc20Proxy.createCreationAction({ + feeAddress: '0x0000000000000000000000000000000000000001', + feeAmount: '0', + paymentAddress: '0x0000000000000000000000000000000000000002', + refundAddress: '0x0000000000000000000000000000000000000003', + salt: 'ea3bc7caf64110ca', + network: 'rinkeby', + acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + maxRateTimespan: 1000000, + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + feeAddress: '0x0000000000000000000000000000000000000001', + feeAmount: '0', + paymentAddress: '0x0000000000000000000000000000000000000002', + refundAddress: '0x0000000000000000000000000000000000000003', + salt: 'ea3bc7caf64110ca', + network: 'rinkeby', + acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + maxRateTimespan: 1000000, + }, + version: '0.1.0', + }); + }); + + it('can create a create action without fee parameters', () => { + // 'extension data is wrong' + expect( + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + refundAddress: '0x0000000000000000000000000000000000000002', + salt: 'ea3bc7caf64110ca', + network: 'rinkeby', + acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + }), + ).toEqual({ + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + paymentAddress: '0x0000000000000000000000000000000000000001', + refundAddress: '0x0000000000000000000000000000000000000002', + salt: 'ea3bc7caf64110ca', + network: 'rinkeby', + acceptedTokens: ['0xFab46E002BbF0b4509813474841E0716E6730136'], + }, + version: '0.1.0', + }); + }); + + it('cannot createCreationAction with payment address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: 'not an ethereum address', + refundAddress: '0x0000000000000000000000000000000000000002', + salt: 'ea3bc7caf64110ca', + }); + }).toThrowError('paymentAddress is not a valid ethereum address'); + }); + + it('cannot createCreationAction with refund address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + refundAddress: 'not an ethereum address', + salt: 'ea3bc7caf64110ca', + }); + }).toThrowError('refundAddress is not a valid ethereum address'); + }); + + it('cannot createCreationAction with fee address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + feeAddress: 'not an ethereum address', + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + }); + }).toThrowError('feeAddress is not a valid ethereum address'); + }); + + it('cannot createCreationAction with invalid fee amount', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + feeAmount: '-20000', + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + }); + }).toThrowError('feeAmount is not a valid amount'); + }); + + it('cannot createCreationAction without acceptedTokens', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + }); + }).toThrowError('acceptedTokens is required'); + }); + + it('cannot createCreationAction with invalid tokens accepted', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['0x0000000000000000000000000000000000000003', 'invalid address'] + }); + }).toThrowError('acceptedTokens must contains only valid ethereum addresses'); + }); + + it('cannot createCreationAction with network not supported', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + network: 'kovan', + acceptedTokens: ['0x0000000000000000000000000000000000000003'] + }); + }).toThrowError('network not supported'); + }); + + it('cannot createCreationAction with tokens accepted not supported', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createCreationAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + salt: 'ea3bc7caf64110ca', + acceptedTokens: ['0x0000000000000000000000000000000000000003'] + }); + }).toThrowError('acceptedTokens must contain only supported token addresses (ERC20 only)'); + }); + + it('cannot applyActionToExtensions of creation on a non supported currency', () => { + const requestCreatedNoExtension: RequestLogicTypes.IRequest = Utils.deepCopy( + TestData.requestCreatedNoExtension, + ); + requestCreatedNoExtension.currency = { + type: RequestLogicTypes.CURRENCY.ETH, + value: 'ETH' + }; + + const action: ExtensionTypes.IAction = Utils.deepCopy( + DataConversionERC20FeeCreate.actionCreationFull, + ); + action.parameters.network = 'invalid network'; + + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + TestData.requestCreatedNoExtension.extensions, + action, + requestCreatedNoExtension, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + `The network (invalid network) is not supported for this payment network.`, + ); + }); + + it('cannot applyActionToExtensions of creation on a non supported currency', () => { + const requestCreatedNoExtension: RequestLogicTypes.IRequest = Utils.deepCopy( + TestData.requestCreatedNoExtension, + ); + requestCreatedNoExtension.currency = { + type: RequestLogicTypes.CURRENCY.ETH, + value: 'invalid value' + }; + + const action: ExtensionTypes.IAction = Utils.deepCopy( + DataConversionERC20FeeCreate.actionCreationFull, + ); + + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + TestData.requestCreatedNoExtension.extensions, + action, + requestCreatedNoExtension, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + `The currency (invalid value) of the request is not supported for this payment network.`, + ); + }); + + }); + + describe('createAddPaymentAddressAction', () => { + it('can createAddPaymentAddressAction', () => { + // 'extension data is wrong' + expect( + anyToErc20Proxy.createAddPaymentAddressAction({ + paymentAddress: '0x0000000000000000000000000000000000000001', + }), + ).toEqual({ + action: ExtensionTypes.PnReferenceBased.ACTION.ADD_PAYMENT_ADDRESS, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + paymentAddress: '0x0000000000000000000000000000000000000001', + }, + }); + }); + + it('cannot createAddPaymentAddressAction with payment address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createAddPaymentAddressAction({ + paymentAddress: 'not an ethereum address', + }); + }).toThrowError('paymentAddress is not a valid ethereum address'); + }); + }); + + describe('createAddRefundAddressAction', () => { + it('can createAddRefundAddressAction', () => { + // 'extension data is wrong' + expect( + anyToErc20Proxy.createAddRefundAddressAction({ + refundAddress: '0x0000000000000000000000000000000000000002', + }), + ).toEqual({ + action: ExtensionTypes.PnReferenceBased.ACTION.ADD_REFUND_ADDRESS, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + refundAddress: '0x0000000000000000000000000000000000000002', + }, + }); + }); + + it('cannot createAddRefundAddressAction with payment address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createAddRefundAddressAction({ + refundAddress: 'not an ethereum address', + }); + }).toThrowError('refundAddress is not a valid ethereum address'); + }); + }); + + describe('createAddFeeAction', () => { + it('can createAddFeeAction', () => { + // 'extension data is wrong' + expect( + anyToErc20Proxy.createAddFeeAction({ + feeAddress: '0x0000000000000000000000000000000000000002', + feeAmount: '2000', + }), + ).toEqual({ + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + feeAddress: '0x0000000000000000000000000000000000000002', + feeAmount: '2000', + }, + }); + }); + + it('cannot createAddFeeAddressAction with payment address not an ethereum address', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createAddFeeAction({ + feeAddress: 'not an ethereum address', + feeAmount: '2000', + }); + }).toThrowError('feeAddress is not a valid ethereum address'); + }); + + it('cannot createAddFeeAction with amount non positive integer', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.createAddFeeAction({ + feeAddress: '0x0000000000000000000000000000000000000002', + feeAmount: '-30000', + }); + }).toThrowError('feeAmount is not a valid amount'); + }); + }); + + describe('applyActionToExtension', () => { + describe('applyActionToExtension/unknown action', () => { + it('cannot applyActionToExtensions of unknown action', () => { + const unknownAction = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddPaymentAddress); + unknownAction.action = 'unknown action'; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + unknownAction, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('Unknown action: unknown action'); + }); + + it('cannot applyActionToExtensions of unknown id', () => { + const unknownAction = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddPaymentAddress); + unknownAction.id = 'unknown id'; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + unknownAction, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('The extension should be created before receiving any other action'); + }); + }); + + describe('applyActionToExtension/create', () => { + it('can applyActionToExtensions of creation', () => { + // 'new extension state wrong' + expect( + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + DataConversionERC20FeeCreate.actionCreationFull, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(DataConversionERC20FeeCreate.extensionFullState); + }); + + it('cannot applyActionToExtensions of creation with a previous state', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestFullStateCreated.extensions, + DataConversionERC20FeeCreate.actionCreationFull, + DataConversionERC20FeeCreate.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 = Utils.deepCopy( + TestData.requestCreatedNoExtension, + ); + requestCreatedNoExtension.currency = { + type: RequestLogicTypes.CURRENCY.BTC, + value: 'BTC', + }; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + TestData.requestCreatedNoExtension.extensions, + DataConversionERC20FeeCreate.actionCreationFull, + requestCreatedNoExtension, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError( + 'The currency (BTC) of the request is not supported for this payment network.', + ); + }); + + it('cannot applyActionToExtensions of creation with payment address not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeCreate.actionCreationFull); + testnetPaymentAddress.parameters.paymentAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('paymentAddress is not a valid address'); + }); + + it('cannot applyActionToExtensions of creation with no tokens accepted', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeCreate.actionCreationFull); + testnetPaymentAddress.parameters.acceptedTokens = []; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('acceptedTokens is required'); + }); + + it('cannot applyActionToExtensions of creation with token address not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeCreate.actionCreationFull); + testnetPaymentAddress.parameters.acceptedTokens = ['invalid address']; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.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 = Utils.deepCopy(DataConversionERC20FeeCreate.actionCreationFull); + testnetRefundAddress.parameters.refundAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + testnetRefundAddress, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.otherIdRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('refundAddress is not a valid address'); + }); + }); + + describe('applyActionToExtension/addPaymentAddress', () => { + it('can applyActionToExtensions of addPaymentAddress', () => { + // 'new extension state wrong' + expect( + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + DataConversionERC20FeeAddData.actionAddPaymentAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(DataConversionERC20FeeAddData.extensionStateWithPaymentAfterCreation); + }); + + it('cannot applyActionToExtensions of addPaymentAddress without a previous state', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + DataConversionERC20FeeAddData.actionAddPaymentAddress, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The extension should be created before receiving any other action`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress without a payee', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + previousState.payee = undefined; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddPaymentAddress, + previousState, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The request must have a payee`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress signed by someone else than the payee', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddPaymentAddress, + previousState, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payee`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress with payment address already given', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestFullStateCreated.extensions, + DataConversionERC20FeeAddData.actionAddPaymentAddress, + DataConversionERC20FeeCreate.requestFullStateCreated, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Payment address already given`); + }); + + it('cannot applyActionToExtensions of addPaymentAddress with payment address not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddPaymentAddress); + testnetPaymentAddress.parameters.paymentAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('paymentAddress is not a valid address'); + }); + }); + + describe('applyActionToExtension/addRefundAddress', () => { + it('can applyActionToExtensions of addRefundAddress', () => { + // 'new extension state wrong' + expect( + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + DataConversionERC20FeeAddData.actionAddRefundAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(DataConversionERC20FeeAddData.extensionStateWithRefundAfterCreation); + }); + + it('cannot applyActionToExtensions of addRefundAddress without a previous state', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + DataConversionERC20FeeAddData.actionAddRefundAddress, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The extension should be created before receiving any other action`); + }); + + it('cannot applyActionToExtensions of addRefundAddress without a payer', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + previousState.payer = undefined; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddRefundAddress, + previousState, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The request must have a payer`); + }); + + it('cannot applyActionToExtensions of addRefundAddress signed by someone else than the payer', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddRefundAddress, + previousState, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payer`); + }); + + it('cannot applyActionToExtensions of addRefundAddress with payment address already given', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestFullStateCreated.extensions, + DataConversionERC20FeeAddData.actionAddRefundAddress, + DataConversionERC20FeeCreate.requestFullStateCreated, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Refund address already given`); + }); + + it('cannot applyActionToExtensions of addRefundAddress with refund address not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddRefundAddress); + testnetPaymentAddress.parameters.refundAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('refundAddress is not a valid address'); + }); + }); + }); + + describe('applyActionToExtension/addFee', () => { + it('can applyActionToExtensions of addFee', () => { + // 'new extension state wrong' + expect( + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + DataConversionERC20FeeAddData.actionAddFee, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ), + ).toEqual(DataConversionERC20FeeAddData.extensionStateWithFeeAfterCreation); + }); + + it('cannot applyActionToExtensions of addFee without a previous state', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateNoExtensions.extensions, + DataConversionERC20FeeAddData.actionAddFee, + DataConversionERC20FeeCreate.requestStateNoExtensions, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The extension should be created before receiving any other action`); + }); + + it('cannot applyActionToExtensions of addFee without a payee', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + previousState.payee = undefined; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddFee, + previousState, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The request must have a payee`); + }); + + it('cannot applyActionToExtensions of addFee signed by someone else than the payee', () => { + const previousState = Utils.deepCopy(DataConversionERC20FeeCreate.requestStateCreatedEmpty); + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + previousState.extensions, + DataConversionERC20FeeAddData.actionAddFee, + previousState, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`The signer must be the payee`); + }); + + it('cannot applyActionToExtensions of addFee with fee data already given', () => { + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestFullStateCreated.extensions, + DataConversionERC20FeeAddData.actionAddFee, + DataConversionERC20FeeCreate.requestFullStateCreated, + TestData.payeeRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError(`Fee address already given`); + }); + + it('cannot applyActionToExtensions of addFee with fee address not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddFee); + testnetPaymentAddress.parameters.feeAddress = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('feeAddress is not a valid address'); + }); + + it('cannot applyActionToExtensions of addFee with fee amount not valid', () => { + const testnetPaymentAddress = Utils.deepCopy(DataConversionERC20FeeAddData.actionAddFee); + testnetPaymentAddress.parameters.feeAmount = DataConversionERC20FeeAddData.invalidAddress; + // 'must throw' + expect(() => { + anyToErc20Proxy.applyActionToExtension( + DataConversionERC20FeeCreate.requestStateCreatedEmpty.extensions, + testnetPaymentAddress, + DataConversionERC20FeeCreate.requestStateCreatedEmpty, + TestData.payerRaw.identity, + TestData.arbitraryTimestamp, + ); + }).toThrowError('feeAmount is not a valid amount'); + }); + }); + +}); diff --git a/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator.ts b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator.ts new file mode 100644 index 0000000000..d3cede8cf5 --- /dev/null +++ b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-add-data-generator.ts @@ -0,0 +1,204 @@ +import * as TestDataCreate from './fee-proxy-contract-create-data-generator'; + +import * as TestData from '../../test-data-generator'; + +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; + +export const arbitraryTimestamp = 1544426030; + +// --------------------------------------------------------------------- +// Mock addresses for testing generic address based 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 salt = 'ea3bc7caf64110ca'; +// actions +export const actionAddPaymentAddress = { + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_PAYMENT_ADDRESS, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + paymentAddress, + }, +}; +export const actionAddRefundAddress = { + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_REFUND_ADDRESS, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + refundAddress, + }, +}; +export const actionAddFee = { + action: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + feeAddress, + feeAmount, + }, +}; + +// --------------------------------------------------------------------- +// extensions states +export const extensionStateWithPaymentAfterCreation = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY as string]: { + events: [ + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE, + parameters: {}, + timestamp: arbitraryTimestamp, + }, + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_PAYMENT_ADDRESS, + parameters: { + paymentAddress, + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress, + }, + version: '0.1.0', + }, +}; + +export const extensionStateWithRefundAfterCreation = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY as string]: { + events: [ + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE, + parameters: {}, + timestamp: arbitraryTimestamp, + }, + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_REFUND_ADDRESS, + parameters: { + refundAddress, + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + refundAddress, + }, + version: '0.1.0', + }, +}; + +export const extensionStateWithFeeAfterCreation = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY as string]: { + events: [ + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.CREATE, + parameters: {}, + timestamp: arbitraryTimestamp, + }, + { + name: ExtensionTypes.PnFeeReferenceBased.ACTION.ADD_FEE, + parameters: { + feeAddress, + feeAmount, + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount, + }, + version: '0.1.0', + }, +}; + +// --------------------------------------------------------------------- +// request states +export const requestStateCreatedEmptyThenAddPayment: 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: 2, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateWithPaymentAfterCreation, + extensionsData: [TestDataCreate.actionCreationEmpty, actionAddPaymentAddress], + 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 requestStateCreatedEmptyThenAddFee: 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: 2, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateWithFeeAfterCreation, + extensionsData: [TestDataCreate.actionCreationEmpty, actionAddFee], + 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/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 new file mode 100644 index 0000000000..ba8061367d --- /dev/null +++ b/packages/advanced-logic/test/utils/payment-network/erc20/any-to-erc20-proxy-create-data-generator.ts @@ -0,0 +1,236 @@ +import * as TestData from '../../test-data-generator'; + +import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types'; + +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 salt = 'ea3bc7caf64110ca'; +// actions +export const actionCreationFull = { + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + feeAddress, + feeAmount, + paymentAddress, + refundAddress, + salt, + acceptedTokens: [tokenAddress], + }, + version: '0.1.0', +}; +export const actionCreationOnlyPayment = { + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + paymentAddress, + acceptedTokens: [tokenAddress], + }, + version: '0.1.0', +}; +export const actionCreationOnlyRefund = { + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + refundAddress, + acceptedTokens: [tokenAddress], + }, + version: '0.1.0', +}; +export const actionCreationOnlyFee = { + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: { + feeAddress, + feeAmount, + acceptedTokens: [tokenAddress], + }, + version: '0.1.0', +}; +export const actionCreationEmpty = { + action: 'create', + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + parameters: {}, + version: '0.1.0', +}; + +// --------------------------------------------------------------------- +// extensions states +export const extensionFullState = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY as string]: { + events: [ + { + name: 'create', + parameters: { + feeAddress, + feeAmount, + paymentAddress, + refundAddress, + salt, + acceptedTokens: [tokenAddress], + }, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount, + paymentAddress, + refundAddress, + salt, + acceptedTokens: [tokenAddress], + }, + version: '0.1.0', + }, +}; +export const extensionStateCreatedEmpty = { + [ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY as string]: { + events: [ + { + name: 'create', + parameters: {}, + timestamp: arbitraryTimestamp, + }, + ], + id: ExtensionTypes.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: {}, + version: '0.1.0', + }, +}; + +// --------------------------------------------------------------------- +// request states +export const requestStateNoExtensions: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 0, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: {}, + extensionsData: [], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestFullStateCreated: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionFullState, + extensionsData: [actionCreationFull], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; + +export const requestStateCreatedEmpty: RequestLogicTypes.IRequest = { + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + currency: { + type: RequestLogicTypes.CURRENCY.ISO4217, + value: 'EUR', + }, + events: [ + { + actionSigner: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + name: RequestLogicTypes.ACTION_NAME.CREATE, + parameters: { + expectedAmount: '123400000000000000', + extensionsDataLength: 1, + isSignedRequest: false, + }, + timestamp: arbitraryTimestamp, + }, + ], + expectedAmount: TestData.arbitraryExpectedAmount, + extensions: extensionStateCreatedEmpty, + extensionsData: [actionCreationEmpty], + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payeeRaw.address, + }, + payer: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: TestData.payerRaw.address, + }, + requestId: TestData.requestIdMock, + state: RequestLogicTypes.STATE.CREATED, + timestamp: TestData.arbitraryTimestamp, + version: '0.1.0', +}; diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index 3122cd5484..b3c1cb7f7e 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -3,10 +3,11 @@ import * as PnAddressBased from './extensions/pn-any-address-based-types'; import * as PnAnyDeclarative from './extensions/pn-any-declarative-types'; import * as PnFeeReferenceBased from './extensions/pn-any-fee-reference-based-types'; import * as PnReferenceBased from './extensions/pn-any-reference-based-types'; +import * as PnAnyToErc20 from './extensions/pn-any-to-er20-types'; import * as Identity from './identity-types'; import * as RequestLogic from './request-logic-types'; -export { ContentData, PnAnyDeclarative, PnAddressBased, PnFeeReferenceBased, PnReferenceBased }; +export { ContentData, PnAnyDeclarative, PnAddressBased, PnFeeReferenceBased, PnReferenceBased, PnAnyToErc20 }; /** Extension interface is extended by the extensions implementation */ export interface IExtension { @@ -53,6 +54,7 @@ export enum ID { PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT = 'pn-erc20-fee-proxy-contract', PAYMENT_NETWORK_ETH_INPUT_DATA = 'pn-eth-input-data', PAYMENT_NETWORK_ANY_DECLARATIVE = 'pn-any-declarative', + PAYMENT_NETWORK_ANY_TO_ERC20_PROXY = 'pn-any-to-erc20-proxy', } /** Type of extensions */ diff --git a/packages/types/src/extensions/pn-any-to-er20-types.ts b/packages/types/src/extensions/pn-any-to-er20-types.ts new file mode 100644 index 0000000000..dd819fb2ac --- /dev/null +++ b/packages/types/src/extensions/pn-any-to-er20-types.ts @@ -0,0 +1,14 @@ +import * as Extension from '../extension-types'; +import * as PnAnyFees from './pn-any-fee-reference-based-types'; + +/** Any to ERC20e reference-based payment network extension interface */ +export interface IAnyToERC20 extends PnAnyFees.IFeeReferenceBased { + createCreationAction: (creationParameters: ICreationParameters) => Extension.IAction; +} + +/** Parameters for the creation action */ +export interface ICreationParameters extends PnAnyFees.ICreationParameters { + network?: string; + acceptedTokens?: string[]; + maxRateTimespan?: number; +} diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 66eb5008c9..cc1cc401fb 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -29,12 +29,19 @@ export interface IReferenceBasedCreationParameters { salt?: string; } -/** Parameters to create a request with fees in reference based payment network */ +/** Parameters to create a request with "fees in reference based" payment network */ export interface IFeeReferenceBasedCreationParameters extends IReferenceBasedCreationParameters { feeAddress?: string; feeAmount?: string; } +/** Parameters to create a request with "any to erc20" payment network */ +export interface IAnyToErc20CreationParameters extends IFeeReferenceBasedCreationParameters { + network?: string; + acceptedTokens?: string[]; + maxRateTimespan?: number; +} + /** Interface of the class to manage a payment network */ export interface IPaymentNetwork { createExtensionsDataForCreation: (paymentNetworkCreationParameters: any) => Promise; @@ -96,6 +103,7 @@ export enum PAYMENT_NETWORK_ID { ERC20_FEE_PROXY_CONTRACT = Extension.ID.PAYMENT_NETWORK_ERC20_FEE_PROXY_CONTRACT, ETH_INPUT_DATA = Extension.ID.PAYMENT_NETWORK_ETH_INPUT_DATA, DECLARATIVE = Extension.ID.PAYMENT_NETWORK_ANY_DECLARATIVE, + ANY_TO_ERC20_PROXY = Extension.ID.PAYMENT_NETWORK_ANY_TO_ERC20_PROXY, } /** Generic info retriever interface */ @@ -117,6 +125,9 @@ export interface IERC20PaymentEventParameters { export interface IERC20FeePaymentEventParameters extends IERC20PaymentEventParameters { feeAddress?: string; feeAmount?: string; + feeAmountInCrypto?: string; + amountInCrypto?: string; + tokenAddress?: string; } /** ERC20 Payment Network Event */ @@ -152,8 +163,6 @@ export interface IDeclarativePaymentEventParameters { note?: string; } /** Declarative Payment Network Event */ -export type DeclarativePaymentNetworkEvent = IPaymentNetworkEvent< - IDeclarativePaymentEventParameters ->; +export type DeclarativePaymentNetworkEvent = IPaymentNetworkEvent; /** Declarative BalanceWithEvents */ export type DeclarativeBalanceWithEvents = IBalanceWithEvents;