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: 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/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); } 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/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/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-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/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/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()); 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/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/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index fd5f2d1657..15650fcd0b 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -115,7 +115,6 @@ export function encodeMintErc20TransferableReceivableRequest( validateERC20TransferableReceivable(request); const tokenAddress = request.currencyInfo.value; - const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId const { paymentReference, paymentAddress } = getRequestPaymentValues(request); const amount = getAmountToPay(request); @@ -126,7 +125,6 @@ export function encodeMintErc20TransferableReceivableRequest( `0x${paymentReference}`, amount, tokenAddress, - metadata, ]); } @@ -204,10 +202,11 @@ export async function encodePayErc20TransferableReceivableRequest( const receivableContract = ERC20TransferableReceivable__factory.createInterface(); + // get tokenId from request 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/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..aa5da329e0 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: BigNumber.from(amount).add(feeAmount).toString(), + 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/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index f1cf3f3fca..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: { @@ -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.', @@ -192,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, @@ -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, @@ -276,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, @@ -309,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/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/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', }, ], 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/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', */ ]; /** 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 0d7fc47679..26e3967acc 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,14 +10,9 @@ 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; - /** - * @dev Counter for uniquely identifying payments - */ - Counters.Counter private _paymentId; - /** * @dev Counter for uniquely identifying receivables */ @@ -53,20 +47,14 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora * @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, ERC721Enumerable, ERC721URIStora ) 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, ERC721Enumerable, ERC721URIStora msg.sender, owner, amount, - paymentProxy, receivableTokenId, - tokenAddress, - _paymentId.current(), paymentReference ); } @@ -158,15 +142,13 @@ 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 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, - string memory newTokenURI + address erc20Addr ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); @@ -187,56 +169,5 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora }); _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; - - 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/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 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 5a9f012e27..3e6c00f904 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -41,82 +41,69 @@ 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( + 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, '')).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(user1Addr, '0x01', 1, ethers.constants.AddressZero, ''), + 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); + }); + 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), ).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); await expect( - receivable.connect(user1).mint(user1Addr, '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'); }); it('success', async function () { - const receivableId = '0x0134cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; - const metadata = ethers.utils.base64.encode(receivableId); 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]; + await receivable.connect(user1).mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address); + 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); }); 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); + 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, '4'); - await receivable.connect(user2).mint(user2Addr, '0x05', BASE_DECIMAL, testToken.address, '5'); + 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]); @@ -149,9 +136,8 @@ 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); + tokenId = BN.from(1); feeAmount = BN.from(10).mul(BASE_DECIMAL); }); @@ -191,21 +177,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); 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'); + await receivable.connect(user1).mint(user2Addr, paymentRef, amount, testToken.address); 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); }); 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), @@ -219,11 +205,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 () { 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/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. 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 */ 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); }