diff --git a/README.md b/README.md index 043561d..766a268 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,5 @@ npm run create npm run pay npm run declare npm run delegate +npm run create-many ``` diff --git a/package-lock.json b/package-lock.json index 8a78ab2..84a9e22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@requestnetwork/epk-signature": "0.9.5", "@requestnetwork/payment-processor": "0.52.0", "@requestnetwork/request-client.js": "0.54.0", + "cli-progress": "^3.12.0", "dotenv": "16.3.1", - "ethers": "5.7.2" + "ethers": "5.7.2", + "p-limit": "^6.2.0" }, "devDependencies": { "husky": "8.0.3", @@ -1893,6 +1895,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-progress/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -3587,6 +3651,21 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4765,6 +4844,18 @@ "engines": { "node": ">= 14" } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { @@ -6144,6 +6235,49 @@ "restore-cursor": "^4.0.0" } }, + "cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "requires": { + "string-width": "^4.2.3" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -7465,6 +7599,14 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" }, + "p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "requires": { + "yocto-queue": "^1.1.1" + } + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -8384,6 +8526,11 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "dev": true + }, + "yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==" } } } diff --git a/package.json b/package.json index 7f0e330..c987e7a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "pay": "node src/payRequest.js", "declare": "node src/declarePaymentSentAndReceived.js", "delegate": "node src/delegateDeclarePaymentSentAndReceived.js", - "hinkal-deposit": "node src/depositToHinkalShieldedAddress.js" + "hinkal-deposit": "node src/depositToHinkalShieldedAddress.js", + "create-many": "node src/createManyConcurrentRequests.js" }, "author": "", "license": "ISC", @@ -19,8 +20,10 @@ "@requestnetwork/epk-signature": "0.9.5", "@requestnetwork/payment-processor": "0.52.0", "@requestnetwork/request-client.js": "0.54.0", + "cli-progress": "^3.12.0", "dotenv": "16.3.1", - "ethers": "5.7.2" + "ethers": "5.7.2", + "p-limit": "^6.2.0" }, "devDependencies": { "husky": "8.0.3", diff --git a/src/createManyConcurrentRequests.js b/src/createManyConcurrentRequests.js new file mode 100644 index 0000000..d0f8391 --- /dev/null +++ b/src/createManyConcurrentRequests.js @@ -0,0 +1,184 @@ +(async () => { + const { + RequestNetwork, + Types, + Utils, + } = require("@requestnetwork/request-client.js"); + const { + EthereumPrivateKeySignatureProvider, + } = require("@requestnetwork/epk-signature"); + const { config } = require("dotenv"); + const { Wallet } = require("ethers"); + const pLimit = (await import("p-limit")).default; // Use dynamic import for ESM module + const cliProgress = require("cli-progress"); + + // --- Configuration --- + const TOTAL_REQUESTS = 100; // Total number of requests to create + const CONCURRENCY_LIMIT = 100; // Number of requests to create concurrently + // --------------------- + + // Load environment variables from .env file + config(); + + if (!process.env.PAYEE_PRIVATE_KEY) { + console.error("Error: PAYEE_PRIVATE_KEY is not defined in the .env file."); + process.exit(1); + } + + let aborted = false; + let successfulRequests = 0; + let failedRequests = 0; + let inProgressRequests = 0; + + // Setup Abort Handling (Ctrl+C) + process.on("SIGINT", () => { + console.log("\nAborting request creation..."); + aborted = true; + }); + + // Create a multi-bar container + const multibar = new cliProgress.MultiBar( + { + clearOnComplete: false, + hideCursor: true, + format: + " {bar} | {percentage}% | S: {successful}, F: {failed}, IP: {inProgress} | {value}/{total}", + }, + cliProgress.Presets.shades_classic, + ); + + const progressBar = multibar.create(TOTAL_REQUESTS, 0, { + successful: 0, + failed: 0, + inProgress: 0, + }); + + try { + const epkSignatureProvider = new EthereumPrivateKeySignatureProvider({ + method: Types.Signature.METHOD.ECDSA, + privateKey: process.env.PAYEE_PRIVATE_KEY, // Must include 0x prefix + }); + + const requestClient = new RequestNetwork({ + nodeConnectionConfig: { + baseURL: process.env.REQUEST_NODE_URL || "http://localhost:3000/", + }, + signatureProvider: epkSignatureProvider, + }); + + // In this example, the payee is also the payer and payment recipient. + const payeeIdentity = new Wallet(process.env.PAYEE_PRIVATE_KEY).address; + const payerIdentity = payeeIdentity; + const paymentRecipient = payeeIdentity; + const feeRecipient = "0x0000000000000000000000000000000000000000"; + + const limit = pLimit(CONCURRENCY_LIMIT); + const creationPromises = []; + + console.log( + `Attempting to create ${TOTAL_REQUESTS} requests with concurrency ${CONCURRENCY_LIMIT}...`, + ); + + for (let i = 0; i < TOTAL_REQUESTS; i++) { + if (aborted) { + console.log(`Skipping remaining requests due to abort signal.`); + break; + } + + creationPromises.push( + limit(async () => { + if (aborted) return; + + try { + // Increment in-progress counter and update progress bar + inProgressRequests++; + progressBar.update(successfulRequests + failedRequests, { + successful: successfulRequests, + failed: failedRequests, + inProgress: inProgressRequests, + }); + + const uniqueContent = `Request #${i + 1} - ${Date.now()}`; + + const requestCreateParameters = { + requestInfo: { + currency: { + type: Types.RequestLogic.CURRENCY.ERC20, + value: "0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C", + network: "sepolia", + }, + expectedAmount: "1000000000000000000", + payee: { + type: Types.Identity.TYPE.ETHEREUM_ADDRESS, + value: payeeIdentity, + }, + payer: { + type: Types.Identity.TYPE.ETHEREUM_ADDRESS, + value: payerIdentity, + }, + timestamp: Utils.getCurrentTimestampInSecond(), + }, + paymentNetwork: { + id: Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + parameters: { + paymentNetworkName: "sepolia", + paymentAddress: paymentRecipient, + feeAddress: feeRecipient, + feeAmount: "0", + }, + }, + contentData: { + reason: `🍕 - ${uniqueContent}`, + dueDate: "2023.06.16", + }, + signer: { + type: Types.Identity.TYPE.ETHEREUM_ADDRESS, + value: payeeIdentity, + }, + }; + + const request = await requestClient.createRequest( + requestCreateParameters, + ); + const requestData = await request.waitForConfirmation(); + successfulRequests++; + } catch (error) { + const errorMessage = error.message || error.toString(); + // Simplified error output to avoid console spam + console.error(`\nRequest failed: ${errorMessage.split("\n")[0]}`); + failedRequests++; + } finally { + inProgressRequests--; // Decrement in-progress counter + progressBar.update(successfulRequests + failedRequests, { + successful: successfulRequests, + failed: failedRequests, + inProgress: inProgressRequests, + }); + } + }), + ); + } + + await Promise.all(creationPromises); + } catch (error) { + console.error(`\nAn unexpected error occurred: ${error.message || error}`); + aborted = true; + } finally { + // Stop the progress bar + multibar.stop(); + + console.log("\n--- Request Creation Summary ---"); + console.log(`Total attempted: ${successfulRequests + failedRequests}`); + console.log(`Successful: ${successfulRequests}`); + console.log(`Failed: ${failedRequests}`); + + if (aborted && successfulRequests + failedRequests < TOTAL_REQUESTS) { + console.log( + `Process aborted. ${ + TOTAL_REQUESTS - (successfulRequests + failedRequests) + } requests were not attempted.`, + ); + } + console.log("------------------------------"); + } +})();