From b5caa91cb7c37c85a2997b11f96ba98d43b197ae Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 4 Apr 2023 14:46:05 -0400 Subject: [PATCH 01/14] fix: Gas optimizations for ERC20TransferableReceivable contract --- packages/payment-processor/package.json | 1 + .../payment/erc20-transferable-receivable.ts | 8 +- .../erc20-transferable-receivable.test.ts | 9 +- .../contracts/ERC20TransferableReceivable.sol | 65 +------ .../ERC20TransferableReceivable.test.ts | 172 +++++++++++++----- 5 files changed, 145 insertions(+), 110 deletions(-) diff --git a/packages/payment-processor/package.json b/packages/payment-processor/package.json index ce6c03f5f6..a0a0146e87 100644 --- a/packages/payment-processor/package.json +++ b/packages/payment-processor/package.json @@ -43,6 +43,7 @@ "@requestnetwork/currency": "0.9.0", "@requestnetwork/payment-detection": "0.36.0", "@requestnetwork/smart-contracts": "0.29.0", + "@requestnetwork/multi-format": "0.15.10", "@requestnetwork/types": "0.36.0", "@requestnetwork/utils": "0.36.0", "@superfluid-finance/sdk-core": "0.5.0", diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index fd5f2d1657..3f8bb563dc 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -14,6 +14,7 @@ import { } from '@requestnetwork/payment-detection'; import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes } from '@requestnetwork/types'; +import MultiFormat from '@requestnetwork/multi-format'; import { ITransactionOverrides } from './transaction-overrides'; import { @@ -115,7 +116,7 @@ export function encodeMintErc20TransferableReceivableRequest( validateERC20TransferableReceivable(request); const tokenAddress = request.currencyInfo.value; - const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId + const requestIdDeserialized = MultiFormat.deserialize(request.requestId).value; const { paymentReference, paymentAddress } = getRequestPaymentValues(request); const amount = getAmountToPay(request); @@ -126,7 +127,7 @@ export function encodeMintErc20TransferableReceivableRequest( `0x${paymentReference}`, amount, tokenAddress, - metadata, + requestIdDeserialized, ]); } @@ -204,10 +205,11 @@ export async function encodePayErc20TransferableReceivableRequest( const receivableContract = ERC20TransferableReceivable__factory.createInterface(); + // get tokenId from requestId const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); return receivableContract.encodeFunctionData('payOwner', [ - receivableTokenId, // get tokenId from requestId + receivableTokenId, amountToPay, `0x${paymentReference}`, feeToPay, 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 f1cf3f3fca..9de7774faf 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -118,7 +118,8 @@ describe('erc20-transferable-receivable', () => { it('rejects paying without minting', async () => { // Different request without a minted receivable const request = deepCopy(validRequest) as ClientTypes.IRequestData; - request.requestId = '0x01'; + // Change the request id + request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e2'; await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( 'The receivable for this request has not been minted yet. Please check with the payee.', @@ -235,7 +236,8 @@ describe('erc20-transferable-receivable', () => { it('other wallets can mint receivable for owner', async () => { // Request without a receivable minted yet const request = deepCopy(validRequest) as ClientTypes.IRequestData; - request.requestId = '0x01'; + // Change the request id + request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e3'; const mintTx = await mintErc20TransferableReceivable(request, thirdPartyWallet, { gasLimit: BigNumber.from('20000000'), @@ -268,7 +270,8 @@ describe('erc20-transferable-receivable', () => { it('rejects paying unless minted to correct owner', async () => { // Request without a receivable minted yet const request = deepCopy(validRequest) as ClientTypes.IRequestData; - request.requestId = '0x02'; + // Change the request id + request.requestId = '0188791633ff0ec72a7dbdefb886d2db6cccfa98287320839c2f173c7a4e3ce7e4'; let shortReference = PaymentReferenceCalculator.calculate( request.requestId, diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol index 5ae0409d30..c3d6b45e0f 100644 --- a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -2,8 +2,7 @@ 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'; +import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; /** * @title ERC20TransferableReceivable @@ -11,7 +10,7 @@ import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; * @dev ERC721 contract for creating and managing unique NFTs representing receivables * that can be paid with any ERC20 token */ -contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStorage { +contract ERC20TransferableReceivable is ERC721 { using Counters for Counters.Counter; /** @@ -31,6 +30,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora address tokenAddress; uint256 amount; uint256 balance; + bytes32 requestID; } /** @@ -158,7 +158,8 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora * @param paymentReference A reference for the payment. * @param amount The amount of ERC20 tokens to be paid. * @param erc20Addr The address of the ERC20 token to be used as payment. - * @param newTokenURI The URI of the token. + * @param requestID The ID of the request associated with the receivable. + * Useful for retrieving details of the request from the Request Network. * @dev Anyone can pay for the mint of a receivable on behalf of a user */ function mint( @@ -166,7 +167,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora bytes calldata paymentReference, uint256 amount, address erc20Addr, - string memory newTokenURI + bytes32 requestID ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); @@ -182,60 +183,10 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora receivableInfoMapping[currentReceivableTokenId] = ReceivableInfo({ tokenAddress: erc20Addr, amount: amount, - balance: 0 + balance: 0, + requestID: requestID }); _mint(owner, currentReceivableTokenId); - _setTokenURI(currentReceivableTokenId, newTokenURI); - } - - /** - * @notice Get an array of all receivable token IDs owned by a specific address. - * @param _owner The address of the owner to retrieve the receivable tokens for. - * @return An array of all receivable token IDs owned by the specified address. - */ - 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. - /// @dev Overrides ERC721's _beforeTokenTransfer method to include functionality from ERC721Enumerable. - function _beforeTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal override(ERC721, ERC721Enumerable) { - super._beforeTokenTransfer(from, to, tokenId); - } - - /// @dev Overrides ERC721's _burn method to include functionality from ERC721URIStorage. - function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { - super._burn(tokenId); - } - - /// @dev Overrides ERC721's tokenURI method to include functionality from ERC721URIStorage. - function tokenURI(uint256 tokenId) - public - view - override(ERC721, ERC721URIStorage) - returns (string memory) - { - return super.tokenURI(tokenId); - } - - /// @dev Overrides ERC721's supportsInterface method to include functionality from ERC721Enumerable. - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC721, ERC721Enumerable) - returns (bool) - { - return super.supportsInterface(interfaceId); } } diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index f5390030de..4357f110dc 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -41,76 +41,137 @@ describe('contract: ERC20TransferableReceivable', () => { 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()); + async function verifyReceivables(userAddr: string, tokenIds: any) { + for (let tokenId of tokenIds) { + expect(await receivable.ownerOf(tokenId)).to.equals(userAddr); + } } describe('mint', async function () { it('revert with empty paymentReference', async function () { - await expect(receivable.mint(user1Addr, [], 1, testToken.address, '')).to.be.revertedWith( - 'Zero paymentReference provided', - ); + await expect( + receivable.mint( + user1Addr, + [], + 1, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ), + ).to.be.revertedWith('Zero paymentReference provided'); }); it('revert with zero amount', async function () { - await expect(receivable.mint(user1Addr, '0x01', 0, testToken.address, '')).to.be.revertedWith( - 'Zero amount provided', - ); + await expect( + receivable.mint( + user1Addr, + '0x01', + 0, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ), + ).to.be.revertedWith('Zero amount provided'); }); it('revert with empty asset address', async function () { await expect( - receivable.mint(user1Addr, '0x01', 1, ethers.constants.AddressZero, ''), + receivable.mint( + user1Addr, + '0x01', + 1, + ethers.constants.AddressZero, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ), ).to.be.revertedWith('Zero address provided'); }); - it('revert with duplicated receivableId', async function () { - await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address, ''); + it('revert when trying to mint a receivable for the same request', async function () { + await receivable + .connect(user1) + .mint( + user1Addr, + '0x01', + 1, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); await expect( - receivable.connect(user1).mint(user1Addr, '0x01', 2, testToken.address, ''), + receivable + .connect(user1) + .mint( + user1Addr, + '0x01', + 2, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ), ).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 requestID = '0x34cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; const paymentRef = '0x01' as BytesLike; await receivable .connect(user1) - .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, metadata); - const ids = await receivable.getTokenIds(user1Addr); - const tokenId = ids[0]; + .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, requestID); + const tokenId = 1; 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('mints with tokenURI set', async function () { - const receivableId = '0x0134cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; - const paymentRef = '0x01' as BytesLike; - await receivable - .connect(user1) - .mint(user1Addr, 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); + const receivableInfo = await receivable.receivableInfoMapping(tokenId); + expect(receivableInfo.tokenAddress).to.equal(testToken.address); + expect(receivableInfo.amount).to.equal(BASE_DECIMAL); + expect(receivableInfo.balance).to.equal(0); + expect(receivableInfo.requestID).to.equal(requestID); }); it('list receivables', async function () { - await receivable.connect(user1).mint(user1Addr, '0x01', BASE_DECIMAL, testToken.address, '1'); - await receivable.connect(user1).mint(user1Addr, '0x02', BASE_DECIMAL, testToken.address, '2'); - await receivable.connect(user1).mint(user1Addr, '0x03', BASE_DECIMAL, testToken.address, '3'); + await receivable + .connect(user1) + .mint( + user1Addr, + '0x01', + BASE_DECIMAL, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); + await receivable + .connect(user1) + .mint( + user1Addr, + '0x02', + BASE_DECIMAL, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); + await receivable + .connect(user1) + .mint( + user1Addr, + '0x03', + BASE_DECIMAL, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); await verifyReceivables(user1Addr, [1, 2, 3]); - await receivable.connect(user2).mint(user2Addr, '0x04', BASE_DECIMAL, testToken.address, '4'); - await receivable.connect(user2).mint(user2Addr, '0x05', BASE_DECIMAL, testToken.address, '5'); + await receivable + .connect(user2) + .mint( + user2Addr, + '0x04', + BASE_DECIMAL, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); + await receivable + .connect(user2) + .mint( + user2Addr, + '0x05', + BASE_DECIMAL, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); await verifyReceivables(user2Addr, [4, 5]); await receivable.connect(user1).transferFrom(user1Addr, user2Addr, 1); await verifyReceivables(user1Addr, [3, 2]); @@ -143,9 +204,16 @@ describe('contract: ERC20TransferableReceivable', () => { beforeEach(async () => { paymentRef = '0x01' as BytesLike; amount = BN.from(100).mul(BASE_DECIMAL); - await receivable.connect(user1).mint(user1Addr, paymentRef, amount, testToken.address, '1'); - const ids = await receivable.getTokenIds(await user1.getAddress()); - tokenId = ids[0]; + await receivable + .connect(user1) + .mint( + user1Addr, + paymentRef, + amount, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); + tokenId = BN.from(1); feeAmount = BN.from(10).mul(BASE_DECIMAL); }); @@ -185,12 +253,21 @@ describe('contract: ERC20TransferableReceivable', () => { }); it('allow multiple mints per receivable', async function () { - await receivable.connect(user2).mint(user2Addr, paymentRef, amount, testToken.address, '1'); + await receivable + .connect(user2) + .mint( + user2Addr, + paymentRef, + amount, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); 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 receivableInfoBefore = await receivable.receivableInfoMapping(tokenId); const beforeBal = await testToken.balanceOf(user1Addr); await expect( receivable.payOwner(tokenId, amount.mul(2), paymentRef, 0, ethers.constants.AddressZero), @@ -204,11 +281,12 @@ describe('contract: ERC20TransferableReceivable', () => { 0, ethers.constants.AddressZero, ); + const receivableInfoAfter = await receivable.receivableInfoMapping(tokenId); 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); + expect(amount.mul(2)).to.equals( + receivableInfoAfter.balance.sub(receivableInfoBefore.balance), + ); }); it('payment less than amount', async function () { From eea59fdc6c15c8e46f0463e3555d4a0f51b6f75e Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Tue, 4 Apr 2023 18:03:27 -0400 Subject: [PATCH 02/14] fix tests after merge --- .../ERC20TransferableReceivable.test.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index 191c049a0b..225e198afb 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -98,12 +98,26 @@ describe('contract: ERC20TransferableReceivable', () => { it('revert with empty owner address', async function () { await expect( - receivable.mint(ethers.constants.AddressZero, '0x01', 1, testToken.address, ''), + receivable.mint( + ethers.constants.AddressZero, + '0x01', + 1, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ), ).to.be.revertedWith('Zero address provided for owner'); }); it('revert with duplicated receivableId', async function () { - await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address, ''); + await receivable + .connect(user1) + .mint( + user1Addr, + '0x01', + 1, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); await expect( receivable .connect(user1) @@ -278,10 +292,17 @@ describe('contract: ERC20TransferableReceivable', () => { it('allows user to mint on behalf of another user', async function () { paymentRef = '0x02' as BytesLike; - await receivable.connect(user1).mint(user2Addr, paymentRef, amount, testToken.address, '1'); + await receivable + .connect(user1) + .mint( + user2Addr, + paymentRef, + amount, + testToken.address, + '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', + ); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user2Addr, paymentRef]); - const ids = await receivable.getTokenIds(user2Addr); - tokenId = ids[0]; + tokenId = BN.from(2); expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); }); From 750908c6c094ae512d9a09b87b7ffb3b7c3bce18 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:13:38 +0200 Subject: [PATCH 03/14] fix: default getTheGraphClient accounts for Near (#1095) --- .../src/payment-network-factory.ts | 10 +---- .../payment-detection/src/thegraph/client.ts | 12 ++++++ .../test/near/near-native.test.ts | 40 +++++++++++++++++++ .../test/declarative-payments.test.ts | 2 +- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/payment-detection/src/payment-network-factory.ts b/packages/payment-detection/src/payment-network-factory.ts index b97af4d648..f2f702de75 100644 --- a/packages/payment-detection/src/payment-network-factory.ts +++ b/packages/payment-detection/src/payment-network-factory.ts @@ -25,7 +25,7 @@ import { EthFeeProxyPaymentDetector, EthInputDataPaymentDetector } from './eth'; import { AnyToERC20PaymentDetector, AnyToEthFeeProxyPaymentDetector } from './any'; import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector } from './near'; import { getPaymentNetworkExtension } from './utils'; -import { getTheGraphClient } from './thegraph'; +import { defaultGetTheGraphClient } from './thegraph'; import { getDefaultProvider } from 'ethers'; const PN_ID = ExtensionTypes.PAYMENT_NETWORK_ID; @@ -104,13 +104,7 @@ export class PaymentNetworkFactory { private buildOptions(options: Partial): PaymentNetworkOptions { const defaultOptions: PaymentNetworkOptions = { - getSubgraphClient: (network) => { - return network === 'private' - ? undefined - : getTheGraphClient( - `https://api.thegraph.com/subgraphs/name/requestnetwork/request-payments-${network}`, - ); - }, + getSubgraphClient: defaultGetTheGraphClient, explorerApiKeys: {}, getRpcProvider: getDefaultProvider, }; diff --git a/packages/payment-detection/src/thegraph/client.ts b/packages/payment-detection/src/thegraph/client.ts index 1803ab10e0..a95d570777 100644 --- a/packages/payment-detection/src/thegraph/client.ts +++ b/packages/payment-detection/src/thegraph/client.ts @@ -1,9 +1,13 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { CurrencyTypes } from '@requestnetwork/types'; +import { NearChains } from '@requestnetwork/currency'; import { GraphQLClient } from 'graphql-request'; import { getSdk } from './generated/graphql'; import { getSdk as getNearSdk } from './generated/graphql-near'; +const HOSTED_THE_GRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/requestnetwork/request-payments-'; + // NB: the GraphQL client is automatically generated based on files present in ./queries, // using graphql-codegen. // To generate types, run `yarn codegen`, then open the generated files so that the code editor picks up the changes. @@ -26,3 +30,11 @@ export const getTheGraphClient = (url: string, options?: TheGraphClientOptions) export const getTheGraphNearClient = (url: string, options?: TheGraphClientOptions) => getNearSdk(new GraphQLClient(url, options)); + +export const defaultGetTheGraphClient = (network: CurrencyTypes.ChainName) => { + return network === 'private' + ? undefined + : NearChains.isChainSupported(network) + ? getTheGraphNearClient(`${HOSTED_THE_GRAPH_URL}${network.replace('aurora', 'near')}`) + : getTheGraphClient(`${HOSTED_THE_GRAPH_URL}${network}`); +}; diff --git a/packages/payment-detection/test/near/near-native.test.ts b/packages/payment-detection/test/near/near-native.test.ts index 1060a87db0..9f89aec0e4 100644 --- a/packages/payment-detection/test/near/near-native.test.ts +++ b/packages/payment-detection/test/near/near-native.test.ts @@ -113,6 +113,46 @@ describe('Near payments detection', () => { expect(balance.balance).toBe('1000000000000000000000000'); }); + it('NearNativeTokenPaymentDetector can detect a payment on Near with an additional declarative payment', async () => { + const paymentDetector = new NearNativeTokenPaymentDetector({ + network: 'aurora', + advancedLogic: advancedLogic, + currencyManager: CurrencyManager.getDefault(), + getSubgraphClient: mockedGetSubgraphClient, + }); + const declarativeRequest = { + ...request, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN as string]: { + id: ExtensionTypes.PAYMENT_NETWORK_ID.NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + paymentAddress, + salt, + }, + version: '0.2.0', + events: [ + { + name: ExtensionTypes.PnAnyDeclarative.ACTION.DECLARE_RECEIVED_PAYMENT, + parameters: { + amount: '1000000000000000000000000', + note: 'first payment', + txHash: 'the-first-hash', + network: 'aurora', + }, + timestamp: 10, + }, + ], + }, + }, + }; + const balance = await paymentDetector.getBalance(declarativeRequest); + + expect(mockedGetSubgraphClient).toHaveBeenCalled(); + expect(balance.events).toHaveLength(2); + expect(balance.balance).toBe('2000000000000000000000000'); + }); + describe('Edge cases for NearNativeTokenPaymentDetector', () => { it('throws with a wrong version', async () => { let requestWithWrongVersion = deepCopy(request); diff --git a/packages/request-client.js/test/declarative-payments.test.ts b/packages/request-client.js/test/declarative-payments.test.ts index 54a1fd9dec..11a7f036d0 100644 --- a/packages/request-client.js/test/declarative-payments.test.ts +++ b/packages/request-client.js/test/declarative-payments.test.ts @@ -331,7 +331,7 @@ describe('request-client.js: declarative payments', () => { type: RequestLogicTypes.CURRENCY.ERC20, address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', decimals: 18, - network: 'private', + network: 'private', // private network forces RPC-based `getLogs` symbol: 'FAKE', }, ], From 1a5618b368a3ca49aaf6e4bca3f3210caca3cac1 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Sat, 8 Apr 2023 16:57:24 -0400 Subject: [PATCH 04/14] feat: Allow minting ERC20TransferableReceivables on behalf of other users (#1091) --- .../payment/erc20-transferable-receivable.ts | 4 +- .../erc20-transferable-receivable.test.ts | 136 +++- .../contracts/ERC20TransferableReceivable.sol | 94 ++- .../ERC20TransferableReceivable/0.1.1.json | 655 ++++++++++++++++++ .../ERC20TransferableReceivable.test.ts | 49 +- 5 files changed, 908 insertions(+), 30 deletions(-) create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index c209f89c1c..fd5f2d1657 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -117,12 +117,12 @@ export function encodeMintErc20TransferableReceivableRequest( const tokenAddress = request.currencyInfo.value; const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId - const { paymentReference } = getRequestPaymentValues(request); + const { paymentReference, paymentAddress } = 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 + paymentAddress, `0x${paymentReference}`, amount, tokenAddress, 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 10563ec6c4..f1cf3f3fca 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -8,7 +8,8 @@ import { } from '@requestnetwork/types'; import { deepCopy } from '@requestnetwork/utils'; -import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; +import { Erc20PaymentNetwork, PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; +import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; import { approveErc20, getErc20Balance } from '../../src/payment/erc20'; import { @@ -16,6 +17,7 @@ import { mintErc20TransferableReceivable, payErc20TransferableReceivableRequest, } from '../../src/payment/erc20-transferable-receivable'; +import { getProxyAddress } from '../../src/payment/utils'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -26,6 +28,7 @@ const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const payeeWallet = Wallet.createRandom().connect(provider); +const thirdPartyWallet = Wallet.createRandom().connect(provider); const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider); const paymentAddress = payeeWallet.address; @@ -85,7 +88,17 @@ describe('erc20-transferable-receivable', () => { value: utils.parseEther('1'), }; - const txResponse = await wallet.sendTransaction(tx); + let txResponse = await wallet.sendTransaction(tx); + await txResponse.wait(1); + + // Send funds to thirdPartyWallet + tx = { + to: thirdPartyWallet.address, + // Convert currency unit from ether to wei + value: utils.parseEther('1'), + }; + + txResponse = await wallet.sendTransaction(tx); await txResponse.wait(1); const mintTx = await mintErc20TransferableReceivable(validRequest, payeeWallet, { @@ -218,5 +231,124 @@ describe('erc20-transferable-receivable', () => { BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), ).toBeTruthy(); }); + + it('other wallets can mint receivable for owner', async () => { + // Request without a receivable minted yet + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = '0x01'; + + const mintTx = await mintErc20TransferableReceivable(request, thirdPartyWallet, { + gasLimit: BigNumber.from('20000000'), + }); + let confirmedTx = await mintTx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(mintTx.hash).not.toBeUndefined(); + + // get the balance to compare after payment + const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider); + + const tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, { + gasLimit: BigNumber.from('20000000'), + }); + + confirmedTx = await tx.wait(1); + + const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // ERC20 balance should be lower + expect( + BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), + ).toBeTruthy(); + }); + + it('rejects paying unless minted to correct owner', async () => { + // Request without a receivable minted yet + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = '0x02'; + + let shortReference = PaymentReferenceCalculator.calculate( + request.requestId, + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values + .salt, + paymentAddress, + ); + let metadata = Buffer.from(request.requestId).toString('base64'); + let receivableContract = ERC20TransferableReceivable__factory.createInterface(); + let data = receivableContract.encodeFunctionData('mint', [ + thirdPartyWallet.address, + `0x${shortReference}`, + '100', + erc20ContractAddress, + metadata, + ]); + let tx = await thirdPartyWallet.sendTransaction({ + data, + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }); + let confirmedTx = await tx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); + + // Mint the receivable for the correct paymentAddress + shortReference = PaymentReferenceCalculator.calculate( + request.requestId, + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values + .salt, + paymentAddress, + ); + metadata = Buffer.from(request.requestId).toString('base64'); + receivableContract = ERC20TransferableReceivable__factory.createInterface(); + data = receivableContract.encodeFunctionData('mint', [ + paymentAddress, + `0x${shortReference}`, + '100', + erc20ContractAddress, + metadata, + ]); + tx = await thirdPartyWallet.sendTransaction({ + data, + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }); + confirmedTx = await tx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // get the balance to compare after payment + const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider); + + tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, { + gasLimit: BigNumber.from('20000000'), + }); + + confirmedTx = await tx.wait(1); + + const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // ERC20 balance should be lower + expect( + BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), + ).toBeTruthy(); + }); }); }); diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol index 2c09882298..0d7fc47679 100644 --- a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -5,29 +5,60 @@ import '@openzeppelin/contracts/utils/Counters.sol'; import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol'; import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; +/** + * @title ERC20TransferableReceivable + * @author Request Network + * @dev ERC721 contract for creating and managing unique NFTs representing receivables + * that can be paid with any ERC20 token + */ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStorage { using Counters for Counters.Counter; - // Counter for uniquely identifying payments + /** + * @dev Counter for uniquely identifying payments + */ Counters.Counter private _paymentId; - // Counter for uniquely identifying receivable tokens + /** + * @dev Counter for uniquely identifying receivables + */ Counters.Counter private _receivableTokenId; + /** + * @dev Struct for storing information about a receivable + */ 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 + /** + * @notice Mapping for looking up a receivable given a paymentReference and minter address + */ mapping(bytes32 => uint256) public receivableTokenIdMapping; + /** + * @notice Mapping for looking up information about a receivable given a receivableTokenId + */ + mapping(uint256 => ReceivableInfo) public receivableInfoMapping; + + /** + * @notice Address of the payment proxy contract that handles the transfer of ERC20 tokens + */ address public paymentProxy; - // Event to declare payments to a receivableTokenId + /** + * @notice Event to declare payments to a receivableTokenId + * @param sender The address of the sender + * @param recipient The address of the recipient of the payment + * @param amount The amount of the payment + * @param paymentProxy The address of the payment proxy contract + * @param receivableTokenId The ID of the receivable being paid + * @param tokenAddress The address of the ERC20 token used to pay the receivable + * @param paymentId The ID of the payment + * @param paymentReference The reference for the payment + */ event TransferableReceivablePayment( address sender, address recipient, @@ -39,8 +70,16 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora bytes indexed paymentReference ); - // Event to declare a transfer with a reference - // This event is emitted from a delegatecall to an ERC20FeeProxy contract + /** + * @notice Event to declare ERC20 token transfers + * @param tokenAddress The address of the ERC20 token being transferred + * @param to The address of the recipient of the transfer + * @param amount The amount of the transfer + * @param paymentReference The reference for the transfer + * @param feeAmount The amount of the transfer fee + * @param feeAddress The address of the fee recipient + * @dev This event is emitted from a delegatecall to an ERC20FeeProxy contract + */ event TransferWithReferenceAndFee( address tokenAddress, address to, @@ -50,6 +89,11 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora address feeAddress ); + /** + * @param name The name of the ERC721 token + * @param symbol The symbol of the ERC721 token + * @param _paymentProxyAddress The address of the payment proxy contract + */ constructor( string memory name, string memory symbol, @@ -58,6 +102,16 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora paymentProxy = _paymentProxyAddress; } + /** + * @notice Pay the owner of the specified receivable with the provided amount of ERC20 tokens. + * @param receivableTokenId The ID of the receivable token to pay. + * @param amount The amount of ERC20 tokens to pay the owner. + * @param paymentReference The reference for the payment. + * @param feeAmount The amount of ERC20 tokens to be paid as a fee. + * @param feeAddress The address to which the fee should be paid. + * @dev This function uses delegatecall to call on a contract which emits + a TransferWithReferenceAndFee event. + */ function payOwner( uint256 receivableTokenId, uint256 amount, @@ -98,7 +152,17 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora ); } + /** + * @notice Mint a new transferable receivable. + * @param owner The address to whom the receivable token will be minted. + * @param paymentReference A reference for the payment. + * @param amount The amount of ERC20 tokens to be paid. + * @param erc20Addr The address of the ERC20 token to be used as payment. + * @param newTokenURI The URI to be set on the minted receivable token. + * @dev Anyone can pay for the mint of a receivable on behalf of a user + */ function mint( + address owner, bytes calldata paymentReference, uint256 amount, address erc20Addr, @@ -106,8 +170,9 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); + require(owner != address(0), 'Zero address provided for owner'); require(erc20Addr != address(0), 'Zero address provided'); - bytes32 idKey = keccak256(abi.encodePacked(msg.sender, paymentReference)); + bytes32 idKey = keccak256(abi.encodePacked(owner, paymentReference)); require( receivableTokenIdMapping[idKey] == 0, 'Receivable has already been minted for this user and request' @@ -121,10 +186,15 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora balance: 0 }); - _mint(msg.sender, currentReceivableTokenId); + _mint(owner, currentReceivableTokenId); _setTokenURI(currentReceivableTokenId, newTokenURI); } + /** + * @notice Get an array of all receivable token IDs owned by a specific address. + * @param _owner The address that owns the receivable tokens. + * @return An array of all receivable token IDs owned by the specified address. + */ function getTokenIds(address _owner) public view returns (uint256[] memory) { uint256[] memory _tokensOfOwner = new uint256[](ERC721.balanceOf(_owner)); uint256 i; @@ -136,6 +206,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora } // The following functions are overrides required by Solidity. + /// @dev Overrides ERC721's _beforeTokenTransfer method to include functionality from ERC721Enumerable. function _beforeTokenTransfer( address from, address to, @@ -144,10 +215,12 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora super._beforeTokenTransfer(from, to, tokenId); } + /// @dev Overrides ERC721's _burn method to include functionality from ERC721URIStorage. function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { super._burn(tokenId); } + /// @dev Overrides ERC721's tokenURI method to include functionality from ERC721URIStorage. function tokenURI(uint256 tokenId) public view @@ -157,6 +230,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora return super.tokenURI(tokenId); } + /// @dev Overrides ERC721's supportsInterface method to include functionality from ERC721Enumerable. function supportsInterface(bytes4 interfaceId) public view diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json new file mode 100644 index 0000000000..aa1b105406 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json @@ -0,0 +1,655 @@ +{ + "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": 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" + }, + { + "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": "TransferableReceivablePayment", + "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": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "erc20Addr", + "type": "address" + }, + { + "internalType": "string", + "name": "newTokenURI", + "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/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index 844db3fa26..5a9f012e27 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -48,27 +48,33 @@ describe('contract: ERC20TransferableReceivable', () => { describe('mint', async function () { it('revert with empty paymentReference', async function () { - await expect(receivable.mint([], 1, testToken.address, '')).to.be.revertedWith( + await expect(receivable.mint(user1Addr, [], 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( + await expect(receivable.mint(user1Addr, '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', - ); + await expect( + receivable.mint(user1Addr, '0x01', 1, ethers.constants.AddressZero, ''), + ).to.be.revertedWith('Zero address provided'); + }); + + it('revert with empty owner address', async function () { + await expect( + receivable.mint(ethers.constants.AddressZero, '0x01', 1, testToken.address, ''), + ).to.be.revertedWith('Zero address provided for owner'); }); it('revert with duplicated receivableId', async function () { - await receivable.connect(user1).mint('0x01', 1, testToken.address, ''); + await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address, ''); await expect( - receivable.connect(user1).mint('0x01', 2, testToken.address, ''), + receivable.connect(user1).mint(user1Addr, '0x01', 2, testToken.address, ''), ).to.be.revertedWith('Receivable has already been minted for this user and request'); }); @@ -76,7 +82,9 @@ describe('contract: ERC20TransferableReceivable', () => { 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); + await receivable + .connect(user1) + .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, metadata); const ids = await receivable.getTokenIds(user1Addr); const tokenId = ids[0]; expect(await receivable.ownerOf(tokenId)).to.equals(user1Addr); @@ -94,7 +102,7 @@ describe('contract: ERC20TransferableReceivable', () => { const paymentRef = '0x01' as BytesLike; await receivable .connect(user1) - .mint(paymentRef, BASE_DECIMAL, testToken.address, receivableId); + .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, receivableId); const ids = await receivable.getTokenIds(user1Addr); const tokenId = ids[0]; @@ -103,12 +111,12 @@ describe('contract: ERC20TransferableReceivable', () => { }); 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 receivable.connect(user1).mint(user1Addr, '0x01', BASE_DECIMAL, testToken.address, '1'); + await receivable.connect(user1).mint(user1Addr, '0x02', BASE_DECIMAL, testToken.address, '2'); + await receivable.connect(user1).mint(user1Addr, '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 receivable.connect(user2).mint(user2Addr, '0x04', BASE_DECIMAL, testToken.address, '4'); + await receivable.connect(user2).mint(user2Addr, '0x05', BASE_DECIMAL, testToken.address, '5'); await verifyReceivables(user2Addr, [4, 5]); await receivable.connect(user1).transferFrom(user1Addr, user2Addr, 1); await verifyReceivables(user1Addr, [3, 2]); @@ -141,7 +149,7 @@ describe('contract: ERC20TransferableReceivable', () => { beforeEach(async () => { paymentRef = '0x01' as BytesLike; amount = BN.from(100).mul(BASE_DECIMAL); - await receivable.connect(user1).mint(paymentRef, amount, testToken.address, '1'); + await receivable.connect(user1).mint(user1Addr, paymentRef, amount, testToken.address, '1'); const ids = await receivable.getTokenIds(await user1.getAddress()); tokenId = ids[0]; feeAmount = BN.from(10).mul(BASE_DECIMAL); @@ -183,11 +191,20 @@ describe('contract: ERC20TransferableReceivable', () => { }); it('allow multiple mints per receivable', async function () { - await receivable.connect(user2).mint(paymentRef, amount, testToken.address, '1'); + await receivable.connect(user2).mint(user2Addr, paymentRef, amount, testToken.address, '1'); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); }); + it('allows user to mint on behalf of another user', async function () { + paymentRef = '0x02' as BytesLike; + await receivable.connect(user1).mint(user2Addr, paymentRef, amount, testToken.address, '1'); + const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user2Addr, paymentRef]); + const ids = await receivable.getTokenIds(user2Addr); + tokenId = ids[0]; + expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); + }); + it('payment greater than amount', async function () { const beforeBal = await testToken.balanceOf(user1Addr); await expect( From ccc0bd4ffed49ee41b9991b816798797d6930f67 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 13 Apr 2023 17:01:49 +0200 Subject: [PATCH 05/14] feat: processor for ERC20FeeProxy on Near (#1096) --- packages/payment-processor/src/index.ts | 1 + .../src/payment/erc20-fee-proxy.ts | 5 +- .../payment-processor/src/payment/index.ts | 4 +- .../src/payment/near-fungible.ts | 62 ++++++ .../src/payment/swap-any-to-erc20.ts | 6 +- .../src/payment/swap-erc20-fee-proxy.ts | 5 +- .../src/payment/utils-near.ts | 122 +++++++++++- .../payment-processor/src/payment/utils.ts | 9 +- .../test/payment/any-to-near.test.ts | 10 +- .../test/payment/erc20-fee-proxy-near.test.ts | 187 ++++++++++++++++++ .../test/payment/index.test.ts | 4 +- .../src/lib/artifacts/ERC20FeeProxy/index.ts | 4 + 12 files changed, 387 insertions(+), 32 deletions(-) create mode 100644 packages/payment-processor/src/payment/near-fungible.ts create mode 100644 packages/payment-processor/test/payment/erc20-fee-proxy-near.test.ts diff --git a/packages/payment-processor/src/index.ts b/packages/payment-processor/src/index.ts index b4f430b1ef..f6b50a77a7 100644 --- a/packages/payment-processor/src/index.ts +++ b/packages/payment-processor/src/index.ts @@ -8,6 +8,7 @@ export * from './payment/erc777-utils'; export * from './payment/eth-input-data'; export * from './payment/near-input-data'; export * from './payment/near-conversion'; +export * from './payment/near-fungible'; export * from './payment/eth-proxy'; export * from './payment/eth-fee-proxy'; export * from './payment/batch-proxy'; diff --git a/packages/payment-processor/src/payment/erc20-fee-proxy.ts b/packages/payment-processor/src/payment/erc20-fee-proxy.ts index bbf203f98a..9d21d4b45e 100644 --- a/packages/payment-processor/src/payment/erc20-fee-proxy.ts +++ b/packages/payment-processor/src/payment/erc20-fee-proxy.ts @@ -80,9 +80,8 @@ export function _getErc20FeeProxyPaymentUrl( feeAmountOverride?: BigNumberish, ): string { validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); - const { paymentReference, paymentAddress, feeAddress, feeAmount, version } = + const { paymentReference, paymentAddress, feeAddress, feeAmount, version, network } = getRequestPaymentValues(request); - const { network } = request.currencyInfo; EvmChains.assertChainSupported(network!); const contractAddress = erc20FeeProxyArtifact.getAddress(network, version); const amountToPay = getAmountToPay(request, amount); @@ -92,7 +91,7 @@ export function _getErc20FeeProxyPaymentUrl( } /** - * Prepate the transaction to pay a request through the ERC20 fee proxy contract, can be used with a Multisig contract. + * Prepare the transaction to pay a request through the ERC20 fee proxy 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. diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 4236b795a4..f73b6dca08 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -243,9 +243,9 @@ export async function isSolvent( ): Promise { // Near case if (NearChains.isChainSupported(currency.network) && providerOptions?.nearWalletConnection) { - return isNearAccountSolvent(amount, providerOptions.nearWalletConnection); + return isNearAccountSolvent(amount, providerOptions.nearWalletConnection, currency); } - // Main case (web3) + // Main case (EVM) if (!providerOptions?.provider) { throw new Error('provider missing'); } diff --git a/packages/payment-processor/src/payment/near-fungible.ts b/packages/payment-processor/src/payment/near-fungible.ts new file mode 100644 index 0000000000..938ca65285 --- /dev/null +++ b/packages/payment-processor/src/payment/near-fungible.ts @@ -0,0 +1,62 @@ +import { BigNumberish } from 'ethers'; +import { WalletConnection } from 'near-api-js'; + +import { erc20FeeProxyArtifact } from '@requestnetwork/smart-contracts'; +import { ClientTypes, ExtensionTypes } from '@requestnetwork/types'; + +import { getRequestPaymentValues, validateRequest, getAmountToPay } from './utils'; +import { + INearTransactionCallback, + isReceiverReady, + processNearFungiblePayment, +} from './utils-near'; +import { NearChains } from '@requestnetwork/currency'; + +/** + * Processes the transaction to pay a request in fungible token on NEAR with fee (Erc20FeeProxy). + * @param request the request to pay + */ +export async function payNearFungibleRequest( + request: ClientTypes.IRequestData, + walletConnection: WalletConnection, + amount?: BigNumberish, + callback?: INearTransactionCallback, +): Promise { + validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT); + + const { paymentReference, paymentAddress, feeAddress, feeAmount, network } = + getRequestPaymentValues(request); + + if (!paymentReference) { + throw new Error('Cannot pay without a paymentReference'); + } + + if (!network || !NearChains.isChainSupported(network)) { + throw new Error('Should be a Near network'); + } + NearChains.assertChainSupported(network); + + const amountToPay = getAmountToPay(request, amount).toString(); + + if (!(await isReceiverReady(walletConnection, request.currencyInfo.value, paymentAddress))) { + throw new Error( + `The paymentAddress is not registered for the token ${request.currencyInfo.value}`, + ); + } + const proxyAddress = erc20FeeProxyArtifact.getAddress(network, 'near'); + if (!(await isReceiverReady(walletConnection, request.currencyInfo.value, proxyAddress))) { + throw new Error(`The proxy is not registered for the token ${request.currencyInfo.value}`); + } + + return processNearFungiblePayment( + walletConnection, + network, + amountToPay, + paymentAddress, + paymentReference, + request.currencyInfo.value, + feeAddress || '0x', + feeAmount || 0, + callback, + ); +} diff --git a/packages/payment-processor/src/payment/swap-any-to-erc20.ts b/packages/payment-processor/src/payment/swap-any-to-erc20.ts index bf16e5dfdc..e53d3278df 100644 --- a/packages/payment-processor/src/payment/swap-any-to-erc20.ts +++ b/packages/payment-processor/src/payment/swap-any-to-erc20.ts @@ -77,8 +77,8 @@ export function encodeSwapToPayAnyToErc20Request( signerOrProvider: providers.Provider | Signer = getProvider(), options: IRequestPaymentOptions, ): string { - const conversionSettings = options?.conversion; - const swapSettings = options?.swap; + const conversionSettings = options.conversion; + const swapSettings = options.swap; if (!conversionSettings) { throw new Error(`Conversion Settings are required`); @@ -87,7 +87,7 @@ export function encodeSwapToPayAnyToErc20Request( throw new Error(`Swap Settings are required`); } const currencyManager = conversionSettings.currencyManager || CurrencyManager.getDefault(); - const network = conversionSettings.currency?.network; + const network = conversionSettings.currency.network; if (!network) { throw new Error(`Currency in conversion settings must have a network`); } diff --git a/packages/payment-processor/src/payment/swap-erc20-fee-proxy.ts b/packages/payment-processor/src/payment/swap-erc20-fee-proxy.ts index 19b4f41c5c..6adc58379b 100644 --- a/packages/payment-processor/src/payment/swap-erc20-fee-proxy.ts +++ b/packages/payment-processor/src/payment/swap-erc20-fee-proxy.ts @@ -114,15 +114,14 @@ export function encodeSwapToPayErc20FeeRequest( swapSettings: ISwapSettings, options?: IRequestPaymentOptions, ): string { - const { network } = request.currencyInfo; + const { paymentReference, paymentAddress, feeAddress, feeAmount, network } = + getRequestPaymentValues(request); EvmChains.assertChainSupported(network!); validateErc20FeeProxyRequest(request, options?.amount, options?.feeAmount); const signer = getSigner(signerOrProvider); const tokenAddress = request.currencyInfo.value; - const { paymentReference, paymentAddress, feeAddress, feeAmount } = - getRequestPaymentValues(request); const amountToPay = getAmountToPay(request, options?.amount); const feeToPay = BigNumber.from(options?.feeAmount || feeAmount || 0); diff --git a/packages/payment-processor/src/payment/utils-near.ts b/packages/payment-processor/src/payment/utils-near.ts index 3d8c9ca5d9..8efa0a2d0b 100644 --- a/packages/payment-processor/src/payment/utils-near.ts +++ b/packages/payment-processor/src/payment/utils-near.ts @@ -4,7 +4,8 @@ import { NearConversionNativeTokenPaymentDetector, NearNativeTokenPaymentDetector, } from '@requestnetwork/payment-detection'; -import { CurrencyTypes } from '@requestnetwork/types'; +import { CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { erc20FeeProxyArtifact } from '@requestnetwork/smart-contracts'; /** * Callback arguments for the Near web wallet. @@ -28,20 +29,36 @@ export const isValidNearAddress = async (nearNetwork: Near, address: string): Pr export const isNearAccountSolvent = ( amount: BigNumberish, nearWalletConnection: WalletConnection, + token?: RequestLogicTypes.ICurrency, ): Promise => { - return nearWalletConnection - .account() - .state() - .then((state) => { - const balance = BigNumber.from(state?.amount ?? '0'); - return balance.gte(amount); - }); + if (!token || token.type === RequestLogicTypes.CURRENCY.ETH) { + return nearWalletConnection + .account() + .state() + .then((state) => { + const balance = BigNumber.from(state?.amount ?? '0'); + return balance.gte(amount); + }); + } + if (token.type === RequestLogicTypes.CURRENCY.ERC20) { + const fungibleContract = new Contract(nearWalletConnection.account(), token.value, { + changeMethods: [], + viewMethods: ['ft_balance_of'], + }) as any; + return fungibleContract + .ft_balance_of({ + account_id: nearWalletConnection.account().accountId, + }) + .then((balance: string) => BigNumber.from(balance).gte(amount)); + } + throw new Error(`isNearAccountSolvent not implemented for ${token.type}`); }; const GAS_LIMIT_IN_TGAS = 50; const GAS_LIMIT = ethers.utils.parseUnits(GAS_LIMIT_IN_TGAS.toString(), 12); const GAS_LIMIT_NATIVE = GAS_LIMIT.toString(); -const GAS_LIMIT_CONVERSION_TO_NATIVE = GAS_LIMIT.mul(2).toString(); +const GAS_LIMIT_CONVERSION_TO_NATIVE = GAS_LIMIT.mul(2).toString(); // 200 TGas +const GAS_LIMIT_FUNGIBLE_PROXY = GAS_LIMIT.mul(4).toString(); // 400 TGas export const processNearPayment = async ( walletConnection: WalletConnection, @@ -148,3 +165,90 @@ export const processNearPaymentWithConversion = async ( throw new Error(`Could not pay Near request. Got ${e.message}`); } }; + +export const processNearFungiblePayment = async ( + walletConnection: WalletConnection, + network: CurrencyTypes.NearChainName, + amount: BigNumberish, + to: string, + paymentReference: string, + currencyAddress: string, + feeAddress: string, + feeAmount: BigNumberish, + callback: INearTransactionCallback | undefined = undefined, +): Promise => { + const fungibleContract = new Contract(walletConnection.account(), currencyAddress, { + changeMethods: ['ft_transfer_call'], + viewMethods: [], + }) as any; + + const proxyAddress = erc20FeeProxyArtifact.getAddress(network, 'near'); + await fungibleContract.ft_transfer_call({ + args: { + receiver_id: proxyAddress, + amount, + msg: JSON.stringify({ + fee_address: feeAddress, + fee_amount: feeAmount, + payment_reference: paymentReference, + to, + }), + }, + gas: GAS_LIMIT_FUNGIBLE_PROXY, + amount: '1'.toString(), // 1 yoctoNEAR deposit is mandatory for ft_transfer_call + ...callback, + }); +}; + +type StorageBalance = { + total: string; + available: string; +}; + +// min. 0.00125 Ⓝ +const MIN_STORAGE_FOR_FUNGIBLE = '1250000000000000000000'; + +/** + * Stores the minimum deposit amount on the `paymentAddress` account for `tokenAddress`. + * This does not check the existing deposit, if any, and should be called if `isReceiverReady` is false. + * @param walletConnection + * @param tokenAddress + * @param paymentAddress + */ +export const storageDeposit = async ( + walletConnection: WalletConnection, + tokenAddress: string, + paymentAddress: string, +): Promise => { + const fungibleContract = new Contract(walletConnection.account(), tokenAddress, { + changeMethods: ['storage_deposit'], + viewMethods: [], + }) as any; + await fungibleContract.storage_deposit({ + args: { account_id: paymentAddress }, + value: MIN_STORAGE_FOR_FUNGIBLE, + }); +}; + +/** + * This checks that the `paymentAddress` has enough storage on the `tokenAddress` to receive tokens. + * + * It returns false if trying to send tokens to the `paymentAddress` would result in: + * + * - 'Smart contract panicked: The account account.identifier.near is not registered' + * + */ +export const isReceiverReady = async ( + walletConnection: WalletConnection, + tokenAddress: string, + paymentAddress: string, +): Promise => { + const fungibleContract = new Contract(walletConnection.account(), tokenAddress, { + changeMethods: [], + viewMethods: ['storage_balance_of'], + }) as any; + const storage = (await fungibleContract.storage_balance_of({ + account_id: paymentAddress, + })) as StorageBalance | null; + return !!storage && BigNumber.from(storage?.total).gte(MIN_STORAGE_FOR_FUNGIBLE); +}; diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index c3e8a95e23..307ca240e7 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -67,9 +67,8 @@ export function getSigner( } /** - * Utility to access the payment address, reference, - * and optional feeAmount, feeAddress, expectedFlowRate, expectedStartDate - * of a Request. + * Utility to access payment-related information from a request. + * All data is taken from the request's payment extension, except the network that may be retrieved from the request's currency if needed. */ export function getRequestPaymentValues(request: ClientTypes.IRequestData): { paymentAddress: string; @@ -107,7 +106,7 @@ export function getRequestPaymentValues(request: ClientTypes.IRequestData): { expectedStartDate, tokensAccepted, maxRateTimespan, - network, + network: network ?? request.currencyInfo.network, version: extension.version, }; } @@ -207,7 +206,7 @@ export function validateRequest( getRequestPaymentValues(request); let extension = request.extensions[paymentNetworkId]; - // FIXME: updating the extension: not needed anymore when "invoicing" will use only ethFeeProxy + // FIXME: updating the extension: not needed anymore when ETH_INPUT_DATA gets deprecated if (paymentNetworkId === ExtensionTypes.PAYMENT_NETWORK_ID.ETH_FEE_PROXY_CONTRACT && !extension) { extension = request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ETH_INPUT_DATA]; } diff --git a/packages/payment-processor/test/payment/any-to-near.test.ts b/packages/payment-processor/test/payment/any-to-near.test.ts index 0c673127fc..88eda1750e 100644 --- a/packages/payment-processor/test/payment/any-to-near.test.ts +++ b/packages/payment-processor/test/payment/any-to-near.test.ts @@ -89,7 +89,7 @@ describe('payNearWithConversionRequest', () => { }, ); }); - it('throws when tyring to pay another payment extension', async () => { + it('throws when trying to pay another payment extension', async () => { // A mock is used to bypass Near wallet connection for address validation and contract interaction const paymentSpy = jest .spyOn(nearUtils, 'processNearPaymentWithConversion') @@ -115,9 +115,9 @@ describe('payNearWithConversionRequest', () => { ).rejects.toThrowError( 'request cannot be processed, or is not an pn-any-to-native-token request', ); - expect(paymentSpy).toHaveBeenCalledTimes(0); + expect(paymentSpy).not.toHaveBeenCalled(); }); - it('throws when tyring to pay with an unsupported currency', async () => { + it('throws when trying to pay with an unsupported currency', async () => { // A mock is used to bypass Near wallet connection for address validation and contract interaction const paymentSpy = jest .spyOn(nearUtils, 'processNearPaymentWithConversion') @@ -140,7 +140,7 @@ describe('payNearWithConversionRequest', () => { await expect( payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), ).rejects.toThrowError('Near payment with conversion only implemented for fiat denominations.'); - expect(paymentSpy).toHaveBeenCalledTimes(0); + expect(paymentSpy).not.toHaveBeenCalled(); }); it('throws when the netwrok is not near', async () => { // A mock is used to bypass Near wallet connection for address validation and contract interaction @@ -171,6 +171,6 @@ describe('payNearWithConversionRequest', () => { await expect( payNearConversionRequest(invalidRequest, mockedNearWalletConnection, conversionSettings), ).rejects.toThrowError('Should be a Near network'); - expect(paymentSpy).toHaveBeenCalledTimes(0); + expect(paymentSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/payment-processor/test/payment/erc20-fee-proxy-near.test.ts b/packages/payment-processor/test/payment/erc20-fee-proxy-near.test.ts new file mode 100644 index 0000000000..e114acf178 --- /dev/null +++ b/packages/payment-processor/test/payment/erc20-fee-proxy-near.test.ts @@ -0,0 +1,187 @@ +import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; +import { deepCopy } from '@requestnetwork/utils'; +import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; + +import * as nearUtils from '../../src/payment/utils-near'; +import { payNearFungibleRequest } from '../../src/payment/near-fungible'; + +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable @typescript-eslint/await-thenable */ + +const fau = { + type: RequestLogicTypes.CURRENCY.ERC20, + value: 'fau.reqnetwork.testnet', + network: 'aurora-testnet', +}; + +const salt = 'a6475e4c3d45feb6'; +const paymentAddress = 'issuer.testnet'; +const feeAddress = 'fee.testnet'; +const network = 'aurora-testnet'; +const feeAmount = '5'; +const request: any = { + requestId: '0x123', + expectedAmount: '100', + currencyInfo: fau, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + events: [], + id: ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_NATIVE_TOKEN, + type: ExtensionTypes.TYPE.PAYMENT_NETWORK, + values: { + salt, + paymentAddress, + feeAddress, + network, + feeAmount, + }, + version: '0.1.0', + }, + }, +}; +let paymentSpy: ReturnType; + +describe('payNearFungibleRequest', () => { + beforeEach(() => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + paymentSpy = jest + .spyOn(nearUtils, 'processNearFungiblePayment') + .mockReturnValue(Promise.resolve()); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + it('pays a FAU-near request (with mock)', async () => { + jest.spyOn(nearUtils, 'isReceiverReady').mockReturnValue(Promise.resolve(true)); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + }), + } as any; + + const paymentReference = PaymentReferenceCalculator.calculate( + request.requestId, + salt, + paymentAddress, + ); + + await payNearFungibleRequest(request, mockedNearWalletConnection, undefined, { + callbackUrl: 'https://some.callback.url', + meta: 'param', + }); + expect(paymentSpy).toHaveBeenCalledWith( + expect.anything(), + 'aurora-testnet', + '100', + paymentAddress, + paymentReference, + 'fau.reqnetwork.testnet', + feeAddress, + feeAmount, + { + callbackUrl: 'https://some.callback.url', + meta: 'param', + }, + ); + }); + + it('throws when trying to pay if the recipient has no storage deposit', async () => { + jest + .spyOn(nearUtils, 'isReceiverReady') + .mockImplementation((_walletConnection, _tokenAddress, address) => + Promise.resolve(address !== paymentAddress), + ); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + account: { viewFunction: () => 'payer.testnet' }, + }), + } as any; + + await expect(async () => { + await payNearFungibleRequest(request, mockedNearWalletConnection, undefined, { + callbackUrl: 'https://some.callback.url', + meta: 'param', + }); + }).rejects.toThrowError( + 'The paymentAddress is not registered for the token fau.reqnetwork.testnet', + ); + expect(paymentSpy).not.toHaveBeenCalled(); + }); + + it('throws when trying to pay if the proxy has no storage deposit', async () => { + jest + .spyOn(nearUtils, 'isReceiverReady') + .mockImplementation((_walletConnection, _tokenAddress, address) => + Promise.resolve(address !== 'pay.reqnetwork.testnet'), + ); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + account: { viewFunction: () => 'payer.testnet' }, + }), + } as any; + + await expect(async () => { + await payNearFungibleRequest(request, mockedNearWalletConnection, undefined, { + callbackUrl: 'https://some.callback.url', + meta: 'param', + }); + }).rejects.toThrowError('The proxy is not registered for the token fau.reqnetwork.testnet'); + expect(paymentSpy).not.toHaveBeenCalled(); + }); + + it('throws when trying to pay another payment extension', async () => { + // A mock is used to bypass Near wallet connection for address validation and contract interaction + const paymentSpy = jest + .spyOn(nearUtils, 'processNearFungiblePayment') + .mockReturnValue(Promise.resolve()); + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + // state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + let invalidRequest = deepCopy(request); + invalidRequest = { + ...invalidRequest, + extensions: { + [ExtensionTypes.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY]: { + ...invalidRequest.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT], + }, + }, + }; + + await expect( + payNearFungibleRequest(invalidRequest, mockedNearWalletConnection, undefined, undefined), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + expect(paymentSpy).not.toHaveBeenCalled(); + }); + + it('throws when trying to pay with a native token', async () => { + const mockedNearWalletConnection = { + account: () => ({ + functionCall: () => true, + state: () => Promise.resolve({ amount: 100 }), + }), + } as any; + let invalidRequest = deepCopy(request); + invalidRequest = { + ...invalidRequest, + currencyInfo: { + type: RequestLogicTypes.CURRENCY.ETH, + value: 'NEAR', + network: 'aurora', + }, + }; + + await expect( + payNearFungibleRequest(invalidRequest, mockedNearWalletConnection), + ).rejects.toThrowError( + 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', + ); + expect(paymentSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/payment-processor/test/payment/index.test.ts b/packages/payment-processor/test/payment/index.test.ts index cd3578d96c..74d4150658 100644 --- a/packages/payment-processor/test/payment/index.test.ts +++ b/packages/payment-processor/test/payment/index.test.ts @@ -194,7 +194,7 @@ describe('payNearInputDataRequest', () => { { callbackUrl: 'https://some.callback.url', meta: 'param' }, ); }); - it('throws when tyring to pay another payment extension', async () => { + it('throws when trying to pay another payment extension', async () => { // A mock is used to bypass Near wallet connection for address validation and contract interaction const paymentSpy = jest .spyOn(nearUtils, 'processNearPayment') @@ -225,7 +225,7 @@ describe('payNearInputDataRequest', () => { await expect( payNearInputDataRequest(request, mockedNearWalletConnection, '1'), ).rejects.toThrowError('request cannot be processed, or is not an pn-native-token request'); - expect(paymentSpy).toHaveBeenCalledTimes(0); + expect(paymentSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts index 0c36174a87..6bf59dd683 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20FeeProxy/index.ts @@ -144,6 +144,10 @@ export const erc20FeeProxyArtifact = new ContractArtifact( address: 'pay.reqnetwork.testnet', creationBlockNumber: 120566834, }, + aurora: { + address: 'pay.reqnetwork.near', + creationBlockNumber: 89421541, + }, }, }, // Additional deployments of same versions, not worth upgrading the version number but worth using within next versions From 48bb03345c64dcaac79cd4547870e085ea5cbecd Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Wed, 19 Apr 2023 11:39:28 +0200 Subject: [PATCH 06/14] chore(payment-detection): export hashReference util (#1100) --- packages/payment-detection/src/index.ts | 2 ++ packages/payment-detection/src/utils.ts | 12 ++++++++---- .../test/erc20/thegraph-info-retriever.test.ts | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/payment-detection/src/index.ts b/packages/payment-detection/src/index.ts index 0e42895a7e..aea300fff9 100644 --- a/packages/payment-detection/src/index.ts +++ b/packages/payment-detection/src/index.ts @@ -16,6 +16,7 @@ import { formatAddress, getPaymentNetworkExtension, getPaymentReference, + hashReference, padAmountForChainlink, parseLogArgs, unpadAmountFromChainlink, @@ -59,5 +60,6 @@ export { calculateEscrowState, getPaymentNetworkExtension, getPaymentReference, + hashReference, formatAddress, }; diff --git a/packages/payment-detection/src/utils.ts b/packages/payment-detection/src/utils.ts index ff4c72ed6d..b1e444e228 100644 --- a/packages/payment-detection/src/utils.ts +++ b/packages/payment-detection/src/utils.ts @@ -103,10 +103,6 @@ export const makeGetDeploymentInformation = < }; }; -export const hashReference = (paymentReference: string): string => { - return keccak256(`0x${paymentReference}`); -}; - /** * Returns escrow status based on array of escrow events * @param escrowEvents Balance of the request being updated @@ -177,6 +173,14 @@ export function getPaymentReference( return PaymentReferenceCalculator.calculate(requestId, salt, info); } +/** + * Returns the hash of a payment reference. + * @see getPaymentReference + */ +export const hashReference = (paymentReference: string): string => { + return keccak256(`0x${paymentReference}`); +}; + /** * For EVMs: alias to ethers.utils.getAddress that adds the key to error message, and supports nullish values. * For other chains: applies lower-case to the address. 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 29943f2ba9..798bb84ed0 100644 --- a/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts +++ b/packages/payment-detection/test/erc20/thegraph-info-retriever.test.ts @@ -4,6 +4,7 @@ import PaymentReferenceCalculator from '../../src/payment-reference-calculator'; import { utils } from 'ethers'; import { PaymentTypes } from '@requestnetwork/types'; import { CurrencyManager } from '@requestnetwork/currency'; +import { hashReference } from '../../src'; const paymentsMockData = { ['0xc6e23a20c0a1933acc8e30247b5d1e2215796c1f' as string]: [ @@ -82,7 +83,7 @@ describe('api/erc20/thegraph-info-retriever', () => { paymentData.salt, paymentData.to, ); - const onChainReference = utils.keccak256(`0x${paymentReference}`); + const onChainReference = hashReference(paymentReference); expect(onChainReference).toEqual(paymentData.reference); const graphRetriever = new TheGraphInfoRetriever(clientMock, CurrencyManager.getDefault()); From 35a8e0b6198bb5ad0cfa9865a2fde13d5395c6ca Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 20 Apr 2023 11:40:06 +0200 Subject: [PATCH 07/14] feat: chain supported type guard (#1102) --- packages/currency/src/chains/ChainsAbstract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/currency/src/chains/ChainsAbstract.ts b/packages/currency/src/chains/ChainsAbstract.ts index 979a7e1e22..d498baa959 100644 --- a/packages/currency/src/chains/ChainsAbstract.ts +++ b/packages/currency/src/chains/ChainsAbstract.ts @@ -48,7 +48,7 @@ export abstract class ChainsAbstract< /** * Check if chainName lives amongst the list of supported chains by this chain type. */ - public isChainSupported(chainName?: string): boolean { + public isChainSupported(chainName?: string): chainName is CHAIN_NAME { return !!chainName && (this.chainNames as string[]).includes(chainName); } From af5c90292376a4c841d621e22b12c38d2c61efb6 Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Fri, 12 May 2023 13:50:48 +0200 Subject: [PATCH 08/14] fix(integration-tests): unskip tests (#1107) --- .../integration-test/test/node-client.test.ts | 57 +++++++++++++------ .../src/request/confirmedTransactionStore.ts | 2 +- .../src/request/persistTransaction.ts | 2 +- .../thegraph-data-access/src/data-access.ts | 4 +- packages/utils/src/retry.ts | 2 +- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/packages/integration-test/test/node-client.test.ts b/packages/integration-test/test/node-client.test.ts index a070ea72be..a4a6fc286a 100644 --- a/packages/integration-test/test/node-client.test.ts +++ b/packages/integration-test/test/node-client.test.ts @@ -30,7 +30,7 @@ const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); // eslint-disable-next-line no-magic-numbers -jest.setTimeout(10000); +jest.setTimeout(30000); const requestCreationHashBTC: Types.IRequestInfo = { currency: 'BTC', @@ -71,6 +71,29 @@ const wrongDecryptionProvider = new EthereumPrivateKeyDecryptionProvider({ method: Types.Encryption.METHOD.ECIES, }); +const waitForConfirmation = async (input: Request | Types.IRequestDataWithEvents) => { + // Create the promise to wait for confirmation. + const waitForConfirmationPromise = new Promise((resolve) => + input.on('confirmed', resolve), + ); + + // In parallel, mine an empty block, because a confirmation needs to wait for two blocks + // (the block that persisted the action + another block). + const mineBlockPromise = provider.send('evm_mine', []); + + // Set a time limit to wait for confirmation before throwing. + // Create the error object right away to conserve the context's stacktrace. + const timeoutError = new Error('waiting for confirmation took too long'); + const timeout = setTimeout(() => { + throw timeoutError; + }, 5000); + + // Return the confirmation result. + const promiseResults = await Promise.all([waitForConfirmationPromise, mineBlockPromise]); + clearTimeout(timeout); + return promiseResults[0]; +}; + describe('Request client using a request node', () => { it('can create a request, change the amount and get data', async () => { // Create a request @@ -89,7 +112,7 @@ describe('Request client using a request node', () => { expect(requestData.meta).toBeDefined(); expect(requestData.pending!.state).toBe(Types.RequestLogic.STATE.CREATED); - requestData = await request.waitForConfirmation(); + requestData = await waitForConfirmation(request); expect(requestData.state).toBe(Types.RequestLogic.STATE.CREATED); expect(requestData.pending).toBeNull(); @@ -101,7 +124,7 @@ describe('Request client using a request node', () => { expect(requestData.meta).toBeDefined(); expect(requestData.pending!.expectedAmount).toBe('800'); - requestData = await new Promise((resolve) => requestData.on('confirmed', resolve)); + requestData = await waitForConfirmation(requestData); expect(requestData.expectedAmount).toBe('800'); expect(requestData.pending).toBeNull(); }); @@ -145,7 +168,7 @@ describe('Request client using a request node', () => { expect(extension.events[0].name).toBe('create'); expect(extension.events[0].parameters).toEqual(paymentNetwork.parameters); - requestData = await request.waitForConfirmation(); + requestData = await waitForConfirmation(request); expect(requestData.state).toBe(Types.RequestLogic.STATE.CREATED); expect(requestData.pending).toBeNull(); @@ -153,7 +176,7 @@ describe('Request client using a request node', () => { expect(requestData.balance).toBeDefined(); expect(requestData.balance!.balance).toBe('0'); - requestData = await new Promise((resolve) => requestData.on('confirmed', resolve)); + requestData = await waitForConfirmation(requestData); expect(requestData.balance!.balance).toBe('0'); requestData = await request.declareReceivedPayment( @@ -164,7 +187,7 @@ describe('Request client using a request node', () => { expect(requestData.balance).toBeDefined(); expect(requestData.balance!.balance).toBe('0'); - requestData = await new Promise((resolve) => requestData.on('confirmed', resolve)); + requestData = await waitForConfirmation(requestData); expect(requestData.balance!.balance).toBe('100'); }); @@ -187,7 +210,7 @@ describe('Request client using a request node', () => { signer: payeeIdentity, topics: topicsRequest1and2, }); - await request1.waitForConfirmation(); + await waitForConfirmation(request1); const timestampBeforeReduce = getCurrentTimestampInSecond(); // make sure that request 2 timestamp is greater than request 1 timestamp @@ -209,15 +232,15 @@ describe('Request client using a request node', () => { topics: topicsRequest1and2, }); - await request2.waitForConfirmation(); + await waitForConfirmation(request2); // reduce request 1 const requestDataReduce = await request1.reduceExpectedAmountRequest('10000000', payeeIdentity); - await new Promise((r) => requestDataReduce.on('confirmed', r)); + await waitForConfirmation(requestDataReduce); // cancel request 1 const requestDataCancel = await request1.cancel(payeeIdentity); - await new Promise((r) => requestDataCancel.on('confirmed', r)); + await waitForConfirmation(requestDataCancel); // get requests without boundaries let requests = await requestNetwork.fromTopic(topicsRequest1and2[0]); @@ -375,7 +398,7 @@ describe('Request client using a request node', () => { expect(requestData.pending!.state).toBe(Types.RequestLogic.STATE.CREATED); expect(requestData.meta!.transactionManagerMeta.encryptionMethod).toBe('ecies-aes256-gcm'); - await new Promise((resolve) => request.on('confirmed', resolve)); + await waitForConfirmation(request); // Fetch the created request by its id const fetchedRequest = await requestNetwork.fromRequestId(request.requestId); @@ -395,7 +418,7 @@ describe('Request client using a request node', () => { expect(fetchedRequestData.state).toBe(Types.RequestLogic.STATE.CREATED); const acceptData = await request.accept(payerIdentity); - await new Promise((resolve) => acceptData.on('confirmed', resolve)); + await waitForConfirmation(acceptData); await fetchedRequest.refresh(); fetchedRequestData = fetchedRequest.getData(); @@ -405,7 +428,7 @@ describe('Request client using a request node', () => { requestCreationHashBTC.expectedAmount, payerIdentity, ); - await new Promise((resolve) => increaseData.on('confirmed', resolve)); + await waitForConfirmation(increaseData); await fetchedRequest.refresh(); expect(fetchedRequest.getData().expectedAmount).toEqual( @@ -416,7 +439,7 @@ describe('Request client using a request node', () => { Number(requestCreationHashBTC.expectedAmount) * 2, payeeIdentity, ); - await new Promise((resolve) => reduceData.on('confirmed', resolve)); + await waitForConfirmation(reduceData); await fetchedRequest.refresh(); expect(fetchedRequest.getData().expectedAmount).toBe('0'); @@ -442,7 +465,7 @@ describe('Request client using a request node', () => { }, [encryptionData.encryptionParams], ); - await encryptedRequest.waitForConfirmation(); + await waitForConfirmation(encryptedRequest); // Create a plain request const plainRequest = await requestNetwork.createRequest({ @@ -533,12 +556,12 @@ describe('ERC20 localhost request creation and detection test', () => { expect(requestData.meta).toBeDefined(); expect(requestData.pending!.state).toBe(Types.RequestLogic.STATE.CREATED); - requestData = await new Promise((resolve) => request.on('confirmed', resolve)); + requestData = await waitForConfirmation(request); expect(requestData.state).toBe(Types.RequestLogic.STATE.CREATED); expect(requestData.pending).toBeNull(); }); - it.only('can create ERC20 requests with any to erc20 proxy', async () => { + it('can create ERC20 requests with any to erc20 proxy', async () => { const tokenContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'; const currencies: CurrencyInput[] = [ diff --git a/packages/request-node/src/request/confirmedTransactionStore.ts b/packages/request-node/src/request/confirmedTransactionStore.ts index 6e65475e18..b48332f915 100644 --- a/packages/request-node/src/request/confirmedTransactionStore.ts +++ b/packages/request-node/src/request/confirmedTransactionStore.ts @@ -3,7 +3,7 @@ import Keyv, { Store } from 'keyv'; /** * Class for storing confirmed transactions information - * When 'confirmed' event is receive from a 'persistTransaction', the event data are stored. + * When 'confirmed' event is received from a 'persistTransaction', the event data are stored. * The client can call the getConfirmed entry point, to get the confirmed event. */ export default class ConfirmedTransactionStore { diff --git a/packages/request-node/src/request/persistTransaction.ts b/packages/request-node/src/request/persistTransaction.ts index 768e72244e..3cb2b63d27 100644 --- a/packages/request-node/src/request/persistTransaction.ts +++ b/packages/request-node/src/request/persistTransaction.ts @@ -71,7 +71,7 @@ export default class PersistTransactionHandler { clientRequest.body.topics, ); - // when the transaction is confirmed, store the information to be serve when requested + // when the transaction is confirmed, store the information to be served when requested dataAccessResponse.on('confirmed', async (dataAccessConfirmedResponse) => { await this.confirmedTransactionStore.addConfirmedTransaction( transactionHash.value, diff --git a/packages/thegraph-data-access/src/data-access.ts b/packages/thegraph-data-access/src/data-access.ts index 7c2faf393a..19bf9333ad 100644 --- a/packages/thegraph-data-access/src/data-access.ts +++ b/packages/thegraph-data-access/src/data-access.ts @@ -4,12 +4,11 @@ import TypedEmitter from 'typed-emitter'; import { BigNumber } from 'ethers'; import { getCurrentTimestampInSecond, retry, SimpleLogger } from '@requestnetwork/utils'; -import { Block } from '@requestnetwork/data-access'; +import { Block, CombinedDataAccess } from '@requestnetwork/data-access'; import { DataAccessTypes, LogTypes, StorageTypes } from '@requestnetwork/types'; import { Transaction } from './queries'; import { SubgraphClient } from './subgraph-client'; -import { CombinedDataAccess } from '@requestnetwork/data-access'; import { PendingStore } from './pending-store'; import { RequestInit } from 'graphql-request/dist/types.dom'; @@ -270,6 +269,7 @@ export class TheGraphDataWrite implements DataAccessTypes.IDataWrite { }; storageResult.on('confirmed', () => { + this.logger.debug(`Looking for ${storageResult.id} in subgraph`); retry( async () => { const response = await this.graphql.getTransactionsByHash(storageResult.id); diff --git a/packages/utils/src/retry.ts b/packages/utils/src/retry.ts index 141cacdb92..3521defa82 100644 --- a/packages/utils/src/retry.ts +++ b/packages/utils/src/retry.ts @@ -37,7 +37,7 @@ const retry = ( maxExponentialBackoffDelay?: number; } = {}, ): ((...params: TParams) => Promise) => { - // If a context was passed in, bind it to to the target function + // If a context was passed in, bind it to the target function if (context) { target = target.bind(context); } From acce3ce1413b1310f3c4a093e351d972eeb8955e Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Tue, 16 May 2023 14:31:18 +0200 Subject: [PATCH 09/14] chore: auto-assign dependabot alerts (#1105) Co-authored-by: Alexandre ABRIOUX --- .github/workflows/auto_assign_pr.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto_assign_pr.yml b/.github/workflows/auto_assign_pr.yml index f602f7f05d..7dc5326ec4 100644 --- a/.github/workflows/auto_assign_pr.yml +++ b/.github/workflows/auto_assign_pr.yml @@ -1,7 +1,12 @@ name: 'Auto Assign' on: - pull_request: + pull_request_target: types: [opened, ready_for_review] + branches: [master] + +permissions: + contents: read + pull-requests: write jobs: add-reviews: From 54e61d9c9295e776b73a03698985cb654ebff626 Mon Sep 17 00:00:00 2001 From: Darko Kolev Date: Tue, 16 May 2023 17:42:59 +0200 Subject: [PATCH 10/14] chore: fixing outdated request-toolbox README (#1099) --- packages/toolbox/README.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/toolbox/README.md b/packages/toolbox/README.md index 072f779b07..0ea1ce0dd9 100644 --- a/packages/toolbox/README.md +++ b/packages/toolbox/README.md @@ -31,6 +31,28 @@ yarn request-toolbox request create yarn request-toolbox request create 12 ``` +or if ran from the `/toolbox` folder + +```bash +yarn cli request create +yarn cli request create 12 +``` + +#### CLI Troubleshooting + +If you receive the following error + +```bash +error Command "request-toolbox" not found. +``` + +then build the toolbox package like bellow: + +```bash +cd packages/toolbox +yarn --check-files +``` + ### Conversion paths #### Adding & removing aggregators @@ -49,9 +71,9 @@ It will suggest pairs of currencies: The following commands are also available: -- `yarn cli addAggregator` can be used if you have all information about an aggregator you want to add -- `yarn cli removeAggregator` will set the given currency pair to the 0x00[...]00 address. -- `yarn cli listMissingAggregators ` (where `name` is a valid Request Finance currency list, [https://api.request.network/currency/list/name]() should be valid) will display missing aggregators for that list on all networks. +- `yarn request-toolbox addAggregator` can be used if you have all information about an aggregator you want to add +- `yarn request-toolbox removeAggregator` will set the given currency pair to the 0x00[...]00 address. +- `yarn request-toolbox listMissingAggregators ` (where `name` is a valid Request Finance currency list, [https://api.request.network/currency/list/name]() should be valid) will display missing aggregators for that list on all networks. Use `--help` for details about each command. From 194ac3260bf970b26aaa4e4825c8e255cf9dd931 Mon Sep 17 00:00:00 2001 From: Alexandre ABRIOUX Date: Wed, 24 May 2023 10:27:48 +0200 Subject: [PATCH 11/14] chore(ethereum-storage-ethers): return nonce in append meta (#1109) --- packages/ethereum-storage/src/ethereum-storage-ethers.ts | 3 ++- packages/types/src/storage-types.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ethereum-storage/src/ethereum-storage-ethers.ts b/packages/ethereum-storage/src/ethereum-storage-ethers.ts index f3dd381976..7c1ef5e9ee 100644 --- a/packages/ethereum-storage/src/ethereum-storage-ethers.ts +++ b/packages/ethereum-storage/src/ethereum-storage-ethers.ts @@ -69,13 +69,14 @@ export class EthereumStorageEthers implements StorageTypes.IStorageWrite { ipfs: { size: ipfsSize }, local: { location: ipfsHash }, ethereum: { + nonce: tx.nonce, + transactionHash: tx.hash, blockConfirmation: tx.confirmations, blockNumber: Number(tx.blockNumber), // wrong value, but this metadata will not be used, as it's in Pending state blockTimestamp: -1, networkName: this.network, smartContractAddress: this.txSubmitter.hashSubmitterAddress, - transactionHash: tx.hash, }, state: StorageTypes.ContentState.PENDING, storageType: StorageTypes.StorageSystemType.LOCAL, diff --git a/packages/types/src/storage-types.ts b/packages/types/src/storage-types.ts index e60405e840..cc3c8ffe18 100644 --- a/packages/types/src/storage-types.ts +++ b/packages/types/src/storage-types.ts @@ -136,6 +136,8 @@ export interface IEthereumMetadata { fee?: string; /** gas fee in wei of the transaction that stored the data id */ gasFee?: string; + /** nonce of the transaction that stored the data id */ + nonce?: number; } /** Ethereum network id */ From ce1b54c5a0ff43d5ef3eacd561dc83e0e5134c13 Mon Sep 17 00:00:00 2001 From: leoslr <50319677+leoslr@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:25:42 +0200 Subject: [PATCH 12/14] chore: improve contract administration (#1103) Co-authored-by: Alexandre ABRIOUX --- packages/smart-contracts/README.md | 20 +++++ packages/smart-contracts/hardhat.config.ts | 9 ++ .../scripts-create2/constructor-args.ts | 5 -- .../contract-setup/adminTasks.ts | 9 +- .../setupBatchConversionPayments.ts | 29 ++++--- .../setupChainlinkConversionPath.ts | 26 ++++-- .../setupERC20SwapToConversion.ts | 28 +++--- .../contract-setup/setupERC20SwapToPay.ts | 28 +++--- .../contract-setup/setupETHConversionProxy.ts | 26 +++--- .../setupErc20ConversionProxy.ts | 27 +++--- .../scripts-create2/contract-setup/setups.ts | 42 ++++++--- .../smart-contracts/scripts-create2/deploy.ts | 85 ++++--------------- .../scripts-create2/update-contracts-setup.ts | 18 ++++ .../smart-contracts/scripts-create2/utils.ts | 10 +-- 14 files changed, 210 insertions(+), 152 deletions(-) create mode 100644 packages/smart-contracts/scripts-create2/update-contracts-setup.ts diff --git a/packages/smart-contracts/README.md b/packages/smart-contracts/README.md index ef7ca47ebb..f60f83cd2d 100644 --- a/packages/smart-contracts/README.md +++ b/packages/smart-contracts/README.md @@ -206,6 +206,26 @@ yarn hardhat deploy-live-payments --network private --force yarn hardhat deploy-live-payments --network private --force --dry-run ``` +## Administrate the contracts + +The contracts to be updated are listed in the array `create2ContractDeploymentList` in [Utils](scripts-create2/utils.ts). +Modify the content of the array depending on your need when you perform an administration task. +Environment variables needed: `ADMIN_PRIVATE_KEY` + +To update the contracts on one network, use: + +```bash +NETWORK= yarn hardhat update-contracts +``` + +If you want to update the contracts on all networks: + +```bash +yarn hardhat update-contracts +``` + +This command will output details about each update on each chain + ## Tests After a local deployment: diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 873cc227b4..6df0f1446d 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -15,6 +15,7 @@ import { deployWithCreate2FromList } from './scripts-create2/deploy'; import { NUMBER_ERRORS } from './scripts/utils'; import { networkRpcs } from '@requestnetwork/utils'; import { tenderlyImportAll } from './scripts-create2/tenderly'; +import { updateContractsFromList } from './scripts-create2/update-contracts-setup'; config(); @@ -271,6 +272,14 @@ task( await deployWithCreate2FromList(hre as HardhatRuntimeEnvironmentExtended); }); +task( + 'update-contracts', + 'Update the latest deployed contracts from the Create2DeploymentList', +).setAction(async (_args, hre) => { + await hre.run(DEPLOYER_KEY_GUARD); + await updateContractsFromList(hre as HardhatRuntimeEnvironmentExtended); +}); + task( 'verify-contract-from-deployer', 'Verify the contracts from the Create2DeploymentList for a specific network', diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index 77a7a08498..042da3569c 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -48,11 +48,6 @@ export const getConstructorArgs = ( return [erc20FeeProxyAddress, getAdminWalletAddress(contract)]; } case 'BatchConversionPayments': { - if (!network) { - throw new Error( - 'Batch conversion contract requires network parameter to get correct address of erc20FeeProxy, erc20ConversionFeeProxy, ethereumFeeProxy, ethereumConversionFeeProxy, and chainlinkConversionPath', - ); - } return [ '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 6f94045aa7..cc74a3682a 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -12,10 +12,11 @@ import { } from '@requestnetwork/utils'; import { CurrencyTypes } from '@requestnetwork/types'; -// Fees: 0.5% -export const REQUEST_SWAP_FEES = 5; -// Batch Fees: .3% -export const BATCH_FEE = BigNumber.from(30); +// Swap Fees: set to 5 for 0.5% +const REQUEST_SWAP_FEES = 0; +// Batch Fees: set to 30 for 0.3% +const BATCH_FEE = BigNumber.from(0); + // Batch fee amount in USD Limit: 150 * 1e8 ($150) const BATCH_FEE_AMOUNT_USD_LIMIT = parseUnits('150', 8); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index 06ffdad522..536b9d55b2 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -12,24 +12,33 @@ import { CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types'; /** * Updates the values of the batch fees of the BatchConversionPayments contract, if needed. - * @param contractAddress address of the BatchConversionPayments proxy. + * @param contractAddress address of the BatchConversionPayments contract. + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment. */ -export const setupBatchConversionPayments = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { +export const setupBatchConversionPayments = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { // Setup contract parameters - const batchConversionPaymentContract = new hre.ethers.Contract( - contractAddress, - batchConversionPaymentsArtifact.getContractAbi(), - ); + // constants related to chainlink and conversion rate const currencyManager = CurrencyManager.getDefault(); const setUpActions = async (network: CurrencyTypes.EvmChainName) => { console.log(`Setup BatchConversionPayments on ${network}`); + if (!contractAddress) { + contractAddress = batchConversionPaymentsArtifact.getAddress(network); + } + const batchConversionPaymentContract = new hre.ethers.Contract( + contractAddress, + batchConversionPaymentsArtifact.getContractAbi(), + ); + const NativeAddress = currencyManager.getNativeCurrency( RequestLogicTypes.CURRENCY.ETH, network, @@ -80,7 +89,7 @@ export const setupBatchConversionPayments = async ( for (const network of hre.config.xdeploy.networks) { try { EvmChains.assertChainSupported(network); - await Promise.resolve(setUpActions(network)); + await setUpActions(network); } catch (err) { console.warn(`An error occurred during the setup of BatchConversion on ${network}`); console.warn(err); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupChainlinkConversionPath.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupChainlinkConversionPath.ts index 5e9d70561c..b1c37075d3 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupChainlinkConversionPath.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupChainlinkConversionPath.ts @@ -1,4 +1,4 @@ -import { CurrencyManager } from '@requestnetwork/currency'; +import { CurrencyManager, EvmChains } from '@requestnetwork/currency'; import { RequestLogicTypes } from '@requestnetwork/types'; import { chainlinkConversionPath } from '../../src/lib'; import { HardhatRuntimeEnvironmentExtended } from '../types'; @@ -7,20 +7,28 @@ import { getSignerAndGasFees, updateNativeTokenHash } from './adminTasks'; /** * Setup the chainlinkConversionPath values once deployed * @param contractAddress address of the ChainlinkConversionPath contract + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment */ -export const setupChainlinkConversionPath = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { +export const setupChainlinkConversionPath = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { // Setup contract parameters - const ChainlinkConversionPathContract = new hre.ethers.Contract( - contractAddress, - chainlinkConversionPath.getContractAbi(), - ); await Promise.all( hre.config.xdeploy.networks.map(async (network) => { try { + EvmChains.assertChainSupported(network); + if (!contractAddress) { + contractAddress = chainlinkConversionPath.getAddress(network); + } + const ChainlinkConversionPathContract = new hre.ethers.Contract( + contractAddress, + chainlinkConversionPath.getContractAbi(), + ); const { signer, txOverrides } = await getSignerAndGasFees(network, hre); const nativeTokenHash = CurrencyManager.getDefault().getNativeCurrency( RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToConversion.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToConversion.ts index 41763e16f8..7606cd410a 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToConversion.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToConversion.ts @@ -9,23 +9,29 @@ import { import { EvmChains } from '@requestnetwork/currency'; /** - * Updates the values of the chainlinkConversionPath and swap router of the ERC20SwapToConversion contract, if needed - * @param contractAddress address of the ERC20SwapToConversion Proxy + * Updates the values of the chainlinkConversionPath and swap router of the ERC20SwapToConversion contract + * @param contractAddress address of the ERC20SwapToConversion contract + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment */ -export const setupERC20SwapToConversion = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const ERC20SwapToConversionContract = new hre.ethers.Contract( - contractAddress, - erc20SwapConversionArtifact.getContractAbi(), - ); +export const setupERC20SwapToConversion = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { await Promise.all( hre.config.xdeploy.networks.map(async (network) => { try { EvmChains.assertChainSupported(network); + if (!contractAddress) { + contractAddress = erc20SwapConversionArtifact.getAddress(network); + } + const ERC20SwapToConversionContract = new hre.ethers.Contract( + contractAddress, + erc20SwapConversionArtifact.getContractAbi(), + ); const { signer, txOverrides } = await getSignerAndGasFees(network, hre); const ERC20SwapToConversionConnected = await ERC20SwapToConversionContract.connect(signer); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToPay.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToPay.ts index e21009c0a5..df7d7f89f4 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToPay.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupERC20SwapToPay.ts @@ -1,24 +1,32 @@ +import { EvmChains } from '@requestnetwork/currency'; import { erc20SwapToPayArtifact } from '../../src/lib'; import { HardhatRuntimeEnvironmentExtended } from '../types'; import { getSignerAndGasFees, updateRequestSwapFees, updateSwapRouter } from './adminTasks'; /** * Once deployed, setup the values of the ERC20SwapToPay contract - * @param contractAddress address of the ERC20SwapToPay Proxy + * @param contractAddress address of the ERC20SwapToPay contract + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment */ -export const setupERC20SwapToPay = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const ERC20SwapToPayContract = new hre.ethers.Contract( - contractAddress, - erc20SwapToPayArtifact.getContractAbi(), - ); +export const setupERC20SwapToPay = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { await Promise.all( hre.config.xdeploy.networks.map(async (network) => { try { + EvmChains.assertChainSupported(network); + if (!contractAddress) { + contractAddress = erc20SwapToPayArtifact.getAddress(network); + } + const ERC20SwapToPayContract = new hre.ethers.Contract( + contractAddress, + erc20SwapToPayArtifact.getContractAbi(), + ); const { signer, txOverrides } = await getSignerAndGasFees(network, hre); const ERC20SwapToPayConnected = await ERC20SwapToPayContract.connect(signer); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts index 8a97f44050..8dedd28156 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupETHConversionProxy.ts @@ -11,22 +11,28 @@ import { /** * Updates the values of the chainlinkConversionPath and EthFeeProxy addresses if needed - * @param contractAddress address of the ETHConversion Proxy + * @param contractAddress address of the ETHConversion contract + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment */ -export const setupETHConversionProxy = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const EthConversionProxyContract = new hre.ethers.Contract( - contractAddress, - ethConversionArtifact.getContractAbi(), - ); +export const setupETHConversionProxy = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { await Promise.all( hre.config.xdeploy.networks.map(async (network) => { try { EvmChains.assertChainSupported(network); + if (!contractAddress) { + contractAddress = ethConversionArtifact.getAddress(network); + } + const EthConversionProxyContract = new hre.ethers.Contract( + contractAddress, + ethConversionArtifact.getContractAbi(), + ); const { signer, txOverrides } = await getSignerAndGasFees(network, hre); const nativeTokenHash = CurrencyManager.getDefault().getNativeCurrency( RequestLogicTypes.CURRENCY.ETH, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupErc20ConversionProxy.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupErc20ConversionProxy.ts index e668949390..349d081cf6 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupErc20ConversionProxy.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupErc20ConversionProxy.ts @@ -11,22 +11,29 @@ const ERC20ConversionVersion = '0.1.2'; /** * Updates the values of the chainlinkConversionPath and ERC20FeeProxy addresses if needed - * @param contractAddress address of the ERC20Conversion Proxy + * @param contractAddress address of the ERC20Conversion contract. + * If not provided fallback to the latest deployment address * @param hre Hardhat runtime environment */ -export const setupErc20ConversionProxy = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, -): Promise => { - // Setup contract parameters - const Erc20ConversionProxyContract = new hre.ethers.Contract( - contractAddress, - erc20ConversionProxy.getContractAbi(ERC20ConversionVersion), - ); +export const setupErc20ConversionProxy = async ({ + contractAddress, + hre, +}: { + contractAddress?: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { await Promise.all( hre.config.xdeploy.networks.map(async (network) => { try { EvmChains.assertChainSupported(network); + if (!contractAddress) { + contractAddress = erc20ConversionProxy.getAddress(network); + } + const Erc20ConversionProxyContract = new hre.ethers.Contract( + contractAddress, + erc20ConversionProxy.getContractAbi(ERC20ConversionVersion), + ); + const { signer, txOverrides } = await getSignerAndGasFees(network, hre); const Erc20ConversionProxyConnected = Erc20ConversionProxyContract.connect(signer); await updatePaymentFeeProxyAddress( diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index 4e7749772e..a5fc18069b 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -2,33 +2,53 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; import { setupETHConversionProxy } from './setupETHConversionProxy'; import { setupBatchConversionPayments } from './setupBatchConversionPayments'; import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; +import { setupERC20SwapToPay } from './setupERC20SwapToPay'; +import { setupChainlinkConversionPath } from './setupChainlinkConversionPath'; +import { setupErc20ConversionProxy } from './setupErc20ConversionProxy'; /** - * Updates the values of either BatchConversionPayments, ETHConversionProxy, or ERC20SwapToConversion contract, if needed + * Administrate the specified contract at the specified address + * If the address is not provided fallback to the contract latest deployment address * @param contractAddress address of the proxy * @param hre Hardhat runtime environment * @param contractName name of the contract */ -export const setupContract = async ( - contractAddress: string, - hre: HardhatRuntimeEnvironmentExtended, - contractName: string, -): Promise => { +export const setupContract = async ({ + contractAddress, + contractName, + hre, +}: { + contractAddress?: string; + contractName: string; + hre: HardhatRuntimeEnvironmentExtended; +}): Promise => { switch (contractName) { - case 'ETHConversionProxy': { - await setupETHConversionProxy(contractAddress, hre); + case 'ChainlinkConversionPath': { + await setupChainlinkConversionPath({ contractAddress, hre }); + break; + } + case 'EthConversionProxy': { + await setupETHConversionProxy({ contractAddress, hre }); + break; + } + case 'Erc20ConversionProxy': { + await setupErc20ConversionProxy({ contractAddress, hre }); + break; + } + case 'ERC20SwapToPay': { + await setupERC20SwapToPay({ contractAddress, hre }); break; } case 'ERC20SwapToConversion': { - await setupERC20SwapToConversion(contractAddress, hre); + await setupERC20SwapToConversion({ contractAddress, hre }); break; } case 'BatchConversionPayments': { - await setupBatchConversionPayments(contractAddress, hre); + await setupBatchConversionPayments({ contractAddress, hre }); break; } default: { - console.log('Contract name not found'); + console.log(`No setup to perform for contract ${contractName}`); break; } } diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index 117d0ff1ff..b7a0c3754d 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -2,17 +2,16 @@ import { create2ContractDeploymentList, isContractDeployed } from './utils'; import { HardhatRuntimeEnvironmentExtended, IDeploymentParams } from './types'; import { xdeploy } from './xdeployer'; import { getConstructorArgs } from './constructor-args'; -import { - setupBatchConversionPayments, - setupChainlinkConversionPath, - setupErc20ConversionProxy, - setupERC20SwapToConversion, - setupERC20SwapToPay, - setupETHConversionProxy, -} from './contract-setup'; import { EvmChains } from '@requestnetwork/currency'; +import { setupContract } from './contract-setup/setups'; -// Deploys, set up the contracts and returns the address +/** + * Deploy a contract on the networks specified in the hardhat config. + * Use the CREATE2 scheme for the deployments. + * @param deploymentParams contract and constructor arguments + * @param hre hardhat runtime environment + * @returns The address of the deployed contract - same for all network + */ export const deployOneWithCreate2 = async ( deploymentParams: IDeploymentParams, hre: HardhatRuntimeEnvironmentExtended, @@ -46,67 +45,19 @@ export const deployOneWithCreate2 = async ( return deploymentResult[0].address; }; +/** + * Deploy all the contracts specified in create2ContractDeploymentList. + * Once deployed, do the setup. + * @param hre + */ export const deployWithCreate2FromList = async ( hre: HardhatRuntimeEnvironmentExtended, ): Promise => { for (const contract of create2ContractDeploymentList) { - switch (contract) { - case 'EthereumProxy': - case 'ERC20FeeProxy': - case 'EthereumFeeProxy': { - const constructorArgs = getConstructorArgs(contract); - await deployOneWithCreate2({ contract, constructorArgs }, hre); - break; - } - case 'ChainlinkConversionPath': { - const constructorArgs = getConstructorArgs(contract); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupChainlinkConversionPath(address, hre); - break; - } - case 'EthConversionProxy': { - const constructorArgs = getConstructorArgs(contract); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupETHConversionProxy(address, hre); - break; - } - case 'Erc20ConversionProxy': { - const constructorArgs = getConstructorArgs(contract); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupErc20ConversionProxy(address, hre); - break; - } - case 'ERC20SwapToPay': { - const constructorArgs = getConstructorArgs(contract); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupERC20SwapToPay(address, hre); - break; - } - case 'ERC20SwapToConversion': { - const constructorArgs = getConstructorArgs(contract); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupERC20SwapToConversion(address, hre); - break; - } - case 'ERC20EscrowToPay': - case 'ERC20TransferableReceivable': { - const network = hre.config.xdeploy.networks[0]; - EvmChains.assertChainSupported(network); - const constructorArgs = getConstructorArgs(contract, network); - await deployOneWithCreate2({ contract, constructorArgs }, hre); - break; - } - case 'BatchConversionPayments': { - const network = hre.config.xdeploy.networks[0]; - EvmChains.assertChainSupported(network); - const constructorArgs = getConstructorArgs(contract, network); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupBatchConversionPayments(address, hre); - break; - } - // Other cases to add when necessary - default: - throw new Error(`The contract ${contract} is not to be deployed using the CREATE2 scheme`); - } + const network = hre.config.xdeploy.networks[0]; + EvmChains.assertChainSupported(network); + const constructorArgs = getConstructorArgs(contract, network); + const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); + await setupContract({ contractAddress: address, contractName: contract, hre }); } }; diff --git a/packages/smart-contracts/scripts-create2/update-contracts-setup.ts b/packages/smart-contracts/scripts-create2/update-contracts-setup.ts new file mode 100644 index 0000000000..3ba579d198 --- /dev/null +++ b/packages/smart-contracts/scripts-create2/update-contracts-setup.ts @@ -0,0 +1,18 @@ +import { create2ContractDeploymentList } from './utils'; +import { HardhatRuntimeEnvironmentExtended } from './types'; +import { setupContract } from './contract-setup/setups'; + +/** + * Update the contract latest version registered in the artifacts. + * @param hre Hardhat runtime environment + */ +export const updateContractsFromList = async ( + hre: HardhatRuntimeEnvironmentExtended, +): Promise => { + for (const contract of create2ContractDeploymentList) { + await setupContract({ + contractName: contract, + hre, + }); + } +}; diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index 3ed10e6d91..9cac358961 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -8,16 +8,16 @@ import { EvmChains } from '@requestnetwork/currency'; * If you want to skip deploying one or more, then comment them out in the list bellow. */ export const create2ContractDeploymentList = [ - /* 'ChainlinkConversionPath', + /* 'ChainlinkConversionPath', 'EthereumProxy', 'EthereumFeeProxy', 'EthConversionProxy', 'ERC20FeeProxy', */ 'ERC20SwapToPay', - /* 'ERC20SwapToConversion', - 'ERC20EscrowToPay', - 'BatchConversionPayments', */ - 'ERC20TransferableReceivable', + 'ERC20SwapToConversion', + 'BatchConversionPayments', + /* 'ERC20EscrowToPay', + 'ERC20TransferableReceivable', */ ]; /** From 22888ae0a1c78b27ce0942884754c47a1ca49433 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:31:40 +0200 Subject: [PATCH 13/14] fix: include the feeAmount into the FT transfer (#1110) --- packages/payment-processor/src/payment/utils-near.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-processor/src/payment/utils-near.ts b/packages/payment-processor/src/payment/utils-near.ts index 8efa0a2d0b..aa5da329e0 100644 --- a/packages/payment-processor/src/payment/utils-near.ts +++ b/packages/payment-processor/src/payment/utils-near.ts @@ -186,7 +186,7 @@ export const processNearFungiblePayment = async ( await fungibleContract.ft_transfer_call({ args: { receiver_id: proxyAddress, - amount, + amount: BigNumber.from(amount).add(feeAmount).toString(), msg: JSON.stringify({ fee_address: feeAddress, fee_amount: feeAmount, From f36c28d96d79792e550059ed2877972bb5adde4f Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Fri, 2 Jun 2023 09:43:44 -0400 Subject: [PATCH 14/14] Remove requestId from contract --- .../erc20/transferable-receivable.ts | 2 +- .../src/erc20/transferable-receivable.ts | 1 + packages/payment-processor/package.json | 1 - .../payment/erc20-transferable-receivable.ts | 5 +- .../erc20-transferable-receivable.test.ts | 10 +- ...st-deploy-erc20-transferable-receivable.ts | 1 + .../contracts/ERC20TransferableReceivable.sol | 24 +-- .../{0.1.1.json => 0.2.0.json} | 104 +----------- .../ERC20TransferableReceivable/index.ts | 12 +- .../ERC20TransferableReceivable.test.ts | 156 +++--------------- 10 files changed, 48 insertions(+), 268 deletions(-) rename packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/{0.1.1.json => 0.2.0.json} (85%) 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 ac882f5975..50b7171b1b 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,7 +1,7 @@ import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types'; import { FeeReferenceBasedPaymentNetwork } from '../fee-reference-based'; -const CURRENT_VERSION = '0.1.0'; +const CURRENT_VERSION = '0.2.0'; /** * Implementation of the payment network to pay in ERC20 based on a transferable receivable contract. diff --git a/packages/payment-detection/src/erc20/transferable-receivable.ts b/packages/payment-detection/src/erc20/transferable-receivable.ts index ce5dbeaa2c..7ee8243752 100644 --- a/packages/payment-detection/src/erc20/transferable-receivable.ts +++ b/packages/payment-detection/src/erc20/transferable-receivable.ts @@ -14,6 +14,7 @@ import ProxyERC20InfoRetriever from './proxy-info-retriever'; const ERC20_TRANSFERABLE_RECEIVABLE_CONTRACT_ADDRESS_MAP = { ['0.1.0']: '0.1.0', + ['0.2.0']: '0.2.0', }; /** diff --git a/packages/payment-processor/package.json b/packages/payment-processor/package.json index a0a0146e87..ce6c03f5f6 100644 --- a/packages/payment-processor/package.json +++ b/packages/payment-processor/package.json @@ -43,7 +43,6 @@ "@requestnetwork/currency": "0.9.0", "@requestnetwork/payment-detection": "0.36.0", "@requestnetwork/smart-contracts": "0.29.0", - "@requestnetwork/multi-format": "0.15.10", "@requestnetwork/types": "0.36.0", "@requestnetwork/utils": "0.36.0", "@superfluid-finance/sdk-core": "0.5.0", diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index 3f8bb563dc..15650fcd0b 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -14,7 +14,6 @@ import { } from '@requestnetwork/payment-detection'; import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; import { ClientTypes } from '@requestnetwork/types'; -import MultiFormat from '@requestnetwork/multi-format'; import { ITransactionOverrides } from './transaction-overrides'; import { @@ -116,7 +115,6 @@ export function encodeMintErc20TransferableReceivableRequest( validateERC20TransferableReceivable(request); const tokenAddress = request.currencyInfo.value; - const requestIdDeserialized = MultiFormat.deserialize(request.requestId).value; const { paymentReference, paymentAddress } = getRequestPaymentValues(request); const amount = getAmountToPay(request); @@ -127,7 +125,6 @@ export function encodeMintErc20TransferableReceivableRequest( `0x${paymentReference}`, amount, tokenAddress, - requestIdDeserialized, ]); } @@ -205,7 +202,7 @@ export async function encodePayErc20TransferableReceivableRequest( const receivableContract = ERC20TransferableReceivable__factory.createInterface(); - // get tokenId from requestId + // get tokenId from request const receivableTokenId = await getReceivableTokenIdForRequest(request, signerOrProvider); return receivableContract.encodeFunctionData('payOwner', [ 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 9de7774faf..9a063b1127 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -25,7 +25,7 @@ import { getProxyAddress } from '../../src/payment/utils'; const erc20ContractAddress = '0x9FBDa871d559710256a2502A2517b794B482Db40'; const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'; -const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; +const feeAddress = '0x75c35C980C0d37ef46DF04d31A140b65503c0eEd'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const payeeWallet = Wallet.createRandom().connect(provider); const thirdPartyWallet = Wallet.createRandom().connect(provider); @@ -61,7 +61,7 @@ const validRequest: ClientTypes.IRequestData = { paymentAddress, salt: '0ee84db293a752c6', }, - version: '0.1.0', + version: '0.2.0', }, }, payee: { @@ -193,7 +193,7 @@ describe('erc20-transferable-receivable', () => { .hexZeroPad(tokenId.toHexString(), 16) .substring( 2, - )}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`, + )}000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000075c35c980c0d37ef46df04d31a140b65503c0eed0000000000000000000000000000000000000000000000000000000000000008${shortReference}000000000000000000000000000000000000000000000000`, gasPrice: '20000000000', to: '0xF426505ac145abE033fE77C666840063757Be9cd', value: 0, @@ -279,14 +279,12 @@ describe('erc20-transferable-receivable', () => { .salt, paymentAddress, ); - let metadata = Buffer.from(request.requestId).toString('base64'); let receivableContract = ERC20TransferableReceivable__factory.createInterface(); let data = receivableContract.encodeFunctionData('mint', [ thirdPartyWallet.address, `0x${shortReference}`, '100', erc20ContractAddress, - metadata, ]); let tx = await thirdPartyWallet.sendTransaction({ data, @@ -312,14 +310,12 @@ describe('erc20-transferable-receivable', () => { .salt, paymentAddress, ); - metadata = Buffer.from(request.requestId).toString('base64'); receivableContract = ERC20TransferableReceivable__factory.createInterface(); data = receivableContract.encodeFunctionData('mint', [ paymentAddress, `0x${shortReference}`, '100', erc20ContractAddress, - metadata, ]); tx = await thirdPartyWallet.sendTransaction({ data, 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 4b0fb5d27a..45d2d3aa3b 100644 --- a/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts +++ b/packages/smart-contracts/scripts/test-deploy-erc20-transferable-receivable.ts @@ -18,6 +18,7 @@ export async function deployERC20TransferableReceivable( 'tREC', mainPaymentAddresses.ERC20FeeProxyAddress, ], + version: '0.2.0', }, ); diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol index 2ffb2573b4..26e3967acc 100644 --- a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -13,11 +13,6 @@ import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; contract ERC20TransferableReceivable is ERC721 { using Counters for Counters.Counter; - /** - * @dev Counter for uniquely identifying payments - */ - Counters.Counter private _paymentId; - /** * @dev Counter for uniquely identifying receivables */ @@ -30,7 +25,6 @@ contract ERC20TransferableReceivable is ERC721 { address tokenAddress; uint256 amount; uint256 balance; - bytes32 requestID; } /** @@ -53,20 +47,14 @@ contract ERC20TransferableReceivable is ERC721 { * @param sender The address of the sender * @param recipient The address of the recipient of the payment * @param amount The amount of the payment - * @param paymentProxy The address of the payment proxy contract * @param receivableTokenId The ID of the receivable being paid - * @param tokenAddress The address of the ERC20 token used to pay the receivable - * @param paymentId The ID of the payment * @param paymentReference The reference for the payment */ event TransferableReceivablePayment( address sender, address recipient, uint256 amount, - address paymentProxy, uint256 receivableTokenId, - address tokenAddress, - uint256 paymentId, bytes indexed paymentReference ); @@ -121,7 +109,6 @@ contract ERC20TransferableReceivable is ERC721 { ) external { require(amount != 0, 'Zero amount provided'); address owner = ownerOf(receivableTokenId); - _paymentId.increment(); ReceivableInfo storage receivableInfo = receivableInfoMapping[receivableTokenId]; address tokenAddress = receivableInfo.tokenAddress; @@ -144,10 +131,7 @@ contract ERC20TransferableReceivable is ERC721 { msg.sender, owner, amount, - paymentProxy, receivableTokenId, - tokenAddress, - _paymentId.current(), paymentReference ); } @@ -158,16 +142,13 @@ contract ERC20TransferableReceivable is ERC721 { * @param paymentReference A reference for the payment. * @param amount The amount of ERC20 tokens to be paid. * @param erc20Addr The address of the ERC20 token to be used as payment. - * @param requestID The ID of the request associated with the receivable. - * Can be used to retrieve details of the request from Request Network protocol. * @dev Anyone can pay for the mint of a receivable on behalf of a user */ function mint( address owner, bytes calldata paymentReference, uint256 amount, - address erc20Addr, - bytes32 requestID + address erc20Addr ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); @@ -184,8 +165,7 @@ contract ERC20TransferableReceivable is ERC721 { receivableInfoMapping[currentReceivableTokenId] = ReceivableInfo({ tokenAddress: erc20Addr, amount: amount, - balance: 0, - requestID: requestID + balance: 0 }); _mint(owner, currentReceivableTokenId); diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.2.0.json similarity index 85% rename from packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json rename to packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.2.0.json index aa1b105406..373002ab5c 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.2.0.json @@ -160,30 +160,12 @@ "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", @@ -250,25 +232,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - } - ], - "name": "getTokenIds", - "outputs": [ - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -316,9 +279,9 @@ "type": "address" }, { - "internalType": "string", - "name": "newTokenURI", - "type": "string" + "internalType": "bytes32", + "name": "requestID", + "type": "bytes32" } ], "name": "mint", @@ -428,6 +391,11 @@ "internalType": "uint256", "name": "balance", "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "requestID", + "type": "bytes32" } ], "stateMutability": "view", @@ -553,49 +521,6 @@ "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": [ { @@ -615,19 +540,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts index 7970132e9d..1271dd0ed4 100644 --- a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/index.ts @@ -1,6 +1,7 @@ import { ContractArtifact } from '../../ContractArtifact'; import { abi as ABI_0_1_0 } from './0.1.0.json'; +import { abi as ABI_0_2_0 } from './0.2.0.json'; // @ts-ignore Cannot find module import type { ERC20TransferableReceivable } from '../../../types/ERC20TransferableReceivable'; @@ -28,6 +29,15 @@ export const erc20TransferableReceivableArtifact = }, }, }, + '0.2.0': { + abi: ABI_0_2_0, + deployment: { + private: { + address: '0xF426505ac145abE033fE77C666840063757Be9cd', + creationBlockNumber: 0, + }, + }, + }, }, - '0.1.0', + '0.2.0', ); diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index 225e198afb..3e6c00f904 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -49,94 +49,43 @@ describe('contract: ERC20TransferableReceivable', () => { describe('mint', async function () { it('revert with empty paymentReference', async function () { - await expect( - receivable.mint( - user1Addr, - [], - 1, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ), - ).to.be.revertedWith('Zero paymentReference provided'); + await expect(receivable.mint(user1Addr, [], 1, testToken.address)).to.be.revertedWith( + 'Zero paymentReference provided', + ); }); it('revert with zero amount', async function () { - await expect( - receivable.mint( - user1Addr, - '0x01', - 0, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ), - ).to.be.revertedWith('Zero amount provided'); + await expect(receivable.mint(user1Addr, '0x01', 0, testToken.address)).to.be.revertedWith( + 'Zero amount provided', + ); }); it('revert with empty asset address', async function () { await expect( - receivable.mint( - user1Addr, - '0x01', - 1, - ethers.constants.AddressZero, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ), + receivable.mint(user1Addr, '0x01', 1, ethers.constants.AddressZero), ).to.be.revertedWith('Zero address provided'); }); it('revert when trying to mint a receivable for the same request', async function () { - await receivable - .connect(user1) - .mint( - user1Addr, - '0x01', - 1, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address); }); it('revert with empty owner address', async function () { await expect( - receivable.mint( - ethers.constants.AddressZero, - '0x01', - 1, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ), + receivable.mint(ethers.constants.AddressZero, '0x01', 1, testToken.address), ).to.be.revertedWith('Zero address provided for owner'); }); it('revert with duplicated receivableId', async function () { - await receivable - .connect(user1) - .mint( - user1Addr, - '0x01', - 1, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address); await expect( - receivable - .connect(user1) - .mint( - user1Addr, - '0x01', - 2, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ), + receivable.connect(user1).mint(user1Addr, '0x01', 2, testToken.address), ).to.be.revertedWith('Receivable has already been minted for this user and request'); }); it('success', async function () { - const requestID = '0x34cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; const paymentRef = '0x01' as BytesLike; - await receivable - .connect(user1) - .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, requestID); + await receivable.connect(user1).mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address); const tokenId = 1; expect(await receivable.ownerOf(tokenId)).to.equals(user1Addr); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); @@ -146,56 +95,15 @@ describe('contract: ERC20TransferableReceivable', () => { expect(receivableInfo.tokenAddress).to.equal(testToken.address); expect(receivableInfo.amount).to.equal(BASE_DECIMAL); expect(receivableInfo.balance).to.equal(0); - expect(receivableInfo.requestID).to.equal(requestID); }); it('list receivables', async function () { - await receivable - .connect(user1) - .mint( - user1Addr, - '0x01', - BASE_DECIMAL, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); - await receivable - .connect(user1) - .mint( - user1Addr, - '0x02', - BASE_DECIMAL, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); - await receivable - .connect(user1) - .mint( - user1Addr, - '0x03', - BASE_DECIMAL, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user1).mint(user1Addr, '0x01', BASE_DECIMAL, testToken.address); + await receivable.connect(user1).mint(user1Addr, '0x02', BASE_DECIMAL, testToken.address); + await receivable.connect(user1).mint(user1Addr, '0x03', BASE_DECIMAL, testToken.address); await verifyReceivables(user1Addr, [1, 2, 3]); - await receivable - .connect(user2) - .mint( - user2Addr, - '0x04', - BASE_DECIMAL, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); - await receivable - .connect(user2) - .mint( - user2Addr, - '0x05', - BASE_DECIMAL, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user2).mint(user2Addr, '0x04', BASE_DECIMAL, testToken.address); + await receivable.connect(user2).mint(user2Addr, '0x05', BASE_DECIMAL, testToken.address); await verifyReceivables(user2Addr, [4, 5]); await receivable.connect(user1).transferFrom(user1Addr, user2Addr, 1); await verifyReceivables(user1Addr, [3, 2]); @@ -228,15 +136,7 @@ describe('contract: ERC20TransferableReceivable', () => { beforeEach(async () => { paymentRef = '0x01' as BytesLike; amount = BN.from(100).mul(BASE_DECIMAL); - await receivable - .connect(user1) - .mint( - user1Addr, - paymentRef, - amount, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user1).mint(user1Addr, paymentRef, amount, testToken.address); tokenId = BN.from(1); feeAmount = BN.from(10).mul(BASE_DECIMAL); }); @@ -277,30 +177,14 @@ describe('contract: ERC20TransferableReceivable', () => { }); it('allow multiple mints per receivable', async function () { - await receivable - .connect(user2) - .mint( - user2Addr, - paymentRef, - amount, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user2).mint(user2Addr, paymentRef, amount, testToken.address); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); }); it('allows user to mint on behalf of another user', async function () { paymentRef = '0x02' as BytesLike; - await receivable - .connect(user1) - .mint( - user2Addr, - paymentRef, - amount, - testToken.address, - '0x744383081b5ce5d256b385c914e51a2a5b3dde687d65ea163bc8b76f3f0bf40f', - ); + await receivable.connect(user1).mint(user2Addr, paymentRef, amount, testToken.address); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user2Addr, paymentRef]); tokenId = BN.from(2); expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId);