From df5f6c44a064371c96adf6d3e6bc7b5df1a6bf3f Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Mon, 13 Feb 2023 13:17:55 -0500 Subject: [PATCH 01/14] feat: ERC20TransferableReceivable --- packages/advanced-logic/src/advanced-logic.ts | 5 + .../erc20/transferable-receivable.ts | 19 + .../integration-test/test/scheduled/mocks.ts | 8 + packages/payment-detection/src/erc20/index.ts | 1 + .../src/erc20/proxy-info-retriever.ts | 10 +- .../src/erc20/transferable-receivable.ts | 110 +++ .../src/payment-network-factory.ts | 2 + .../src/thegraph/__mocks__/client.ts | 1 + .../src/thegraph/info-retriever.ts | 19 + .../queries/GetPaymentsAndEscrowState.graphql | 10 + .../test/erc20/fee-proxy-contract.test.ts | 23 + .../test/erc20/proxy-info-retriever.test.ts | 4 +- .../erc20/thegraph-info-retriever.test.ts | 4 + packages/payment-processor/src/index.ts | 1 + .../payment/erc20-transferable-receivable.ts | 224 ++++++ .../payment-processor/src/payment/erc20.ts | 16 + .../payment-processor/src/payment/index.ts | 1 + .../payment-processor/src/payment/utils.ts | 33 +- .../scripts-create2/compute-one-address.ts | 3 +- .../scripts-create2/constructor-args.ts | 10 + .../smart-contracts/scripts-create2/deploy.ts | 3 +- .../smart-contracts/scripts-create2/utils.ts | 3 + .../smart-contracts/scripts-create2/verify.ts | 3 +- .../scripts/test-deploy-all.ts | 2 + ...st-deploy-erc20-transferable-receivable.ts | 30 + .../contracts/ERC20TransferableReceivable.sol | 168 +++++ .../ERC20TransferableReceivable/0.1.0.json | 650 ++++++++++++++++++ .../ERC20TransferableReceivable/index.ts | 21 + .../src/lib/artifacts/index.ts | 4 + .../ERC20TransferableReceivable.test.ts | 283 ++++++++ packages/types/src/advanced-logic-types.ts | 1 + packages/types/src/extension-types.ts | 1 + packages/types/src/payment-types.ts | 3 +- 33 files changed, 1665 insertions(+), 11 deletions(-) create mode 100644 packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts create mode 100644 packages/payment-detection/src/erc20/transferable-receivable.ts create mode 100644 packages/payment-processor/src/payment/erc20-transferable-receivable.ts create mode 100644 packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts create mode 100644 packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts create mode 100644 packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts diff --git a/packages/advanced-logic/src/advanced-logic.ts b/packages/advanced-logic/src/advanced-logic.ts index f23a7a6a21..d917aa7467 100644 --- a/packages/advanced-logic/src/advanced-logic.ts +++ b/packages/advanced-logic/src/advanced-logic.ts @@ -24,6 +24,7 @@ import AnyToNear from './extensions/payment-network/near/any-to-near'; import AnyToNearTestnet from './extensions/payment-network/near/any-to-near-testnet'; 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'; /** * Module to manage Advanced logic extensions @@ -46,6 +47,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic feeProxyContractEth: FeeProxyContractEth; anyToEthProxy: AnyToEthProxy; anyToNativeToken: AnyToNative[]; + erc20TransferableReceivable: Erc20TransferableReceivablePaymentNetwork; }; constructor(currencyManager?: ICurrencyManager) { @@ -67,6 +69,7 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic anyToEthProxy: new AnyToEthProxy(currencyManager), nativeToken: [new NearNative(), new NearTestnetNative()], anyToNativeToken: [new AnyToNear(currencyManager), new AnyToNearTestnet(currencyManager)], + erc20TransferableReceivable: new Erc20TransferableReceivablePaymentNetwork(), }; } @@ -124,6 +127,8 @@ export default class AdvancedLogic implements AdvancedLogicTypes.IAdvancedLogic [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: this.extensions.anyToEthProxy, [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN]: this.getAnyToNativeTokenExtensionForActionAndState(extensionAction, requestState), + [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: + this.extensions.erc20TransferableReceivable, }[id]; if (!extension) { diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts new file mode 100644 index 0000000000..f9bd131c6f --- /dev/null +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts @@ -0,0 +1,19 @@ +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import ReferenceBasedPaymentNetwork from '../reference-based'; + +const CURRENT_VERSION = '0.1.0'; + +/** + * Implementation of the payment network to pay in ERC20 based on a transferrable receivable contract. + */ +export default class Erc20TransferableReceivablePaymentNetwork< + TCreationParameters extends ExtensionTypes.PnReferenceBased.ICreationParameters = ExtensionTypes.PnReferenceBased.ICreationParameters, +> extends ReferenceBasedPaymentNetwork { + public constructor( + extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID + .ERC20_TRANSFERABLE_RECEIVABLE, + currentVersion: string = CURRENT_VERSION, + ) { + super(extensionId, currentVersion, RequestLogicTypes.CURRENCY.ERC20); + } +} diff --git a/packages/integration-test/test/scheduled/mocks.ts b/packages/integration-test/test/scheduled/mocks.ts index 086624efdd..95436ecf5f 100644 --- a/packages/integration-test/test/scheduled/mocks.ts +++ b/packages/integration-test/test/scheduled/mocks.ts @@ -71,5 +71,13 @@ export const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { {} as Extension.PnFeeReferenceBased.IFeeReferenceBased, anyToNativeToken: {} as Extension.PnFeeReferenceBased.IFeeReferenceBased[], + erc20TransferableReceivable: { + createAddPaymentAddressAction, + createAddRefundAddressAction, + createCreationAction, + // inheritance from declarative + createAddPaymentInstructionAction, + createAddRefundInstructionAction, + } as any as Extension.PnReferenceBased.IReferenceBased, }, }; diff --git a/packages/payment-detection/src/erc20/index.ts b/packages/payment-detection/src/erc20/index.ts index 564a49c361..113a1c0a25 100644 --- a/packages/payment-detection/src/erc20/index.ts +++ b/packages/payment-detection/src/erc20/index.ts @@ -1,3 +1,4 @@ export { ERC20AddressBasedPaymentDetector } from './address-based'; export { ERC20FeeProxyPaymentDetector } from './fee-proxy-contract'; export { ERC20ProxyPaymentDetector } from './proxy-contract'; +export { ERC20TransferableReceivablePaymentDetector } from './transferable-receivable'; diff --git a/packages/payment-detection/src/erc20/proxy-info-retriever.ts b/packages/payment-detection/src/erc20/proxy-info-retriever.ts index 2ce08aa646..8dcbf7466c 100644 --- a/packages/payment-detection/src/erc20/proxy-info-retriever.ts +++ b/packages/payment-detection/src/erc20/proxy-info-retriever.ts @@ -64,8 +64,12 @@ export default class ProxyERC20InfoRetriever /** * Retrieves transfer events for the current contract, address and network. + * @param isTransferable Whether or not the request is expected to be paid + * through a receivable proxy contract */ - public async getTransferEvents(): Promise { + public async getTransferEvents( + isTransferable = false, + ): Promise { // Create a filter to find all the Transfer logs for the toAddress const filter = this.contractProxy.filters.TransferWithReference( null, @@ -112,7 +116,7 @@ export default class ProxyERC20InfoRetriever .filter( ({ parsedLog }) => parsedLog.tokenAddress.toLowerCase() === this.tokenContractAddress.toLowerCase() && - parsedLog.to.toLowerCase() === this.toAddress.toLowerCase(), + (isTransferable || parsedLog.to.toLowerCase() === this.toAddress.toLowerCase()), ) // Creates the balance events .map(async ({ parsedLog, blockNumber, transactionHash }) => ({ @@ -122,7 +126,7 @@ export default class ProxyERC20InfoRetriever block: blockNumber, feeAddress: parsedLog.feeAddress || undefined, feeAmount: parsedLog.feeAmount?.toString() || undefined, - to: this.toAddress, + to: parsedLog.to, txHash: transactionHash, }, timestamp: (await this.provider.getBlock(blockNumber || 0)).timestamp, diff --git a/packages/payment-detection/src/erc20/transferable-receivable.ts b/packages/payment-detection/src/erc20/transferable-receivable.ts new file mode 100644 index 0000000000..b84aaac64c --- /dev/null +++ b/packages/payment-detection/src/erc20/transferable-receivable.ts @@ -0,0 +1,110 @@ +import { ExtensionTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; + +import { TheGraphInfoRetriever } from '../thegraph'; +import { erc20TransferableReceivableArtifact } from '@requestnetwork/smart-contracts'; +import { makeGetDeploymentInformation } from '../utils'; +import { PaymentNetworkOptions, ReferenceBasedDetectorOptions } from '../types'; +import { ReferenceBasedDetector } from '../reference-based-detector'; +import ProxyInfoRetriever from './proxy-info-retriever'; + +const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = { + ['0.1.0']: '0.1.0', +}; + +/** + * Handle payment networks with ERC20 transferrable receivable contract extension + */ +export class ERC20TransferableReceivablePaymentDetector extends ReferenceBasedDetector< + ExtensionTypes.PnReferenceBased.IReferenceBased, + PaymentTypes.IERC20PaymentEventParameters +> { + private readonly getSubgraphClient: PaymentNetworkOptions['getSubgraphClient']; + + /** + * @param extension The advanced logic payment network extensions + */ + public constructor({ + advancedLogic, + currencyManager, + getSubgraphClient, + }: ReferenceBasedDetectorOptions & Pick) { + super( + ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, + advancedLogic.extensions.erc20TransferableReceivable, + currencyManager, + ); + this.getSubgraphClient = getSubgraphClient; + } + + /** + * Extracts the balance and events of an address + * + * @private + * @param address Address to check + * @param eventName Indicate if it is an address for payment or refund + * @param network The id of network we want to check + * @param tokenContractAddress the address of the token contract + * @returns The balance and events + */ + protected async extractEvents( + eventName: PaymentTypes.EVENTS_NAMES, + toAddress: string | undefined, + paymentReference: string, + requestCurrency: RequestLogicTypes.ICurrency, + paymentChain: string, + paymentNetwork: ExtensionTypes.IState, + ): Promise> { + // To satisfy typescript + toAddress; + if (!paymentReference) { + return { + paymentEvents: [], + }; + } + + const { + address: receivableContractAddress, + creationBlockNumber: receivableCreationBlockNumber, + } = ERC20TransferableReceivablePaymentDetector.getDeploymentInformation( + paymentChain, + paymentNetwork.version, + ); + + const subgraphClient = this.getSubgraphClient(paymentChain); + if (subgraphClient) { + const graphInfoRetriever = new TheGraphInfoRetriever(subgraphClient, this.currencyManager); + return graphInfoRetriever.getReceivableEvents({ + paymentReference, + toAddress: '', // Filtering by payee address does not apply for transferrable receivables + contractAddress: receivableContractAddress, + paymentChain, + eventName, + acceptedTokens: [requestCurrency.value], + }); + } else { + const transferrableReceivableInfoRetriever = new ProxyInfoRetriever( + paymentReference, + receivableContractAddress, + receivableCreationBlockNumber, + requestCurrency.value, + '', + eventName, + paymentChain, + ); + const paymentEvents = await transferrableReceivableInfoRetriever.getTransferEvents( + true /* isReceivable */, + ); + return { + paymentEvents, + }; + } + } + + /* + * Returns deployment information for the underlying smart contract for a given payment network version + */ + public static getDeploymentInformation = makeGetDeploymentInformation( + erc20TransferableReceivableArtifact, + ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP, + ); +} diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index 8204115fb6..b32795f0ad 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -17,6 +17,7 @@ import { ERC20AddressBasedPaymentDetector, ERC20FeeProxyPaymentDetector, ERC20ProxyPaymentDetector, + ERC20TransferableReceivablePaymentDetector, } from './erc20'; import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EthFeeProxyPaymentDetector, EthInputDataPaymentDetector } from './eth'; @@ -48,6 +49,7 @@ const supportedPaymentNetwork: ISupportedPaymentNetworkByCurrency = { [PN_ID.ERC20_ADDRESS_BASED]: ERC20AddressBasedPaymentDetector, [PN_ID.ERC20_PROXY_CONTRACT]: ERC20ProxyPaymentDetector, [PN_ID.ERC20_FEE_PROXY_CONTRACT]: ERC20FeeProxyPaymentDetector, + [PN_ID.ERC20_TRANSFERABLE_RECEIVABLE]: ERC20TransferableReceivablePaymentDetector, }, }, ETH: { diff --git a/packages/payment-detection/src/thegraph/__mocks__/client.ts b/packages/payment-detection/src/thegraph/__mocks__/client.ts index cc5054c200..08f6c45484 100644 --- a/packages/payment-detection/src/thegraph/__mocks__/client.ts +++ b/packages/payment-detection/src/thegraph/__mocks__/client.ts @@ -3,6 +3,7 @@ import type { TheGraphClient } from '../client'; export const theGraphClient: TheGraphClient = { GetLastSyncedBlock: jest.fn(), GetPaymentsAndEscrowState: jest.fn(), + GetPaymentsAndEscrowStateForReceivables: jest.fn(), GetSyncedBlock: jest.fn(), }; export const getTheGraphClient = () => theGraphClient; diff --git a/packages/payment-detection/src/thegraph/info-retriever.ts b/packages/payment-detection/src/thegraph/info-retriever.ts index 7f7fadd888..655d002889 100644 --- a/packages/payment-detection/src/thegraph/info-retriever.ts +++ b/packages/payment-detection/src/thegraph/info-retriever.ts @@ -48,6 +48,25 @@ export class TheGraphInfoRetriever { }; } + public async getReceivableEvents( + params: TransferEventsParams, + ): Promise> { + const { payments, escrowEvents } = await this.client.GetPaymentsAndEscrowStateForReceivables({ + reference: utils.keccak256(`0x${params.paymentReference}`), + }); + + params.contractAddress = formatAddress(params.contractAddress, 'contractAddress'); + params.acceptedTokens = + params.acceptedTokens?.map((tok) => formatAddress(tok, 'acceptedTokens')) || []; + + return { + paymentEvents: payments + .filter((payment) => this.filterPaymentEvents(payment, params)) + .map((payment) => this.mapPaymentEvents(payment, params)), + escrowEvents: escrowEvents.map((escrow) => this.mapEscrowEvents(escrow, params)), + }; + } + private filterPaymentEvents(payment: PaymentEventResultFragment, params: TransferEventsParams) { // Check contract address matches expected if (formatAddress(payment.contractAddress) !== formatAddress(params.contractAddress)) { diff --git a/packages/payment-detection/src/thegraph/queries/GetPaymentsAndEscrowState.graphql b/packages/payment-detection/src/thegraph/queries/GetPaymentsAndEscrowState.graphql index 463c7711c5..90f6b3f286 100644 --- a/packages/payment-detection/src/thegraph/queries/GetPaymentsAndEscrowState.graphql +++ b/packages/payment-detection/src/thegraph/queries/GetPaymentsAndEscrowState.graphql @@ -35,3 +35,13 @@ query GetPaymentsAndEscrowState($reference: Bytes!, $to: Bytes!) { ...EscrowEventResult } } + +# Receivables can be transferred to different owners, so searching by to could drop balance events. +query GetPaymentsAndEscrowStateForReceivables($reference: Bytes!) { + payments(where: { reference: $reference }, orderBy: timestamp, orderDirection: asc) { + ...PaymentEventResult + } + escrowEvents(where: { reference: $reference }, orderBy: timestamp, orderDirection: asc) { + ...EscrowEventResult + } +} diff --git a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts index b22b7fecc2..120746c7e8 100644 --- a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts +++ b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts @@ -433,6 +433,29 @@ describe('api/erc20/fee-proxy-contract', () => { ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), escrowEvents: [], })), + GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({ + payments: [ + { + contractAddress: '0x370de27fdb7d1ff1e1baa7d11c5820a324cf623c', + tokenAddress: '0x967da4048cd07ab37855c090aaf366e4ce1b9f48', + to: '0x6c9e04997000d6a8a353951231923d776d4cdff2', + from: '0x15339d48fbe31e349a507fd6d48eb01c45fdc79a', + amount: '168040800000000000000000', + feeAmount: '13386000000000000000', + reference: '0x5ac7241d9e6f419409e439c8429eea2f8f089d76528fd1d5df7496a3e58b5ce1', + block: 15767215, + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + gasUsed: '73152', + gasPrice: '12709127644', + timestamp: 1666002347, + amountInCrypto: null, + feeAddress: '0x35d0e078755cd84d3e0656caab417dee1d7939c7', + feeAmountInCrypto: null, + maxRateTimespan: null, + }, + ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), + escrowEvents: [], + })), GetLastSyncedBlock: jest.fn(), GetSyncedBlock: jest.fn(), }), diff --git a/packages/payment-detection/test/erc20/proxy-info-retriever.test.ts b/packages/payment-detection/test/erc20/proxy-info-retriever.test.ts index f4e25839ff..22e4f37ff2 100644 --- a/packages/payment-detection/test/erc20/proxy-info-retriever.test.ts +++ b/packages/payment-detection/test/erc20/proxy-info-retriever.test.ts @@ -12,7 +12,7 @@ const paymentReferenceMock = '01111111111111111111111111111111111111111111111111 /* eslint-disable @typescript-eslint/no-unused-expressions */ describe('api/erc20/proxy-info-retriever', () => { describe('on localhost', () => { - const paymentAddress = '0xf17f52151ebef6c7334fad080c5704d77216b732'; + const paymentAddress = '0xf17f52151EbEF6C7334FAD080c5704D77216b732'; it('can get the localhost balance of an address', async () => { const infoRetriever = new ProxyERC20InfoRetriever( @@ -128,7 +128,7 @@ describe('api/erc20/proxy-info-retriever', () => { const parameters: PaymentTypes.IERC20FeePaymentEventParameters = event.parameters!; - expect(parameters.to).toBe('0x627306090abab3a6e1400e9345bc60c78a8bef57'); + expect(parameters.to).toBe('0x627306090abaB3A6e1400e9345bC60c78a8BEf57'); expect(typeof parameters.block).toBe('number'); expect(typeof parameters.txHash).toBe('string'); expect(parameters.feeAddress).toBe('0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'); diff --git a/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts b/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts index 2d72749750..37f0dab049 100644 --- a/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts +++ b/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts @@ -49,6 +49,10 @@ describe('api/erc20/thegraph-info-retriever', () => { payments: paymentsMockData[reference] || [], escrowEvents: [], })), + GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({ + payments: paymentsMockData[reference] || [], + escrowEvents: [], + })), GetLastSyncedBlock: jest.fn(), GetSyncedBlock: jest.fn(), }; diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index 1f64b26ea6..b4f430b1ef 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -15,6 +15,7 @@ export * from './payment/batch-conversion-proxy'; export * from './payment/swap-conversion-erc20'; export * from './payment/swap-any-to-erc20'; export * from './payment/swap-erc20'; +export * from './payment/erc20-transferable-receivable'; export * from './payment/swap-erc20-fee-proxy'; export * from './payment/conversion-erc20'; export * from './payment/any-to-erc20-proxy'; diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts new file mode 100644 index 0000000000..d5c0848c06 --- /dev/null +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -0,0 +1,224 @@ +import { + ContractTransaction, + Signer, + BigNumberish, + providers, + BigNumber, + constants, + ethers, +} from 'ethers'; + +import { Erc20PaymentNetwork } from '@requestnetwork/payment-detection'; +import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; +import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; + +import { ITransactionOverrides } from './transaction-overrides'; +import { + getAmountToPay, + getProxyAddress, + getProvider, + getSigner, + validateRequest, + getRequestPaymentValues, + validateMintERC20TransferableReceivable, +} from './utils'; +import { IPreparedTransaction } from './prepared-transaction'; + +// The ERC20 receivable smart contract ABI fragment +const erc20TransferableReceivableContractAbiFragment = [ + 'function receivableTokenIdMapping(bytes32) public view returns (uint256)', +]; + +/** + * Gets the receivableTokenId from a ERC20TransferableReceivable contract given + * a paymentReference and paymentAddress of the request + * @param request + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export async function getReceivableTokenIdForRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer, +): Promise { + // Setup the ERC20 proxy contract interface + const contract = new ethers.Contract( + getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + erc20TransferableReceivableContractAbiFragment, + signerOrProvider, + ); + + const { paymentReference, paymentAddress } = getRequestPaymentValues(request); + + return await contract.receivableTokenIdMapping( + ethers.utils.solidityKeccak256(['address', 'bytes'], [paymentAddress, `0x${paymentReference}`]), + ); +} + +/** + * Helper method to determine whether a request has a receivable minted yet + * + * @param request + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + */ +export async function hasReceivableForRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer, +): Promise { + const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); + return !receivableTokenId.isZero(); +} + +/** + * Processes a transaction to mint an ERC20TransferableReceivable. + * @param request + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function mintErc20TransferableReceivable( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer = getProvider(), + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = prepareMintErc20TransferableReceivableTransaction(request); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Encodes the call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract. + * @param request request to pay + */ +export function prepareMintErc20TransferableReceivableTransaction( + request: ClientTypes.IRequestData, +): IPreparedTransaction { + validateMintERC20TransferableReceivable(request); + + return { + data: encodeMintErc20TransferableReceivableRequest(request), + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }; +} + +/** + * Encodes call to mint a request through an ERC20TransferableReceivable contract, can be used with a Multisig contract. + * @param request request to pay + */ +export function encodeMintErc20TransferableReceivableRequest( + request: ClientTypes.IRequestData, +): string { + validateMintERC20TransferableReceivable(request); + + const tokenAddress = request.currencyInfo.value; + const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId + + const { paymentReference } = getRequestPaymentValues(request); + const amount = getAmountToPay(request); + + const receivableContract = ERC20TransferableReceivable__factory.createInterface(); + return receivableContract.encodeFunctionData('mint', [ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `0x${paymentReference}`, + amount, + tokenAddress, + metadata, + ]); +} + +/** + * Processes a transaction to pay an ERC20 receivable Request. + * @param request + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmount optionally, the fee amount to pay. Defaults to the fee amount of the request. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function payErc20TransferableReceivableRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer = getProvider(), + amount?: BigNumberish, + feeAmount?: BigNumberish, + overrides?: ITransactionOverrides, +): Promise { + const { data, to, value } = await prepareErc20TransferableReceivablePaymentTransaction( + request, + signerOrProvider, + amount, + feeAmount, + ); + const signer = getSigner(signerOrProvider); + return signer.sendTransaction({ data, to, value, ...overrides }); +} + +/** + * Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract. + * @param request request to pay + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request. + */ +export async function prepareErc20TransferableReceivablePaymentTransaction( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): Promise { + validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE); + + return { + data: await encodePayErc20TransferableReceivableRequest( + request, + signerOrProvider, + amount, + feeAmountOverride, + ), + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }; +} + +/** + * Encodes the call to pay a request through the ERC20 receivable contract, can be used with a Multisig contract. + * @param request request to pay + * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param amount optionally, the amount to pay. Defaults to remaining amount of the request. + * @param feeAmountOverride optionally, the fee amount to pay. Defaults to the fee amount of the request. + */ +export async function encodePayErc20TransferableReceivableRequest( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): Promise { + validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE); + + const amountToPay = getAmountToPay(request, amount); + const { paymentReference, feeAddress, feeAmount } = getRequestPaymentValues(request); + const feeToPay = BigNumber.from(feeAmountOverride || feeAmount || 0); + + const receivableContract = ERC20TransferableReceivable__factory.createInterface(); + + const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); + + if (receivableTokenId.isZero()) { + throw new Error( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); + } + + return receivableContract.encodeFunctionData('payOwner', [ + receivableTokenId, // get tokenId from requestId + amountToPay, + `0x${paymentReference}`, + feeToPay, + feeAddress || constants.AddressZero, + ]); +} diff --git a/packages/payment-processor/src/payment/erc20.ts b/packages/payment-processor/src/payment/erc20.ts index 034101fce0..818e0dc143 100644 --- a/packages/payment-processor/src/payment/erc20.ts +++ b/packages/payment-processor/src/payment/erc20.ts @@ -7,6 +7,7 @@ import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; import { _getErc20FeeProxyPaymentUrl, payErc20FeeProxyRequest } from './erc20-fee-proxy'; import { ISwapSettings, swapErc20FeeProxyRequest } from './swap-erc20-fee-proxy'; import { _getErc20ProxyPaymentUrl, payErc20ProxyRequest } from './erc20-proxy'; +import { payErc20TransferableReceivableRequest } from './erc20-transferable-receivable'; import { ITransactionOverrides } from './transaction-overrides'; import { @@ -42,6 +43,15 @@ export async function payErc20Request( if (id === ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT) { return payErc20ProxyRequest(request, signerOrProvider, amount, overrides); } + if (id === ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE) { + return payErc20TransferableReceivableRequest( + request, + signerOrProvider, + amount, + feeAmount, + overrides, + ); + } if (id === ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT) { if (swapSettings) { return swapErc20FeeProxyRequest(request, signerOrProvider, swapSettings, { @@ -271,6 +281,12 @@ function getProxyAddress(request: ClientTypes.IRequestData): string { Erc20PaymentNetwork.ERC20FeeProxyPaymentDetector.getDeploymentInformation, ); } + if (id === ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE) { + return genericGetProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ); + } throw new Error(`Unsupported payment network: ${id}`); } diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 54b1850633..1c62a801d8 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -94,6 +94,7 @@ export async function payRequest( switch (paymentNetwork) { case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT: case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT: + case ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE: return payErc20Request(request, signer, amount, undefined, overrides); case ExtensionTypes.PAYMENT_NETWORK_ID.ERC777_STREAM: return payErc777StreamRequest(request, signer); diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index b61058cc7a..e588476357 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -174,6 +174,7 @@ const { ERC20_FEE_PROXY_CONTRACT, ANY_TO_ERC20_PROXY, NATIVE_TOKEN, + ERC20_TRANSFERABLE_RECEIVABLE, } = ExtensionTypes.PAYMENT_NETWORK_ID; const currenciesMap: any = { [ERC777_STREAM]: RequestLogicTypes.CURRENCY.ERC777, @@ -182,6 +183,7 @@ const currenciesMap: any = { [ETH_INPUT_DATA]: RequestLogicTypes.CURRENCY.ETH, [ETH_FEE_PROXY_CONTRACT]: RequestLogicTypes.CURRENCY.ETH, [NATIVE_TOKEN]: RequestLogicTypes.CURRENCY.ETH, + [ERC20_TRANSFERABLE_RECEIVABLE]: RequestLogicTypes.CURRENCY.ERC20, }; /** @@ -215,8 +217,12 @@ export function validateRequest( // ERC20 based payment networks are only valid if the request currency has a value const validCurrencyValue = - ![ERC20_PROXY_CONTRACT, ERC20_FEE_PROXY_CONTRACT, ERC777_STREAM].includes(paymentNetworkId) || - request.currencyInfo.value; + ![ + ERC20_PROXY_CONTRACT, + ERC20_FEE_PROXY_CONTRACT, + ERC777_STREAM, + ERC20_TRANSFERABLE_RECEIVABLE, + ].includes(paymentNetworkId) || request.currencyInfo.value; // Payment network with fees should have both or none of fee address and fee amount const validFeeParams = @@ -306,6 +312,29 @@ export function validateConversionFeeProxyRequest( } } +/** + * Validates the parameters for an ERC20 Fee Proxy payment. + * @param request to validate + */ +export function validateMintERC20TransferableReceivable(request: ClientTypes.IRequestData): void { + // Validate that there exists a payee + if (request.payee == null) { + throw new Error(`Expected a payee for this request`); + } + + // Validate that there exists an assetAddress + const expectedCurrencyType = + currenciesMap[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]; + if ( + !expectedCurrencyType || + request.currencyInfo.type !== expectedCurrencyType || + !request.currencyInfo.network || + !request.currencyInfo.value + ) { + throw new Error(`Expected a valid currency ${expectedCurrencyType} on this request`); + } +} + /** * Computes the amount to pay. * If `amount` is specified, it will return it. diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index e3f8ed19fd..7c3537f7ed 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -57,7 +57,8 @@ export const computeCreate2DeploymentAddressesFromList = async ( case 'Erc20ConversionProxy': case 'ERC20EscrowToPay': case 'BatchConversionPayments': - case 'ERC20SwapToConversion': { + case 'ERC20SwapToConversion': + case 'ERC20TransferableReceivable': { try { const constructorArgs = getConstructorArgs(contract, hre.network.name); address = await computeCreate2DeploymentAddress({ contract, constructorArgs }, hre); diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 693fe1a912..91f904850e 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -58,6 +58,16 @@ export const getConstructorArgs = (contract: string, network?: string): string[] getAdminWalletAddress(contract), ]; } + case 'ERC20TransferableReceivable': { + if (!network) { + throw new Error( + 'Receivable contract requires network parameter to get correct address of erc20FeeProxy', + ); + } + const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; + const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); + return ['Request Transferable Receivable', 'RReceivable', erc20FeeProxyAddress]; + } default: return []; } diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index 6ed99b521c..75511ee12c 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -94,7 +94,8 @@ export const deployWithCreate2FromList = async ( await setupERC20SwapToConversion(address, hre); break; } - case 'ERC20EscrowToPay': { + case 'ERC20EscrowToPay': + case 'ERC20TransferableReceivable': { const network = hre.config.xdeploy.networks[0]; const constructorArgs = getConstructorArgs(contract, network); await deployOneWithCreate2({ contract, constructorArgs }, hre); diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index 8fad5f5132..c63ff96dad 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -16,6 +16,7 @@ export const create2ContractDeploymentList = [ /* 'ERC20SwapToConversion', 'ERC20EscrowToPay', 'BatchConversionPayments', */ + 'ERC20TransferableReceivable', ]; /** @@ -50,6 +51,8 @@ export const getArtifact = (contract: string): artifacts.ContractArtifact { @@ -16,4 +17,5 @@ export default async function deploy(_args: any, hre: HardhatRuntimeEnvironment) await deployBatchPayment(_args, hre); await deploySuperFluid(hre); await deployBatchConversionPayment(_args, hre); + await deployERC20TransferableReceivable(_args, hre, mainPaymentAddresses); } diff --git a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts new file mode 100644 index 0000000000..c9e5efb54e --- /dev/null +++ b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts @@ -0,0 +1,30 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { deployOne } from './deploy-one'; + +export async function deployERC20TransferableReceivable( + args: any, + hre: HardhatRuntimeEnvironment, + mainPaymentAddresses: any, +) { + try { + // Deploy ERC20 Transferable Receivable + const { address: ERC20TransferableReceivableAddress } = await deployOne( + args, + hre, + 'ERC20TransferableReceivable', + { + constructorArguments: [ + 'Request Transferable Receivable', + 'RReceivable', + mainPaymentAddresses.ERC20FeeProxyAddress, + ], + }, + ); + + console.log( + `ERC20TransferableReceivable Contract deployed: ${ERC20TransferableReceivableAddress}`, + ); + } catch (e) { + console.error(e); + } +} diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol new file mode 100644 index 0000000000..cf30aa52f1 --- /dev/null +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/utils/Counters.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; + +contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStorage { + using Counters for Counters.Counter; + + // Counter for uniquely identifying payments + Counters.Counter private _paymentId; + + // Counter for uniquely identifying receivable tokens + Counters.Counter private _receivableTokenId; + + struct ReceivableInfo { + address tokenAddress; + uint256 amount; + uint256 balance; + } + mapping(uint256 => ReceivableInfo) public receivableInfoMapping; + + // Mapping for looking up receivable token given a paymentReference + // and minter address + mapping(bytes32 => uint256) public receivableTokenIdMapping; + + address public paymentProxy; + + // Event to declare payments to a receivableTokenId + event TransferableReceivablePayment( + address sender, + address recipient, + uint256 amount, + address paymentProxy, + uint256 receivableTokenId, + address tokenAddress, + uint256 paymentId, + bytes indexed paymentReference + ); + + // Event to declare a transfer with a reference + // This event is emitted from a delegatecall to an ERC20FeeProxy contract + event TransferWithReferenceAndFee( + address tokenAddress, + address to, + uint256 amount, + bytes indexed paymentReference, + uint256 feeAmount, + address feeAddress + ); + + constructor( + string memory name, + string memory symbol, + address _paymentProxyAddress + ) ERC721(name, symbol) { + paymentProxy = _paymentProxyAddress; + } + + function payOwner( + uint256 receivableTokenId, + uint256 amount, + bytes calldata paymentReference, + uint256 feeAmount, + address feeAddress + ) external { + require(amount != 0, 'Zero amount provided'); + address owner = ownerOf(receivableTokenId); + _paymentId.increment(); + + ReceivableInfo storage receivableInfo = receivableInfoMapping[receivableTokenId]; + address tokenAddress = receivableInfo.tokenAddress; + receivableInfo.balance += amount; + + (bool status, ) = paymentProxy.delegatecall( + abi.encodeWithSignature( + 'transferFromWithReferenceAndFee(address,address,uint256,bytes,uint256,address)', + tokenAddress, + owner, + amount, + paymentReference, + feeAmount, + feeAddress + ) + ); + require(status, 'transferFromWithReferenceAndFee failed'); + + emit TransferableReceivablePayment( + msg.sender, + owner, + amount, + paymentProxy, + receivableTokenId, + tokenAddress, + _paymentId.current(), + paymentReference + ); + } + + function mint( + bytes calldata paymentReference, + uint256 amount, + address erc20Addr, + string memory receivableURI + ) external { + require(paymentReference.length > 0, 'Zero paymentReference provided'); + require(amount > 0, 'Zero amount provided'); + require(erc20Addr != address(0), 'Zero address provided'); + bytes32 idKey = keccak256(abi.encodePacked(msg.sender, paymentReference)); + require( + receivableTokenIdMapping[idKey] == 0, + 'Receivable has already been minted for this user and request' + ); + _receivableTokenId.increment(); + uint256 currentReceivableTokenId = _receivableTokenId.current(); + receivableTokenIdMapping[idKey] = currentReceivableTokenId; + receivableInfoMapping[currentReceivableTokenId] = ReceivableInfo({ + tokenAddress: erc20Addr, + amount: amount, + balance: 0 + }); + + _mint(msg.sender, currentReceivableTokenId); + _setTokenURI(currentReceivableTokenId, receivableURI); + } + + function getTokenIds(address _owner) public view returns (uint256[] memory) { + uint256[] memory _tokensOfOwner = new uint256[](ERC721.balanceOf(_owner)); + uint256 i; + + for (i = 0; i < ERC721.balanceOf(_owner); i++) { + _tokensOfOwner[i] = ERC721Enumerable.tokenOfOwnerByIndex(_owner, i); + } + return (_tokensOfOwner); + } + + // The following functions are overrides required by Solidity. + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal override(ERC721, ERC721Enumerable) { + super._beforeTokenTransfer(from, to, tokenId); + } + + function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { + super._burn(tokenId); + } + + function tokenURI(uint256 tokenId) + public + view + override(ERC721, ERC721URIStorage) + returns (string memory) + { + return super.tokenURI(tokenId); + } + + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC721, ERC721Enumerable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json new file mode 100644 index 0000000000..9768b5edaa --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json @@ -0,0 +1,650 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "address", + "name": "_paymentProxyAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "paymentProxy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "receivableTokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paymentId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "Payment", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "TransferWithReferenceAndFee", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "getTokenIds", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "erc20Addr", + "type": "address" + }, + { + "internalType": "string", + "name": "receivableURI", + "type": "string" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "receivableTokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "payOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paymentProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "receivableInfoMapping", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "receivableTokenIdMapping", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts new file mode 100644 index 0000000000..036d71fea7 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts @@ -0,0 +1,21 @@ +import { ContractArtifact } from '../../ContractArtifact'; + +import { abi as ABI_0_1_0 } from './0.1.0.json'; +// @ts-ignore Cannot find module +import type { ERC20TransferableReceivable } from '../../../types/ERC20TransferableReceivable'; + +export const erc20TransferableReceivableArtifact = + new ContractArtifact( + { + '0.1.0': { + abi: ABI_0_1_0, + deployment: { + private: { + address: '0xF426505ac145abE033fE77C666840063757Be9cd', + creationBlockNumber: 0, + }, + }, + }, + }, + '0.1.0', + ); diff --git a/packages/smart-contracts/src/lib/artifacts/index.ts b/packages/smart-contracts/src/lib/artifacts/index.ts index 9a6987fa8d..618e1f25ef 100644 --- a/packages/smart-contracts/src/lib/artifacts/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/index.ts @@ -20,3 +20,7 @@ export * from './BatchConversionPayments'; export * from './RequestHashStorage'; export * from './RequestHashSubmitter'; export * from './RequestDeployer'; +/** + * ERC20 Transferable Receivable + */ +export * from './ERC20TransferableReceivable'; diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts new file mode 100644 index 0000000000..cf23fe162b --- /dev/null +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -0,0 +1,283 @@ +import '@nomiclabs/hardhat-ethers'; +import { BytesLike, Signer } from 'ethers'; +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { + TestERC20__factory, + TestERC20, + ERC20TransferableReceivable__factory, + ERC20TransferableReceivable, + ERC20FeeProxy, + ERC20FeeProxy__factory, +} from '../../src/types'; +import { BigNumber as BN } from 'ethers'; + +const BASE_DECIMAL = BN.from(10).pow(BN.from(18)); + +describe('contract: ERC20TransferableReceivable', () => { + let deployer: Signer; + let user1: Signer, user1Addr: string; + let user2: Signer, user2Addr: string; + let user3: Signer, user3Addr: string; + + let testToken: TestERC20, receivable: ERC20TransferableReceivable, erc20FeeProxy: ERC20FeeProxy; + + before(async function () { + [deployer, user1, user2, user3] = await ethers.getSigners(); + user1Addr = await user1.getAddress(); + user2Addr = await user2.getAddress(); + user3Addr = await user3.getAddress(); + }); + + beforeEach(async function () { + testToken = await new TestERC20__factory(deployer).deploy(BN.from(1000000).mul(BASE_DECIMAL)); + erc20FeeProxy = await new ERC20FeeProxy__factory(deployer).deploy(); + receivable = await new ERC20TransferableReceivable__factory(deployer).deploy( + 'Request Transferable Receivable', + 'RReceivable', + erc20FeeProxy.address, + ); + + await testToken.approve(receivable.address, ethers.constants.MaxUint256); + }); + + async function verifyReceivables(userAddr: string, receivableIds: any) { + const ids = await receivable.getTokenIds(userAddr); + expect(ids.toString()).to.equals(receivableIds.toString()); + } + + describe('mint', async function () { + it('revert with empty paymentReference', async function () { + await expect(receivable.mint([], 1, testToken.address, '')).to.be.revertedWith( + 'Zero paymentReference provided', + ); + }); + + it('revert with zero amount', async function () { + await expect(receivable.mint('0x01', 0, testToken.address, '')).to.be.revertedWith( + 'Zero amount provided', + ); + }); + + it('revert with empty asset address', async function () { + await expect(receivable.mint('0x01', 1, ethers.constants.AddressZero, '')).to.be.revertedWith( + 'Zero address provided', + ); + }); + + it('revert with duplicated receivableId', async function () { + await receivable.connect(user1).mint('0x01', 1, testToken.address, ''); + await expect( + receivable.connect(user1).mint('0x01', 2, testToken.address, ''), + ).to.be.revertedWith('Receivable has already been minted for this user and request'); + }); + + it('success', async function () { + const receivableId = '0x0134cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; + const metadata = ethers.utils.base64.encode(receivableId); + const paymentRef = '0x01' as BytesLike; + await receivable.connect(user1).mint(paymentRef, BASE_DECIMAL, testToken.address, metadata); + const ids = await receivable.getTokenIds(user1Addr); + const tokenId = ids[0]; + expect(await receivable.ownerOf(tokenId)).to.equals(user1Addr); + expect(await receivable.tokenURI(tokenId)).to.equals(metadata); + const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); + expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); + const info = await receivable.receivableInfoMapping(tokenId); + expect(info[0]).to.equals(testToken.address); + expect(info[1]).to.equals(BASE_DECIMAL); + expect(info[2]).to.equals(0); + }); + + it('list receivables', async function () { + await receivable.connect(user1).mint('0x01', BASE_DECIMAL, testToken.address, '1'); + await receivable.connect(user1).mint('0x02', BASE_DECIMAL, testToken.address, '2'); + await receivable.connect(user1).mint('0x03', BASE_DECIMAL, testToken.address, '3'); + await verifyReceivables(user1Addr, [1, 2, 3]); + await receivable.connect(user2).mint('0x04', BASE_DECIMAL, testToken.address, '4'); + await receivable.connect(user2).mint('0x05', BASE_DECIMAL, testToken.address, '5'); + await verifyReceivables(user2Addr, [4, 5]); + await receivable.connect(user1).transferFrom(user1Addr, user2Addr, 1); + await verifyReceivables(user1Addr, [3, 2]); + await verifyReceivables(user2Addr, [4, 5, 1]); + await receivable + .connect(user2) + ['safeTransferFrom(address,address,uint256)'](user2Addr, user1Addr, 5); + await verifyReceivables(user1Addr, [3, 2, 5]); + await verifyReceivables(user2Addr, [4, 1]); + await receivable.connect(user1).approve(user3Addr, 3); + await receivable.connect(user1).approve(user3Addr, 2); + await receivable.connect(user1).approve(user3Addr, 5); + await receivable.connect(user2).approve(user3Addr, 4); + await receivable.connect(user3).transferFrom(user1Addr, user3Addr, 5); + await receivable.connect(user3).transferFrom(user1Addr, user3Addr, 2); + await receivable.connect(user3).transferFrom(user1Addr, user3Addr, 3); + await receivable.connect(user3).transferFrom(user2Addr, user3Addr, 4); + await verifyReceivables(user1Addr, []); + await verifyReceivables(user2Addr, [1]); + await verifyReceivables(user3Addr, [5, 2, 3, 4]); + }); + }); + + describe('payOwner', async function () { + let tokenId: BN; + let paymentRef: BytesLike; + let amount: BN; + let feeAmount: BN; + + beforeEach(async () => { + paymentRef = '0x01' as BytesLike; + amount = BN.from(100).mul(BASE_DECIMAL); + await receivable.connect(user1).mint(paymentRef, amount, testToken.address, '1'); + const ids = await receivable.getTokenIds(await user1.getAddress()); + tokenId = ids[0]; + feeAmount = BN.from(10).mul(BASE_DECIMAL); + }); + + it('revert with zero amount', async function () { + await expect( + receivable.payOwner(tokenId, 0, paymentRef, 0, ethers.constants.AddressZero), + ).to.be.revertedWith('Zero amount provided'); + }); + + it('reverts when proxy payment ERC20FeeProxy contract fails', async function () { + await testToken.transfer(await user2.getAddress(), amount.add(feeAmount)); + await testToken.connect(user2).approve(receivable.address, amount.add(feeAmount)); + // user2 does not have enough tokens to pay this request + await expect( + receivable + .connect(user2) + .payOwner(tokenId, amount.mul(2), paymentRef, feeAmount, user3Addr), + ).to.be.revertedWith('transferFromWithReferenceAndFee failed'); + }); + + it('success for original owner', async function () { + const beforeBal = await testToken.balanceOf(user1Addr); + await expect( + receivable.payOwner(tokenId, amount, paymentRef, 0, ethers.constants.AddressZero), + ) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user1Addr, + amount, + paymentRef, + 0, + ethers.constants.AddressZero, + ); + const afterBal = await testToken.balanceOf(user1Addr); + expect(amount).to.equals(afterBal.sub(beforeBal)); + }); + + it('allow multiple mints per receivable', async function () { + await receivable.connect(user2).mint(paymentRef, amount, testToken.address, '1'); + const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); + expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); + }); + + it('payment greater than amount', async function () { + const beforeBal = await testToken.balanceOf(user1Addr); + await expect( + receivable.payOwner(tokenId, amount.mul(2), paymentRef, 0, ethers.constants.AddressZero), + ) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user1Addr, + amount.mul(2), + paymentRef, + 0, + ethers.constants.AddressZero, + ); + const afterBal = await testToken.balanceOf(user1Addr); + expect(amount.mul(2)).to.equals(afterBal.sub(beforeBal)); + + const receivableInfo = await receivable.receivableInfoMapping(tokenId); + expect(amount.mul(2)).to.equals(receivableInfo.balance); + }); + + it('payment less than amount', async function () { + const beforeBal = await testToken.balanceOf(user1Addr); + await expect( + receivable.payOwner(tokenId, amount.div(2), paymentRef, 0, ethers.constants.AddressZero), + ) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user1Addr, + amount.div(2), + paymentRef, + 0, + ethers.constants.AddressZero, + ); + const afterBal = await testToken.balanceOf(user1Addr); + expect(amount.div(2)).to.equals(afterBal.sub(beforeBal)); + + const receivableInfo = await receivable.receivableInfoMapping(tokenId); + expect(amount.div(2)).to.equals(receivableInfo.balance); + }); + + it('payment with two different payees', async function () { + const beforeBal = await testToken.balanceOf(user1Addr); + await expect( + receivable.payOwner(tokenId, amount.div(2), paymentRef, 0, ethers.constants.AddressZero), + ) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user1Addr, + amount.div(2), + paymentRef, + 0, + ethers.constants.AddressZero, + ); + const afterBal = await testToken.balanceOf(user1Addr); + expect(amount.div(2)).to.equals(afterBal.sub(beforeBal)); + + let receivableInfo = await receivable.receivableInfoMapping(tokenId); + expect(amount.div(2)).to.equals(receivableInfo.balance); + + // Transfer receivable + await receivable.connect(user1).transferFrom(user1Addr, user2Addr, tokenId); + expect(await receivable.ownerOf(tokenId)).to.equal(user2Addr); + + const beforeBalUser2 = await testToken.balanceOf(user2Addr); + await expect( + receivable.payOwner(tokenId, amount.div(2), paymentRef, 0, ethers.constants.AddressZero), + ) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs( + testToken.address, + user2Addr, + amount.div(2), + paymentRef, + 0, + ethers.constants.AddressZero, + ); + const afterBalUser2 = await testToken.balanceOf(user2Addr); + expect(amount.div(2)).to.equals(afterBalUser2.sub(beforeBalUser2)); + + receivableInfo = await receivable.receivableInfoMapping(tokenId); + expect(amount).to.equals(receivableInfo.balance); + }); + + it('success for new owner', async function () { + const user1Addr = await user1.getAddress(); + const user2Addr = await user2.getAddress(); + const user3Addr = await user3.getAddress(); + + await receivable.connect(user1).transferFrom(user1Addr, user2Addr, tokenId); + + const feeAmount = BN.from(10).mul(BASE_DECIMAL); + const beforeBal2 = await testToken.balanceOf(user2Addr); + const beforeBal3 = await testToken.balanceOf(user3Addr); + await expect(await receivable.payOwner(tokenId, amount, paymentRef, feeAmount, user3Addr)) + .to.emit(receivable, 'TransferWithReferenceAndFee') + .withArgs(testToken.address, user2Addr, amount, paymentRef, feeAmount, user3Addr); + const afterBal2 = await testToken.balanceOf(user2Addr); + const afterBal3 = await testToken.balanceOf(user3Addr); + expect(amount).to.equals(afterBal2.sub(beforeBal2)); + expect(feeAmount).to.equals(afterBal3.sub(beforeBal3)); + }); + }); +}); diff --git a/packages/types/src/advanced-logic-types.ts b/packages/types/src/advanced-logic-types.ts index a4ec04160d..856b01c825 100644 --- a/packages/types/src/advanced-logic-types.ts +++ b/packages/types/src/advanced-logic-types.ts @@ -19,6 +19,7 @@ export interface IAdvancedLogicExtensions { feeProxyContractEth: Extension.PnFeeReferenceBased.IFeeReferenceBased; anyToEthProxy: Extension.PnFeeReferenceBased.IFeeReferenceBased; anyToNativeToken: Extension.PnFeeReferenceBased.IFeeReferenceBased[]; + erc20TransferableReceivable: Extension.PnReferenceBased.IReferenceBased; } /** Advanced Logic layer */ diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index d790693b65..52002111be 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -96,6 +96,7 @@ export enum PAYMENT_NETWORK_ID { ANY_DECLARATIVE = 'pn-any-declarative', ANY_TO_ERC20_PROXY = 'pn-any-to-erc20-proxy', ANY_TO_ETH_PROXY = 'pn-any-to-eth-proxy', + ERC20_TRANSFERABLE_RECEIVABLE = 'pn-erc20-transferrable-receivable', } export const ID = { diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index f36bf2382e..95fc2d39be 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -50,7 +50,8 @@ export type PaymentNetworkCreateParameters = id: | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT | ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA - | ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN; + | ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN + | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE; parameters: ExtensionTypes.PnReferenceBased.ICreationParameters; } | { From 89b584ddfb1893cb175d20b1817988f1522f4302 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Mon, 13 Feb 2023 19:57:10 -0500 Subject: [PATCH 02/14] Rename contract and events, add tests, address comments --- .../erc20/transferable-receivable.ts | 2 +- .../src/erc20/transferable-receivable.ts | 10 +- packages/payment-detection/src/index.ts | 2 + .../erc20/transferable-receivable.test.ts | 317 ++++++++++++++++++ .../payment/erc20-transferable-receivable.ts | 32 +- .../payment-processor/src/payment/index.ts | 2 + .../payment-processor/src/payment/utils.ts | 39 ++- .../erc20-transferable-receivable.test.ts | 187 +++++++++++ .../scripts-create2/constructor-args.ts | 2 +- ...st-deploy-erc20-transferable-receivable.ts | 4 +- .../ERC20TransferableReceivable/0.1.0.json | 92 ++--- .../ERC20TransferableReceivable.test.ts | 4 +- packages/types/src/extension-types.ts | 2 +- 13 files changed, 616 insertions(+), 79 deletions(-) create mode 100644 packages/payment-detection/test/erc20/transferable-receivable.test.ts create mode 100644 packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts index f9bd131c6f..b5f6c7c091 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts @@ -4,7 +4,7 @@ import ReferenceBasedPaymentNetwork from '../reference-based'; const CURRENT_VERSION = '0.1.0'; /** - * Implementation of the payment network to pay in ERC20 based on a transferrable receivable contract. + * Implementation of the payment network to pay in ERC20 based on a transferable receivable contract. */ export default class Erc20TransferableReceivablePaymentNetwork< TCreationParameters extends ExtensionTypes.PnReferenceBased.ICreationParameters = ExtensionTypes.PnReferenceBased.ICreationParameters, diff --git a/packages/payment-detection/src/erc20/transferable-receivable.ts b/packages/payment-detection/src/erc20/transferable-receivable.ts index b84aaac64c..c6902e5583 100644 --- a/packages/payment-detection/src/erc20/transferable-receivable.ts +++ b/packages/payment-detection/src/erc20/transferable-receivable.ts @@ -5,14 +5,14 @@ import { erc20TransferableReceivableArtifact } from '@requestnetwork/smart-contr import { makeGetDeploymentInformation } from '../utils'; import { PaymentNetworkOptions, ReferenceBasedDetectorOptions } from '../types'; import { ReferenceBasedDetector } from '../reference-based-detector'; -import ProxyInfoRetriever from './proxy-info-retriever'; +import ProxyERC20InfoRetriever from './proxy-info-retriever'; const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = { ['0.1.0']: '0.1.0', }; /** - * Handle payment networks with ERC20 transferrable receivable contract extension + * Handle payment networks with ERC20 transferable receivable contract extension */ export class ERC20TransferableReceivablePaymentDetector extends ReferenceBasedDetector< ExtensionTypes.PnReferenceBased.IReferenceBased, @@ -75,14 +75,14 @@ export class ERC20TransferableReceivablePaymentDetector extends ReferenceBasedDe const graphInfoRetriever = new TheGraphInfoRetriever(subgraphClient, this.currencyManager); return graphInfoRetriever.getReceivableEvents({ paymentReference, - toAddress: '', // Filtering by payee address does not apply for transferrable receivables + toAddress: '', // Filtering by payee address does not apply for transferable receivables contractAddress: receivableContractAddress, paymentChain, eventName, acceptedTokens: [requestCurrency.value], }); } else { - const transferrableReceivableInfoRetriever = new ProxyInfoRetriever( + const transferableReceivableInfoRetriever = new ProxyERC20InfoRetriever( paymentReference, receivableContractAddress, receivableCreationBlockNumber, @@ -91,7 +91,7 @@ export class ERC20TransferableReceivablePaymentDetector extends ReferenceBasedDe eventName, paymentChain, ); - const paymentEvents = await transferrableReceivableInfoRetriever.getTransferEvents( + const paymentEvents = await transferableReceivableInfoRetriever.getTransferEvents( true /* isReceivable */, ); return { diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 3ba743999b..0e42895a7e 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -26,6 +26,7 @@ import { SuperFluidPaymentDetector } from './erc777/superfluid-detector'; import { EscrowERC20InfoRetriever } from './erc20/escrow-info-retriever'; import { SuperFluidInfoRetriever } from './erc777/superfluid-retriever'; import { PaymentNetworkOptions } from './types'; +import { ERC20TransferableReceivablePaymentDetector } from './erc20'; export type { TheGraphClient } from './thegraph'; @@ -36,6 +37,7 @@ export { BtcPaymentNetwork, DeclarativePaymentDetector, Erc20PaymentNetwork, + ERC20TransferableReceivablePaymentDetector, EthInputDataPaymentDetector, EthFeeProxyPaymentDetector, AnyToERC20PaymentDetector, diff --git a/packages/payment-detection/test/erc20/transferable-receivable.test.ts b/packages/payment-detection/test/erc20/transferable-receivable.test.ts new file mode 100644 index 0000000000..b4f4f72a64 --- /dev/null +++ b/packages/payment-detection/test/erc20/transferable-receivable.test.ts @@ -0,0 +1,317 @@ +import { CurrencyManager } from '@requestnetwork/currency'; +import { TheGraphInfoRetriever } from '../../src/thegraph'; +import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; +import { ERC20TransferableReceivablePaymentDetector } from '../../src/erc20'; +import { AdvancedLogicTypes, PaymentTypes } from '@requestnetwork/types'; +import { mockAdvancedLogicBase } from '../utils'; +import ProxyERC20InfoRetriever from '../../src/erc20/proxy-info-retriever'; +import { ethers, utils } from 'ethers'; + +let erc20TransferableReceivable: ERC20TransferableReceivablePaymentDetector; + +const erc20LocalhostContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; +const transferableReceivableContractAddress = '0xF426505ac145abE033fE77C666840063757Be9cd'; + +const createAddPaymentAddressAction = jest.fn(); +const createAddRefundAddressAction = jest.fn(); +const createCreationAction = jest.fn(); +const createAddPaymentInstructionAction = jest.fn(); +const createAddRefundInstructionAction = jest.fn(); + +const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { + ...mockAdvancedLogicBase, + extensions: { + erc20TransferableReceivable: { + createAddPaymentAddressAction, + createAddRefundAddressAction, + createCreationAction, + // inherited from declarative + createAddPaymentInstructionAction, + createAddRefundInstructionAction, + }, + } as any as AdvancedLogicTypes.IAdvancedLogicExtensions, +}; + +const currencyManager = CurrencyManager.getDefault(); + +/* eslint-disable @typescript-eslint/no-unused-expressions */ +describe('api/erc20/transferable-receivable-contract', () => { + beforeEach(() => { + erc20TransferableReceivable = new ERC20TransferableReceivablePaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + getSubgraphClient: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('can createExtensionsDataForCreation', async () => { + await erc20TransferableReceivable.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + }); + + expect(createCreationAction).toHaveBeenCalledWith({ + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + }); + }); + + it('can createExtensionsDataForCreation with fee amount and address', async () => { + await erc20TransferableReceivable.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + salt: 'ea3bc7caf64110ca', + }); + + expect(createCreationAction).toHaveBeenCalledWith({ + paymentAddress: 'ethereum address', + refundAddress: undefined, + salt: 'ea3bc7caf64110ca', + }); + }); + + it('can createExtensionsDataForCreation without salt', async () => { + await erc20TransferableReceivable.createExtensionsDataForCreation({ + paymentAddress: 'ethereum address', + }); + + // Can't check parameters since salt is generated in createExtensionsDataForCreation + expect(createCreationAction).toHaveBeenCalled(); + }); + + it('can createExtensionsDataForAddPaymentInformation', async () => { + erc20TransferableReceivable.createExtensionsDataForAddPaymentInformation({ + paymentInfo: 'ethereum address', + }); + + expect(createAddPaymentInstructionAction).toHaveBeenCalledWith({ + paymentInfo: 'ethereum address', + }); + }); + + it('can createExtensionsDataForAddPaymentAddress', async () => { + erc20TransferableReceivable.createExtensionsDataForAddPaymentAddress({ + paymentAddress: 'ethereum address', + }); + + expect(createAddPaymentAddressAction).toHaveBeenCalledWith({ + paymentAddress: 'ethereum address', + }); + }); + + it('can createExtensionsDataForAddRefundAddress', async () => { + erc20TransferableReceivable.createExtensionsDataForAddRefundAddress({ + refundAddress: 'ethereum address', + }); + + expect(createAddRefundAddressAction).toHaveBeenCalledWith({ + refundAddress: 'ethereum address', + }); + }); + + it('can createExtensionsDataForAddRefundInformation', async () => { + erc20TransferableReceivable.createExtensionsDataForAddRefundInformation({ + refundInfo: 'ethereum address', + }); + + expect(createAddRefundInstructionAction).toHaveBeenCalledWith({ + refundInfo: 'ethereum address', + }); + }); + + it('can get payment events from proxy info retriever', async () => { + const infoRetriever = new ProxyERC20InfoRetriever( + 'b7182613b46c5e92', + transferableReceivableContractAddress, + 0, + erc20LocalhostContractAddress, + '', + PaymentTypes.EVENTS_NAMES.PAYMENT, + 'private', + ); + + // inject mock provider.getLogs() + infoRetriever.provider.getLogs = (filter: ethers.EventFilter): any => { + if ( + !filter.topics?.includes( + '0x9f16cbcc523c67a60c450e5ffe4f3b7b6dbe772e7abcadb2686ce029a9a0a2b6', + ) + ) { + return []; + } + return [ + // Payment to recipient 1 + { + blockNumber: 28, + blockHash: '0x40496f2205f0c8d819c2cab683a5a7e0b20b49d3d891c8943780138670f184c7', + transactionIndex: 0, + address: transferableReceivableContractAddress, + data: '0x0000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000627306090abab3a6e1400e9345bc60c78a8bef57000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef', + topics: [ + '0x9f16cbcc523c67a60c450e5ffe4f3b7b6dbe772e7abcadb2686ce029a9a0a2b6', + '0xa1801d1208f939d16ff239f43c66983c01b1f107994ff695f6a195be4137c796', + ], + transactionHash: '0xa4ccc5094096fb6b2e744cb602ade7f37d0c78d8847e58471d6de786fc9c5283', + logIndex: 4, + }, + // Payment to recipient 2 + { + blockNumber: 28, + blockHash: '0x40496f2205f0c8d819c2cab683a5a7e0b20b49d3d891c8943780138670f184c7', + transactionIndex: 0, + address: transferableReceivableContractAddress, + data: '0x0000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000627306090abab3a6e1400e9345bc60c78a8bef58000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef', + topics: [ + '0x9f16cbcc523c67a60c450e5ffe4f3b7b6dbe772e7abcadb2686ce029a9a0a2b6', + '0xa1801d1208f939d16ff239f43c66983c01b1f107994ff695f6a195be4137c796', + ], + transactionHash: '0xa4ccc5094096fb6b2e744cb602ade7f37d0c78d8847e58471d6de786fc9c5283', + logIndex: 4, + }, + ]; + }; + + // inject mock provider.getBlock() + infoRetriever.provider.getBlock = (): any => { + return { + timestamp: 10, + }; + }; + + // isReceivable = false should not grab any payments + let events = await infoRetriever.getTransferEvents(); + + // if this assert fails it means this address received another transaction + expect(events).toHaveLength(0); + + // isReceivable = true should grab both recipient 1 and recipient 2's payment + events = await infoRetriever.getTransferEvents(true /* isReceivable */); + + // if this assert fails it means this address received another transaction + expect(events).toHaveLength(2); + }); + + it('can get payments from thegraph info-retriever', async () => { + const hashedReference = '0x6c93723bc5f82e6fbb2ea994bf0fb572fa19f7a2a3146065e21752b95668efe5'; + const paymentAddress = '0x5000ee9fb9c96a2a09d8efb695ac21d6c429ff11'; + const feeAddress = '0x5000ee9fb9c96a2a09d8efb695ac21d6c429ff12'; + const paymentData = { + reference: hashedReference, + txHash: '0x3e2d6cc2534b1d340ba2954f34e6cc819d6da64ff76863ea89c6d34b15d13c97', + from: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f29', + to: paymentAddress, + network: 'rinkeby', + salt: '0ee84db293a752c6', + amount: '30000000000000', + requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1', + block: 9606098, + feeAddress: feeAddress, + feeAmount: '0', + }; + const shortReference = PaymentReferenceCalculator.calculate( + '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1', + '0ee84db293a752c6', + paymentAddress, + ); + const onChainReference = utils.keccak256(`0x${shortReference}`); + expect(onChainReference).toEqual(paymentData.reference); + + const paymentsMockData = { + [hashedReference as string]: [ + // Correct reference but incorrect contract + { + contractAddress: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f32', + to: paymentAddress, + from: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f29', + amount: '30000000000000', + feeAmount: '0', + reference: hashedReference, + block: 9606098, + txHash: '0x3e2d6cc2534b1d340ba2954f34e6cc819d6da64ff76863ea89c6d34b15d13c97', + feeAddress: feeAddress, + gasPrice: '', + gasUsed: '', + timestamp: 1, + }, + // Correct reference and contract + { + contractAddress: erc20LocalhostContractAddress, + to: paymentAddress, + from: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f29', + amount: '400', + feeAmount: '0', + reference: hashedReference, + block: 9610470, + txHash: '0x2f7b4752aa259166c038cd9073056c5979760cf0eea55d093fca2095c229313b', + feeAddress: feeAddress, + gasPrice: '', + gasUsed: '', + timestamp: 1, + }, + // Correct reference and contract with different receivable owner + { + contractAddress: erc20LocalhostContractAddress, + to: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f30', + from: '0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f29', + amount: '600', + feeAmount: '0', + reference: hashedReference, + block: 9610471, + txHash: '0x2f7b4752aa259166c038cd9073056c5979760cf0eea55d093fca2095c229313b', + feeAddress: feeAddress, + gasPrice: '', + gasUsed: '', + timestamp: 1, + }, + ], + }; + + const clientMock = { + GetPaymentsAndEscrowState: jest.fn().mockImplementation(({}) => ({ + payments: [], + escrowEvents: [], + })), + GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({ + payments: paymentsMockData[reference] || [], + escrowEvents: [], + })), + GetLastSyncedBlock: jest.fn(), + GetSyncedBlock: jest.fn(), + }; + + erc20TransferableReceivable = new ERC20TransferableReceivablePaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + getSubgraphClient: jest.fn().mockImplementation(({}) => { + return clientMock; + }), + }); + + const graphRetriever = new TheGraphInfoRetriever(clientMock, CurrencyManager.getDefault()); + const allNetworkEvents = await graphRetriever.getReceivableEvents({ + paymentReference: shortReference, + contractAddress: erc20LocalhostContractAddress, + toAddress: '', + eventName: PaymentTypes.EVENTS_NAMES.PAYMENT, + paymentChain: paymentData.network, + }); + + const transferEvents = allNetworkEvents.paymentEvents; + expect(transferEvents).toHaveLength(2); + expect(transferEvents[0].amount).toEqual('400'); + expect(transferEvents[0].name).toEqual('payment'); + expect(transferEvents[0].parameters?.to).toEqual(utils.getAddress(paymentData.to)); + expect(transferEvents[0].parameters?.block).toEqual(9610470); + + expect(transferEvents[1].amount).toEqual('600'); + expect(transferEvents[1].name).toEqual('payment'); + expect(transferEvents[1].parameters?.to).toEqual( + utils.getAddress('0x186e7fe6c34ea0eca7f9c2fd29651fc0443e3f30'), + ); + expect(transferEvents[1].parameters?.block).toEqual(9610471); + }); +}); diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index d5c0848c06..c209f89c1c 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -8,9 +8,12 @@ import { ethers, } from 'ethers'; -import { Erc20PaymentNetwork } from '@requestnetwork/payment-detection'; +import { + Erc20PaymentNetwork, + ERC20TransferableReceivablePaymentDetector, +} from '@requestnetwork/payment-detection'; import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; -import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; +import { ClientTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { @@ -18,9 +21,9 @@ import { getProxyAddress, getProvider, getSigner, - validateRequest, getRequestPaymentValues, - validateMintERC20TransferableReceivable, + validateERC20TransferableReceivable, + validatePayERC20TransferableReceivable, } from './utils'; import { IPreparedTransaction } from './prepared-transaction'; @@ -41,10 +44,7 @@ export async function getReceivableTokenIdForRequest( ): Promise { // Setup the ERC20 proxy contract interface const contract = new ethers.Contract( - getProxyAddress( - request, - Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, - ), + getProxyAddress(request, ERC20TransferableReceivablePaymentDetector.getDeploymentInformation), erc20TransferableReceivableContractAbiFragment, signerOrProvider, ); @@ -93,7 +93,7 @@ export async function mintErc20TransferableReceivable( export function prepareMintErc20TransferableReceivableTransaction( request: ClientTypes.IRequestData, ): IPreparedTransaction { - validateMintERC20TransferableReceivable(request); + validateERC20TransferableReceivable(request); return { data: encodeMintErc20TransferableReceivableRequest(request), @@ -112,7 +112,7 @@ export function prepareMintErc20TransferableReceivableTransaction( export function encodeMintErc20TransferableReceivableRequest( request: ClientTypes.IRequestData, ): string { - validateMintERC20TransferableReceivable(request); + validateERC20TransferableReceivable(request); const tokenAddress = request.currencyInfo.value; const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId @@ -145,6 +145,8 @@ export async function payErc20TransferableReceivableRequest( feeAmount?: BigNumberish, overrides?: ITransactionOverrides, ): Promise { + await validatePayERC20TransferableReceivable(request, signerOrProvider, amount, feeAmount); + const { data, to, value } = await prepareErc20TransferableReceivablePaymentTransaction( request, signerOrProvider, @@ -168,8 +170,6 @@ export async function prepareErc20TransferableReceivablePaymentTransaction( amount?: BigNumberish, feeAmountOverride?: BigNumberish, ): Promise { - validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE); - return { data: await encodePayErc20TransferableReceivableRequest( request, @@ -198,8 +198,6 @@ export async function encodePayErc20TransferableReceivableRequest( amount?: BigNumberish, feeAmountOverride?: BigNumberish, ): Promise { - validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE); - const amountToPay = getAmountToPay(request, amount); const { paymentReference, feeAddress, feeAmount } = getRequestPaymentValues(request); const feeToPay = BigNumber.from(feeAmountOverride || feeAmount || 0); @@ -208,12 +206,6 @@ export async function encodePayErc20TransferableReceivableRequest( const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); - if (receivableTokenId.isZero()) { - throw new Error( - 'The receivable for this request has not been minted yet. Please check with the payee.', - ); - } - return receivableContract.encodeFunctionData('payOwner', [ receivableTokenId, // get tokenId from requestId amountToPay, diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 1c62a801d8..143c80ce32 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -27,6 +27,7 @@ export const noConversionNetworks = [ ExtensionTypes.PAYMENT_NETWORK_ID.ERC777_STREAM, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA, ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, ]; @@ -73,6 +74,7 @@ export class UnsupportedPaymentChain extends Error { * - ERC20_FEE_PROXY_CONTRACT * - ANY_TO_ERC20_PROXY * - ERC777_STREAM + * - ERC20_TRANSFERABLE_RECEIVABLE * * @throws UnsupportedNetworkError if network isn't supported for swap or payment. * @throws UnsupportedPaymentChain if the currency network is not supported (eg Near) diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index e588476357..1ab69b7f86 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -5,6 +5,7 @@ import { ClientTypes, ExtensionTypes, RequestLogicTypes } from '@requestnetwork/ import { getCurrencyHash } from '@requestnetwork/currency'; import { ERC20__factory } from '@requestnetwork/smart-contracts/types'; import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection'; +import { getReceivableTokenIdForRequest } from './erc20-transferable-receivable'; /** @constant MAX_ALLOWANCE set to the max uint256 value */ export const MAX_ALLOWANCE = BigNumber.from(2).pow(256).sub(1); @@ -312,11 +313,47 @@ export function validateConversionFeeProxyRequest( } } +/** + * Validates the parameters for an ERC20 Fee Proxy payment before payment + * @param request to validate + * @param amount optionally, the custom amount to pay + * @param feeAmountOverride optionally, the custom fee amount + * @param signerOrProvider + */ +export async function validatePayERC20TransferableReceivable( + request: ClientTypes.IRequestData, + signerOrProvider: providers.Provider | Signer, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): Promise { + const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); + + if (receivableTokenId.isZero()) { + throw new Error( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); + } + + validateERC20TransferableReceivable(request, amount, feeAmountOverride); +} + /** * Validates the parameters for an ERC20 Fee Proxy payment. * @param request to validate + * @param amount optionally, the custom amount to pay + * @param feeAmountOverride optionally, the custom fee amount */ -export function validateMintERC20TransferableReceivable(request: ClientTypes.IRequestData): void { +export function validateERC20TransferableReceivable( + request: ClientTypes.IRequestData, + amount?: BigNumberish, + feeAmountOverride?: BigNumberish, +): void { + validateErc20FeeProxyRequest( + request, + amount, + feeAmountOverride, + ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, + ); // Validate that there exists a payee if (request.payee == null) { throw new Error(`Expected a payee for this request`); diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts new file mode 100644 index 0000000000..9c4932b4f1 --- /dev/null +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -0,0 +1,187 @@ +import { Wallet, BigNumber, providers } from 'ethers'; + +import { + ClientTypes, + ExtensionTypes, + IdentityTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; +import { deepCopy } from '@requestnetwork/utils'; + +import { approveErc20, getErc20Balance } from '../../src/payment/erc20'; +import { + hasReceivableForRequest, + mintErc20TransferableReceivable, + payErc20TransferableReceivableRequest, +} from '../../src/payment/erc20-transferable-receivable'; + +/* eslint-disable no-magic-numbers */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; + +const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; +const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const provider = new providers.JsonRpcProvider('http://localhost:8545'); +const payeeWallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0").connect(provider); +const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider); +const paymentAddress = payeeWallet.address; + +const validRequest: ClientTypes.IRequestData = { + balance: { + balance: '0', + events: [], + }, + contentData: {}, + creator: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: wallet.address, + }, + currency: 'DAI', + currencyInfo: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20, + value: erc20ContractAddress, + }, + events: [], + expectedAmount: '100', + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + feeAddress, + feeAmount: '0', + paymentAddress, + salt: '0ee84db293a752c6', + }, + version: '0.1.0', + }, + }, + payee: { + type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, + value: paymentAddress, + }, + extensionsData: [], + meta: { + transactionManagerMeta: {}, + }, + pending: null, + requestId: '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e1', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '1.0', +}; + +describe('erc20-transferable-receivable', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + describe('mintErc20TransferableReceivable works', () => { + it('rejects paying without minting', async () => { + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + + const receivableExists = await hasReceivableForRequest(request, wallet); + + if (!receivableExists) { + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); + + const tx = await mintErc20TransferableReceivable(request, payeeWallet, { + gasLimit: BigNumber.from('20000000'), + }); + const confirmedTx = await tx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + } + }); + }); + + describe('payErc20TransferableReceivableRequest', () => { + it('should throw an error if the request is not erc20', async () => { + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; + + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-transferable-receivable request', + ); + }); + + it('should throw an error if the currencyInfo has no value', async () => { + const request = deepCopy(validRequest); + request.currencyInfo.value = ''; + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-transferable-receivable request', + ); + }); + + it('should throw an error if currencyInfo has no network', async () => { + const request = deepCopy(validRequest); + request.currencyInfo.network = ''; + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'Payment currency must have a network', + ); + }); + + it('should throw an error if request has no extension', async () => { + const request = deepCopy(validRequest); + request.extensions = [] as any; + + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'PaymentNetwork not found', + ); + }); + + it('should consider override parameters', async () => { + const spy = jest.fn(); + const originalSendTransaction = wallet.sendTransaction.bind(wallet); + wallet.sendTransaction = spy; + await payErc20TransferableReceivableRequest(validRequest, wallet, undefined, undefined, { + gasPrice: '20000000000', + }); + expect(spy).toHaveBeenCalledWith({ + data: '0x314ee2d90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008d23709a91f6b135f000000000000000000000000000000000000000000000000', + gasPrice: '20000000000', + to: '0xF426505ac145abE033fE77C666840063757Be9cd', + value: 0, + }); + wallet.sendTransaction = originalSendTransaction; + }); + + it('should pay an ERC20 transferable receivable request with fees', async () => { + // first approve the contract + const approvalTx = await approveErc20(validRequest, wallet); + const approvalTxReceipt = await approvalTx.wait(1); + + expect(approvalTxReceipt.status).toBe(1); + expect(approvalTx.hash).not.toBeUndefined(); + + // get the balance to compare after payment + const balanceEthBefore = await wallet.getBalance(); + const balanceErc20Before = await getErc20Balance(validRequest, payeeWallet.address, provider); + + const tx = await payErc20TransferableReceivableRequest(validRequest, wallet, 1, 0, { + gasLimit: BigNumber.from('20000000'), + }); + + const confirmedTx = await tx.wait(1); + + const balanceEthAfter = await wallet.getBalance(); + const balanceErc20After = await getErc20Balance(validRequest, payeeWallet.address, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + expect(balanceEthAfter.lte(balanceEthBefore)).toBeTruthy(); // 'ETH balance should be lower' + + // ERC20 balance should be lower + expect( + BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 91f904850e..548319969b 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -66,7 +66,7 @@ export const getConstructorArgs = (contract: string, network?: string): string[] } const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); - return ['Request Transferable Receivable', 'RReceivable', erc20FeeProxyAddress]; + return ['Request Network Transferable Receivable', 'RTR', erc20FeeProxyAddress]; } default: return []; diff --git a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts index c9e5efb54e..73ab6e9b14 100644 --- a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts +++ b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts @@ -14,8 +14,8 @@ export async function deployERC20TransferableReceivable( 'ERC20TransferableReceivable', { constructorArguments: [ - 'Request Transferable Receivable', - 'RReceivable', + 'Request Network Transferable Receivable', + 'RTR', mainPaymentAddresses.ERC20FeeProxyAddress, ], }, diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json index 9768b5edaa..62284e6640 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json @@ -75,45 +75,46 @@ "anonymous": false, "inputs": [ { - "indexed": false, + "indexed": true, "internalType": "address", - "name": "sender", + "name": "from", "type": "address" }, { - "indexed": false, + "indexed": true, "internalType": "address", - "name": "recipient", + "name": "to", "type": "address" }, { - "indexed": false, + "indexed": true, "internalType": "uint256", - "name": "amount", + "name": "tokenId", "type": "uint256" - }, + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { "indexed": false, "internalType": "address", - "name": "paymentProxy", + "name": "tokenAddress", "type": "address" }, - { - "indexed": false, - "internalType": "uint256", - "name": "receivableTokenId", - "type": "uint256" - }, { "indexed": false, "internalType": "address", - "name": "tokenAddress", + "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "paymentId", + "name": "amount", "type": "uint256" }, { @@ -121,34 +122,21 @@ "internalType": "bytes", "name": "paymentReference", "type": "bytes" - } - ], - "name": "Payment", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ + }, { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" }, { - "indexed": true, + "indexed": false, "internalType": "address", - "name": "to", + "name": "feeAddress", "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "tokenId", - "type": "uint256" } ], - "name": "Transfer", + "name": "TransferWithReferenceAndFee", "type": "event" }, { @@ -157,13 +145,13 @@ { "indexed": false, "internalType": "address", - "name": "tokenAddress", + "name": "sender", "type": "address" }, { "indexed": false, "internalType": "address", - "name": "to", + "name": "recipient", "type": "address" }, { @@ -173,25 +161,37 @@ "type": "uint256" }, { - "indexed": true, - "internalType": "bytes", - "name": "paymentReference", - "type": "bytes" + "indexed": false, + "internalType": "address", + "name": "paymentProxy", + "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "feeAmount", + "name": "receivableTokenId", "type": "uint256" }, { "indexed": false, "internalType": "address", - "name": "feeAddress", + "name": "tokenAddress", "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paymentId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" } ], - "name": "TransferWithReferenceAndFee", + "name": "TransferableReceivablePayment", "type": "event" }, { diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index cf23fe162b..ee22736498 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -33,8 +33,8 @@ describe('contract: ERC20TransferableReceivable', () => { testToken = await new TestERC20__factory(deployer).deploy(BN.from(1000000).mul(BASE_DECIMAL)); erc20FeeProxy = await new ERC20FeeProxy__factory(deployer).deploy(); receivable = await new ERC20TransferableReceivable__factory(deployer).deploy( - 'Request Transferable Receivable', - 'RReceivable', + 'Request Network Transferable Receivable', + 'RTR', erc20FeeProxy.address, ); diff --git a/packages/types/src/extension-types.ts b/packages/types/src/extension-types.ts index 52002111be..b2345a80db 100644 --- a/packages/types/src/extension-types.ts +++ b/packages/types/src/extension-types.ts @@ -96,7 +96,7 @@ export enum PAYMENT_NETWORK_ID { ANY_DECLARATIVE = 'pn-any-declarative', ANY_TO_ERC20_PROXY = 'pn-any-to-erc20-proxy', ANY_TO_ETH_PROXY = 'pn-any-to-eth-proxy', - ERC20_TRANSFERABLE_RECEIVABLE = 'pn-erc20-transferrable-receivable', + ERC20_TRANSFERABLE_RECEIVABLE = 'pn-erc20-transferable-receivable', } export const ID = { From a1699b7bf06211632b029cf71af7e76fbab387ef Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 14 Feb 2023 13:06:59 -0500 Subject: [PATCH 03/14] Rename token to tREC --- packages/smart-contracts/scripts-create2/constructor-args.ts | 2 +- .../scripts/test-deploy-erc20-transferable-receivable.ts | 2 +- .../test/contracts/ERC20TransferableReceivable.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 548319969b..df459d352d 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -66,7 +66,7 @@ export const getConstructorArgs = (contract: string, network?: string): string[] } const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); - return ['Request Network Transferable Receivable', 'RTR', erc20FeeProxyAddress]; + return ['Request Network Transferable Receivable', 'tREC', erc20FeeProxyAddress]; } default: return []; diff --git a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts index 73ab6e9b14..4b0fb5d27a 100644 --- a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts +++ b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts @@ -15,7 +15,7 @@ export async function deployERC20TransferableReceivable( { constructorArguments: [ 'Request Network Transferable Receivable', - 'RTR', + 'tREC', mainPaymentAddresses.ERC20FeeProxyAddress, ], }, diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index ee22736498..7d1a088832 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -34,7 +34,7 @@ describe('contract: ERC20TransferableReceivable', () => { erc20FeeProxy = await new ERC20FeeProxy__factory(deployer).deploy(); receivable = await new ERC20TransferableReceivable__factory(deployer).deploy( 'Request Network Transferable Receivable', - 'RTR', + 'tREC', erc20FeeProxy.address, ); From ee75fc85d82bebebba0c026c7092e84099c8c807 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 14 Feb 2023 15:32:21 -0500 Subject: [PATCH 04/14] Rename tokenURI param --- .../src/contracts/ERC20TransferableReceivable.sol | 4 ++-- .../ERC20TransferableReceivable/0.1.0.json | 2 +- .../contracts/ERC20TransferableReceivable.test.ts | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol index cf30aa52f1..2c09882298 100644 --- a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -102,7 +102,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora bytes calldata paymentReference, uint256 amount, address erc20Addr, - string memory receivableURI + string memory newTokenURI ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); @@ -122,7 +122,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora }); _mint(msg.sender, currentReceivableTokenId); - _setTokenURI(currentReceivableTokenId, receivableURI); + _setTokenURI(currentReceivableTokenId, newTokenURI); } function getTokenIds(address _owner) public view returns (uint256[] memory) { diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json index 62284e6640..fe6f78dc0f 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.0.json @@ -312,7 +312,7 @@ }, { "internalType": "string", - "name": "receivableURI", + "name": "newTokenURI", "type": "string" } ], diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index 7d1a088832..844db3fa26 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -89,6 +89,19 @@ describe('contract: ERC20TransferableReceivable', () => { expect(info[2]).to.equals(0); }); + it('mints with tokenURI set', async function () { + const receivableId = '0x0134cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; + const paymentRef = '0x01' as BytesLike; + await receivable + .connect(user1) + .mint(paymentRef, BASE_DECIMAL, testToken.address, receivableId); + const ids = await receivable.getTokenIds(user1Addr); + const tokenId = ids[0]; + + const tokenURI = await receivable.tokenURI(tokenId); + expect(tokenURI).to.equal(receivableId); + }); + it('list receivables', async function () { await receivable.connect(user1).mint('0x01', BASE_DECIMAL, testToken.address, '1'); await receivable.connect(user1).mint('0x02', BASE_DECIMAL, testToken.address, '2'); From 419393ca304ab0e23d860e00effc93dec913db93 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 14 Feb 2023 15:36:52 -0500 Subject: [PATCH 05/14] Update packages/payment-processor/src/payment/utils.ts Co-authored-by: MantisClone --- packages/payment-processor/src/payment/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 1ab69b7f86..5da79c2c99 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -338,7 +338,7 @@ export async function validatePayERC20TransferableReceivable( } /** - * Validates the parameters for an ERC20 Fee Proxy payment. + * Validates the parameters for an ERC20 Transferable Receivable Payment or Mint. * @param request to validate * @param amount optionally, the custom amount to pay * @param feeAmountOverride optionally, the custom fee amount From fd95ccbe306d6b99e107fbe6a1479dd9336cdb82 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 14 Feb 2023 15:37:06 -0500 Subject: [PATCH 06/14] Update packages/payment-processor/src/payment/utils.ts Co-authored-by: MantisClone --- packages/payment-processor/src/payment/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 5da79c2c99..4f2df025c8 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -314,7 +314,7 @@ export function validateConversionFeeProxyRequest( } /** - * Validates the parameters for an ERC20 Fee Proxy payment before payment + * Validates the parameters for an ERC20 Transferable Receivable payment, esp. that token exists * @param request to validate * @param amount optionally, the custom amount to pay * @param feeAmountOverride optionally, the custom fee amount From bf932191affb9c7ef9ea5f42b42f34b39d6966f0 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 14 Feb 2023 17:57:16 -0500 Subject: [PATCH 07/14] comments --- .../erc20/transferable-receivable.ts | 6 +- .../integration-test/test/scheduled/mocks.ts | 2 +- .../src/erc20/transferable-receivable.ts | 6 +- .../test/erc20/fee-proxy-contract.test.ts | 24 +-- .../erc20/transferable-receivable.test.ts | 163 +++++++++++++++++- .../payment-processor/src/payment/utils.ts | 15 +- .../erc20-transferable-receivable.test.ts | 12 ++ packages/types/src/advanced-logic-types.ts | 2 +- 8 files changed, 184 insertions(+), 46 deletions(-) diff --git a/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts index b5f6c7c091..ac882f5975 100644 --- a/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts +++ b/packages/advanced-logic/src/extensions/payment-network/erc20/transferable-receivable.ts @@ -1,5 +1,5 @@ import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; -import ReferenceBasedPaymentNetwork from '../reference-based'; +import { FeeReferenceBasedPaymentNetwork } from '../fee-reference-based'; const CURRENT_VERSION = '0.1.0'; @@ -7,8 +7,8 @@ const CURRENT_VERSION = '0.1.0'; * Implementation of the payment network to pay in ERC20 based on a transferable receivable contract. */ export default class Erc20TransferableReceivablePaymentNetwork< - TCreationParameters extends ExtensionTypes.PnReferenceBased.ICreationParameters = ExtensionTypes.PnReferenceBased.ICreationParameters, -> extends ReferenceBasedPaymentNetwork { + TCreationParameters extends ExtensionTypes.PnFeeReferenceBased.ICreationParameters = ExtensionTypes.PnFeeReferenceBased.ICreationParameters, +> extends FeeReferenceBasedPaymentNetwork { public constructor( extensionId: ExtensionTypes.PAYMENT_NETWORK_ID = ExtensionTypes.PAYMENT_NETWORK_ID .ERC20_TRANSFERABLE_RECEIVABLE, diff --git a/packages/integration-test/test/scheduled/mocks.ts b/packages/integration-test/test/scheduled/mocks.ts index 95436ecf5f..bce37031b4 100644 --- a/packages/integration-test/test/scheduled/mocks.ts +++ b/packages/integration-test/test/scheduled/mocks.ts @@ -78,6 +78,6 @@ export const mockAdvancedLogic: AdvancedLogicTypes.IAdvancedLogic = { // inheritance from declarative createAddPaymentInstructionAction, createAddRefundInstructionAction, - } as any as Extension.PnReferenceBased.IReferenceBased, + } as any as Extension.PnFeeReferenceBased.IFeeReferenceBased, }, }; diff --git a/packages/payment-detection/src/erc20/transferable-receivable.ts b/packages/payment-detection/src/erc20/transferable-receivable.ts index c6902e5583..4b2ac4b097 100644 --- a/packages/payment-detection/src/erc20/transferable-receivable.ts +++ b/packages/payment-detection/src/erc20/transferable-receivable.ts @@ -4,7 +4,7 @@ import { TheGraphInfoRetriever } from '../thegraph'; import { erc20TransferableReceivableArtifact } from '@requestnetwork/smart-contracts'; import { makeGetDeploymentInformation } from '../utils'; import { PaymentNetworkOptions, ReferenceBasedDetectorOptions } from '../types'; -import { ReferenceBasedDetector } from '../reference-based-detector'; +import { FeeReferenceBasedDetector } from '../fee-reference-based-detector'; import ProxyERC20InfoRetriever from './proxy-info-retriever'; const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = { @@ -14,8 +14,8 @@ const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = { /** * Handle payment networks with ERC20 transferable receivable contract extension */ -export class ERC20TransferableReceivablePaymentDetector extends ReferenceBasedDetector< - ExtensionTypes.PnReferenceBased.IReferenceBased, +export class ERC20TransferableReceivablePaymentDetector extends FeeReferenceBasedDetector< + ExtensionTypes.PnFeeReferenceBased.IFeeReferenceBased, PaymentTypes.IERC20PaymentEventParameters > { private readonly getSubgraphClient: PaymentNetworkOptions['getSubgraphClient']; diff --git a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts index 120746c7e8..497f491763 100644 --- a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts +++ b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts @@ -433,29 +433,7 @@ describe('api/erc20/fee-proxy-contract', () => { ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), escrowEvents: [], })), - GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({ - payments: [ - { - contractAddress: '0x370de27fdb7d1ff1e1baa7d11c5820a324cf623c', - tokenAddress: '0x967da4048cd07ab37855c090aaf366e4ce1b9f48', - to: '0x6c9e04997000d6a8a353951231923d776d4cdff2', - from: '0x15339d48fbe31e349a507fd6d48eb01c45fdc79a', - amount: '168040800000000000000000', - feeAmount: '13386000000000000000', - reference: '0x5ac7241d9e6f419409e439c8429eea2f8f089d76528fd1d5df7496a3e58b5ce1', - block: 15767215, - txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', - gasUsed: '73152', - gasPrice: '12709127644', - timestamp: 1666002347, - amountInCrypto: null, - feeAddress: '0x35d0e078755cd84d3e0656caab417dee1d7939c7', - feeAmountInCrypto: null, - maxRateTimespan: null, - }, - ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), - escrowEvents: [], - })), + GetPaymentsAndEscrowStateForReceivables: jest.fn(), GetLastSyncedBlock: jest.fn(), GetSyncedBlock: jest.fn(), }), diff --git a/packages/payment-detection/test/erc20/transferable-receivable.test.ts b/packages/payment-detection/test/erc20/transferable-receivable.test.ts index b4f4f72a64..2718dfcbfa 100644 --- a/packages/payment-detection/test/erc20/transferable-receivable.test.ts +++ b/packages/payment-detection/test/erc20/transferable-receivable.test.ts @@ -2,7 +2,13 @@ import { CurrencyManager } from '@requestnetwork/currency'; import { TheGraphInfoRetriever } from '../../src/thegraph'; import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; import { ERC20TransferableReceivablePaymentDetector } from '../../src/erc20'; -import { AdvancedLogicTypes, PaymentTypes } from '@requestnetwork/types'; +import { + AdvancedLogicTypes, + ExtensionTypes, + IdentityTypes, + PaymentTypes, + RequestLogicTypes, +} from '@requestnetwork/types'; import { mockAdvancedLogicBase } from '../utils'; import ProxyERC20InfoRetriever from '../../src/erc20/proxy-info-retriever'; import { ethers, utils } from 'ethers'; @@ -48,6 +54,15 @@ describe('api/erc20/transferable-receivable-contract', () => { jest.clearAllMocks(); }); + it('does not support declarative payments', async () => { + expect(() => { + erc20TransferableReceivable.createExtensionsDataForDeclareSentPayment({ + amount: 1, + note: '', + }); + }).toThrowError('this.extension.createDeclareSentPaymentAction is not a function'); + }); + it('can createExtensionsDataForCreation', async () => { await erc20TransferableReceivable.createExtensionsDataForCreation({ paymentAddress: 'ethereum address', @@ -123,7 +138,145 @@ describe('api/erc20/transferable-receivable-contract', () => { }); }); - it('can get payment events from proxy info retriever', async () => { + it('can get balance using thegraph info retriever with two payees', async () => { + const mockRequest: RequestLogicTypes.IRequest = { + creator: { type: IdentityTypes.TYPE.ETHEREUM_ADDRESS, value: '0x2' }, + currency: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20, + value: '0x967da4048cd07ab37855c090aaf366e4ce1b9f48', + }, + events: [], + expectedAmount: '168040800000000000000000', + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]: { + events: [ + { + name: 'create', + parameters: { + feeAddress: '0x35d0e078755Cd84D3E0656cAaB417Dee1d7939c7', + feeAmount: '13386000000000000000', + paymentAddress: '0x6c9E04997000d6A8a353951231923d776d4Cdff2', + salt: 'c75c317e05c52f12', + }, + timestamp: 1665989825, + }, + ], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + salt: 'c75c317e05c52f12', + paymentAddress: '0x6c9E04997000d6A8a353951231923d776d4Cdff2', + feeAddress: '0x35d0e078755Cd84D3E0656cAaB417Dee1d7939c7', + feeAmount: '13386000000000000000', + }, + version: '0.1.0', + }, + }, + extensionsData: [], + requestId: '01169f05b855a57396552cc0052b161f70590bdf9c5371649cd89a70c65fb586db', + state: RequestLogicTypes.STATE.CREATED, + timestamp: 0, + version: '0.2', + }; + erc20TransferableReceivable = new ERC20TransferableReceivablePaymentDetector({ + advancedLogic: mockAdvancedLogic, + currencyManager, + getSubgraphClient: () => ({ + GetPaymentsAndEscrowState: jest.fn(), + GetPaymentsAndEscrowStateForReceivables: jest.fn().mockImplementation(({ reference }) => ({ + payments: [ + // These two payments are not to the same payees + { + contractAddress: '0xF426505ac145abE033fE77C666840063757Be9cd', + tokenAddress: '0x967da4048cd07ab37855c090aaf366e4ce1b9f48', + to: '0x6c9e04997000d6a8a353951231923d776d4cdff2', + from: '0x15339d48fbe31e349a507fd6d48eb01c45fdc79a', + amount: '100', + feeAmount: '13386000000000000000', + reference: '0x5ac7241d9e6f419409e439c8429eea2f8f089d76528fd1d5df7496a3e58b5ce1', + block: 15767215, + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + gasUsed: '73152', + gasPrice: '12709127644', + timestamp: 1666002347, + amountInCrypto: null, + feeAddress: '0x35d0e078755cd84d3e0656caab417dee1d7939c7', + feeAmountInCrypto: null, + maxRateTimespan: null, + }, + { + contractAddress: '0xF426505ac145abE033fE77C666840063757Be9cd', + tokenAddress: '0x967da4048cd07ab37855c090aaf366e4ce1b9f48', + to: '0x6C9E04997000D6a8A353951231923d776d4cdfF3', + from: '0x15339d48fbe31e349a507fd6d48eb01c45fdc79a', + amount: '100', + feeAmount: '13386000000000000000', + reference: '0x5ac7241d9e6f419409e439c8429eea2f8f089d76528fd1d5df7496a3e58b5ce1', + block: 15767215, + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + gasUsed: '73152', + gasPrice: '12709127644', + timestamp: 1666002347, + amountInCrypto: null, + feeAddress: '0x35d0e078755cd84d3e0656caab417dee1d7939c7', + feeAmountInCrypto: null, + maxRateTimespan: null, + }, + ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), + escrowEvents: [], + })), + GetLastSyncedBlock: jest.fn(), + GetSyncedBlock: jest.fn(), + }), + }); + + const { balance, error, events } = await erc20TransferableReceivable.getBalance(mockRequest); + expect(error).toBeUndefined(); + expect(balance).toBe('200'); + expect(events).toMatchObject([ + { + amount: '100', + name: 'payment', + parameters: { + amountInCrypto: undefined, + block: 15767215, + feeAddress: '0x35d0e078755Cd84D3E0656cAaB417Dee1d7939c7', + feeAmount: '13386000000000000000', + feeAmountInCrypto: undefined, + from: '0x15339d48Fbe31E349A507FD6d48Eb01c45Fdc79A', + gasPrice: '12709127644', + gasUsed: '73152', + maxRateTimespan: undefined, + to: '0x6c9E04997000d6A8a353951231923d776d4Cdff2', + tokenAddress: '0x967da4048cD07aB37855c090aAF366e4ce1b9F48', + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + }, + timestamp: 1666002347, + }, + { + amount: '100', + name: 'payment', + parameters: { + amountInCrypto: undefined, + block: 15767215, + feeAddress: '0x35d0e078755Cd84D3E0656cAaB417Dee1d7939c7', + feeAmount: '13386000000000000000', + feeAmountInCrypto: undefined, + from: '0x15339d48Fbe31E349A507FD6d48Eb01c45Fdc79A', + gasPrice: '12709127644', + gasUsed: '73152', + maxRateTimespan: undefined, + to: '0x6C9E04997000D6a8A353951231923d776d4cdfF3', + tokenAddress: '0x967da4048cD07aB37855c090aAF366e4ce1b9F48', + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + }, + timestamp: 1666002347, + }, + ]); + }); + + it('can get payment events from proxy info retriever with two payees', async () => { const infoRetriever = new ProxyERC20InfoRetriever( 'b7182613b46c5e92', transferableReceivableContractAddress, @@ -182,6 +335,12 @@ describe('api/erc20/transferable-receivable-contract', () => { }; }; + // On non-receivable based payment networks, the proxy info retriever filters + // transfer events to make sure the receiver of the payment is the same address as + // the payee on the request. Receivable payment networks can have multiple + // legitimate payees for the same request, so we need to check that getTransferEvents + // supports this as expected. + // isReceivable = false should not grab any payments let events = await infoRetriever.getTransferEvents(); diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 4f2df025c8..9813634270 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -314,7 +314,7 @@ export function validateConversionFeeProxyRequest( } /** - * Validates the parameters for an ERC20 Transferable Receivable payment, esp. that token exists + * Validates the parameters for an ERC20 Transferable Receivable payment, esp. that token exists * @param request to validate * @param amount optionally, the custom amount to pay * @param feeAmountOverride optionally, the custom fee amount @@ -354,22 +354,11 @@ export function validateERC20TransferableReceivable( feeAmountOverride, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, ); + // Validate that there exists a payee if (request.payee == null) { throw new Error(`Expected a payee for this request`); } - - // Validate that there exists an assetAddress - const expectedCurrencyType = - currenciesMap[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE]; - if ( - !expectedCurrencyType || - request.currencyInfo.type !== expectedCurrencyType || - !request.currencyInfo.network || - !request.currencyInfo.value - ) { - throw new Error(`Expected a valid currency ${expectedCurrencyType} on this request`); - } } /** diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index 9c4932b4f1..07f98a7aac 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -10,6 +10,7 @@ import { deepCopy } from '@requestnetwork/utils'; import { approveErc20, getErc20Balance } from '../../src/payment/erc20'; import { + getReceivableTokenIdForRequest, hasReceivableForRequest, mintErc20TransferableReceivable, payErc20TransferableReceivableRequest, @@ -97,6 +98,9 @@ describe('erc20-transferable-receivable', () => { expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); + } else { + const tokenId = await getReceivableTokenIdForRequest(request, wallet); + expect(tokenId.isZero()).toBe(false); } }); }); @@ -119,6 +123,14 @@ describe('erc20-transferable-receivable', () => { ); }); + it('should throw an error if the currencyInfo has no value', async () => { + const request = deepCopy(validRequest); + request.payee = undefined; + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'Expected a payee for this request', + ); + }); + it('should throw an error if currencyInfo has no network', async () => { const request = deepCopy(validRequest); request.currencyInfo.network = ''; diff --git a/packages/types/src/advanced-logic-types.ts b/packages/types/src/advanced-logic-types.ts index 856b01c825..0aeb381c0a 100644 --- a/packages/types/src/advanced-logic-types.ts +++ b/packages/types/src/advanced-logic-types.ts @@ -19,7 +19,7 @@ export interface IAdvancedLogicExtensions { feeProxyContractEth: Extension.PnFeeReferenceBased.IFeeReferenceBased; anyToEthProxy: Extension.PnFeeReferenceBased.IFeeReferenceBased; anyToNativeToken: Extension.PnFeeReferenceBased.IFeeReferenceBased[]; - erc20TransferableReceivable: Extension.PnReferenceBased.IReferenceBased; + erc20TransferableReceivable: Extension.PnFeeReferenceBased.IFeeReferenceBased; } /** Advanced Logic layer */ From 89df694d310a3d68034d86b13bee97fe8c152de8 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 09:53:39 -0500 Subject: [PATCH 08/14] Add event checking to test --- .../test/erc20/transferable-receivable.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/payment-detection/test/erc20/transferable-receivable.test.ts b/packages/payment-detection/test/erc20/transferable-receivable.test.ts index 2718dfcbfa..dbb3bdd319 100644 --- a/packages/payment-detection/test/erc20/transferable-receivable.test.ts +++ b/packages/payment-detection/test/erc20/transferable-receivable.test.ts @@ -352,6 +352,19 @@ describe('api/erc20/transferable-receivable-contract', () => { // if this assert fails it means this address received another transaction expect(events).toHaveLength(2); + expect(events[0].name).toBe(PaymentTypes.EVENTS_NAMES.PAYMENT); + expect(events[0].amount).toBe('10'); + expect(typeof events[0].timestamp).toBe('number'); + expect(events[0].parameters!.to).toBe('0x627306090abaB3A6e1400e9345bC60c78a8BEf57'); + expect(typeof events[0].parameters!.block).toBe('number'); + expect(typeof events[0].parameters!.txHash).toBe('string'); + + expect(events[1].name).toBe(PaymentTypes.EVENTS_NAMES.PAYMENT); + expect(events[1].amount).toBe('10'); + expect(typeof events[1].timestamp).toBe('number'); + expect(events[1].parameters!.to).toBe('0x627306090ABAb3A6E1400e9345bc60c78a8bef58'); + expect(typeof events[1].parameters!.block).toBe('number'); + expect(typeof events[1].parameters!.txHash).toBe('string'); }); it('can get payments from thegraph info-retriever', async () => { From 6078cc684bde087f36a21c16e240d7b07a77dd46 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 10:03:19 -0500 Subject: [PATCH 09/14] Fix creation param type --- packages/types/src/payment-types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 95fc2d39be..a0a6ca1643 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -50,14 +50,14 @@ export type PaymentNetworkCreateParameters = id: | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_PROXY_CONTRACT | ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA - | ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN - | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE; + | ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN; parameters: ExtensionTypes.PnReferenceBased.ICreationParameters; } | { id: | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - | ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT; + | ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT + | ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE; parameters: ExtensionTypes.PnFeeReferenceBased.ICreationParameters; } | { From 0d41757224adf870d6ec6c241251b69f2b172441 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 10:21:55 -0500 Subject: [PATCH 10/14] update transferable receivable test --- .../erc20-transferable-receivable.test.ts | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index 07f98a7aac..14f315101e 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -1,4 +1,4 @@ -import { Wallet, BigNumber, providers } from 'ethers'; +import { Wallet, BigNumber, providers, utils } from 'ethers'; import { ClientTypes, @@ -8,10 +8,11 @@ import { } from '@requestnetwork/types'; import { deepCopy } from '@requestnetwork/utils'; +import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; + import { approveErc20, getErc20Balance } from '../../src/payment/erc20'; import { getReceivableTokenIdForRequest, - hasReceivableForRequest, mintErc20TransferableReceivable, payErc20TransferableReceivableRequest, } from '../../src/payment/erc20-transferable-receivable'; @@ -24,7 +25,7 @@ const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); -const payeeWallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0").connect(provider); +const payeeWallet = Wallet.createRandom().connect(provider); const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider); const paymentAddress = payeeWallet.address; @@ -76,6 +77,18 @@ const validRequest: ClientTypes.IRequestData = { }; describe('erc20-transferable-receivable', () => { + beforeAll(async () => { + // Send funds to payeeWallet + let tx = { + to: paymentAddress, + // Convert currency unit from ether to wei + value: utils.parseEther('1'), + }; + + const txResponse = await wallet.sendTransaction(tx); + await txResponse.wait(1); + }); + beforeEach(() => { jest.restoreAllMocks(); }); @@ -84,24 +97,17 @@ describe('erc20-transferable-receivable', () => { it('rejects paying without minting', async () => { const request = deepCopy(validRequest) as ClientTypes.IRequestData; - const receivableExists = await hasReceivableForRequest(request, wallet); - - if (!receivableExists) { - await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( - 'The receivable for this request has not been minted yet. Please check with the payee.', - ); + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); - const tx = await mintErc20TransferableReceivable(request, payeeWallet, { - gasLimit: BigNumber.from('20000000'), - }); - const confirmedTx = await tx.wait(1); + const tx = await mintErc20TransferableReceivable(request, payeeWallet, { + gasLimit: BigNumber.from('20000000'), + }); + const confirmedTx = await tx.wait(1); - expect(confirmedTx.status).toBe(1); - expect(tx.hash).not.toBeUndefined(); - } else { - const tokenId = await getReceivableTokenIdForRequest(request, wallet); - expect(tokenId.isZero()).toBe(false); - } + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); }); }); @@ -123,7 +129,7 @@ describe('erc20-transferable-receivable', () => { ); }); - it('should throw an error if the currencyInfo has no value', async () => { + it('should throw an error if the payee is undefined', async () => { const request = deepCopy(validRequest); request.payee = undefined; await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( @@ -155,8 +161,18 @@ describe('erc20-transferable-receivable', () => { await payErc20TransferableReceivableRequest(validRequest, wallet, undefined, undefined, { gasPrice: '20000000000', }); + const shortReference = PaymentReferenceCalculator.calculate( + validRequest.requestId, + validRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE] + .values.salt, + paymentAddress, + ); + + const tokenId = await getReceivableTokenIdForRequest(validRequest, wallet); + expect(tokenId.isZero()).toBe(false); + expect(spy).toHaveBeenCalledWith({ - data: '0x314ee2d90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008d23709a91f6b135f000000000000000000000000000000000000000000000000', + data: `0x314ee2d9000000000000000000000000000000000000000000000000000000000000000${tokenId}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`, gasPrice: '20000000000', to: '0xF426505ac145abE033fE77C666840063757Be9cd', value: 0, From 121f6108119e83daf4000b7db2639a3deb2b3c24 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 10:34:54 -0500 Subject: [PATCH 11/14] mint receivable in beforeAll --- .../erc20-transferable-receivable.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index 14f315101e..4b8c96d5ba 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -87,6 +87,14 @@ describe('erc20-transferable-receivable', () => { const txResponse = await wallet.sendTransaction(tx); await txResponse.wait(1); + + const mintTx = await mintErc20TransferableReceivable(validRequest, payeeWallet, { + gasLimit: BigNumber.from('20000000'), + }); + const confirmedTx = await mintTx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(mintTx.hash).not.toBeUndefined(); }); beforeEach(() => { @@ -95,19 +103,13 @@ describe('erc20-transferable-receivable', () => { describe('mintErc20TransferableReceivable works', () => { it('rejects paying without minting', async () => { + // Different request without a minted receivable const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = '0x01'; await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( 'The receivable for this request has not been minted yet. Please check with the payee.', ); - - const tx = await mintErc20TransferableReceivable(request, payeeWallet, { - gasLimit: BigNumber.from('20000000'), - }); - const confirmedTx = await tx.wait(1); - - expect(confirmedTx.status).toBe(1); - expect(tx.hash).not.toBeUndefined(); }); }); From 0ef08dc1cdad5fe01d6e295b3a4b9dbe32b31888 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 10:39:35 -0500 Subject: [PATCH 12/14] Adding createrequest test --- packages/request-client.js/test/index.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/request-client.js/test/index.test.ts b/packages/request-client.js/test/index.test.ts index 140452cb74..38e96ece68 100644 --- a/packages/request-client.js/test/index.test.ts +++ b/packages/request-client.js/test/index.test.ts @@ -1686,6 +1686,41 @@ describe('request-client.js', () => { }); }); + describe('ERC20 transferable receivable contract requests', () => { + it('can create ERC20 transferable receivable requests', async () => { + const requestNetwork = new RequestNetwork({ + signatureProvider: TestData.fakeSignatureProvider, + useMockStorage: true, + }); + + const paymentNetwork: PaymentTypes.PaymentNetworkCreateParameters = { + id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, + parameters: { + paymentAddress: '0xc12F17Da12cd01a9CDBB216949BA0b41A6Ffc4EB', + }, + }; + + const requestInfo = Object.assign({}, TestData.parametersWithoutExtensionsData, { + currency: { + network: 'private', + type: RequestLogicTypes.CURRENCY.ERC20, + value: '0x9FBDa871d559710256a2502A2517b794B482Db40', + }, + }); + + const request = await requestNetwork.createRequest({ + paymentNetwork, + requestInfo, + signer: TestData.payee.identity, + }); + + await new Promise((resolve): any => setTimeout(resolve, 150)); + const data = await request.refresh(); + + expect(data.extensionsData[0].parameters.salt.length).toBe(16); + }); + }); + describe('Conversion requests: payment chain should be deduced from the payment network parameters', () => { it('creates any-to-erc20 requests', async () => { const requestNetwork = new RequestNetwork({ From b72ad0181edb9d5d35ca875ddd47bcdfe2d09581 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 11:31:01 -0500 Subject: [PATCH 13/14] use hex utils in test --- .../test/payment/erc20-transferable-receivable.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index 4b8c96d5ba..094b350fa4 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -174,7 +174,11 @@ describe('erc20-transferable-receivable', () => { expect(tokenId.isZero()).toBe(false); expect(spy).toHaveBeenCalledWith({ - data: `0x314ee2d9000000000000000000000000000000000000000000000000000000000000000${tokenId}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`, + data: `0x314ee2d900000000000000000000000000000000${utils + .hexZeroPad(tokenId.toHexString(), 16) + .substring( + 2, + )}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`, gasPrice: '20000000000', to: '0xF426505ac145abE033fE77C666840063757Be9cd', value: 0, From 2ddc8a40177245f093746d10f9c9f72ed2e3cd4b Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 15 Feb 2023 11:43:42 -0500 Subject: [PATCH 14/14] fix test, add fee params --- packages/request-client.js/test/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/request-client.js/test/index.test.ts b/packages/request-client.js/test/index.test.ts index 38e96ece68..bfa9f8bd56 100644 --- a/packages/request-client.js/test/index.test.ts +++ b/packages/request-client.js/test/index.test.ts @@ -1697,6 +1697,8 @@ describe('request-client.js', () => { id: ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE, parameters: { paymentAddress: '0xc12F17Da12cd01a9CDBB216949BA0b41A6Ffc4EB', + feeAddress: '0x0000000000000000000000000000000000000001', + feeAmount: '0', }, };