diff --git a/.env.benchmark.example b/.env.benchmark.example new file mode 100644 index 000000000..f238feeb3 --- /dev/null +++ b/.env.benchmark.example @@ -0,0 +1,29 @@ +THIRDWEB_SDK_SECRET_KEY= + +# benchmark vars +NODE_ENV=benchmark +BENCHMARK_HOST='http://127.0.0.1:3005' +BENCHMARK_URL_PATH='/contract/polygon/0x01De66609582B874FA34ab288859ACC4592aec04/write' +BENCHMARK_POST_BODY='{ + "function_name": "mintTo", + "args": ["0xCF3D06a19263976A540CFf8e7Be7b026801C52A6", "0","", "1"] +}' +# Total request has to be more than the total concurrency +BENCHMARK_CONCURRENCY=2 +BENCHMARK_REQUESTS=2 +SAMPLE_EDITION_CONTRACT_INFO='{ + "contractMetadata": { + "name": "TW WEB3API", + "description": "thirdweb web3API sample collection for testing", + "image": "ipfs://QmYxT4LnK8sqLupjbS6eRvu1si7Ly2wFQAqFebxhWntcf6", + "external_link": "", + "app_uri": "", + "seller_fee_basis_points": 0, + "fee_recipient": "0xCF3D06a19263976A540CFf8e7Be7b026801C52A6", + "symbol": "TWT", + "platform_fee_basis_points": 0, + "platform_fee_recipient": "0xCF3D06a19263976A540CFf8e7Be7b026801C52A6", + "primary_sale_recipient": "0xCF3D06a19263976A540CFf8e7Be7b026801C52A6", + "trusted_forwarders": [] + } +}' \ No newline at end of file diff --git a/.env.example b/.env.example index 180ed4126..da96eea85 100644 --- a/.env.example +++ b/.env.example @@ -54,7 +54,6 @@ CHAIN_OVERRIDES= # from "http://example1.com" or from a subdomain of "example2.com". ACCESS_CONTROL_ALLOW_ORIGIN=* - # benchmark vars [Optional] BENCHMARK_HOST='http://localhost:3005' BENCHMARK_URL_PATH='/contract/mumbai/0xc8be6265C06aC376876b4F62670adB3c4d72EABA/write' @@ -64,3 +63,4 @@ BENCHMARK_POST_BODY='{ }' BENCHMARK_CONCURRENCY=10 BENCHMARK_REQUESTS=10 + diff --git a/.gitignore b/.gitignore index 2b2af460b..8fee4d4c9 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ web_modules/ # dotenv environment variable files .env +.env.benchmark .env.development.local .env.test.local .env.production.local diff --git a/Makefile b/Makefile index 2491317b8..8d4af266b 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ SHELL := /bin/bash export NODE_ENV=testing -export THIRDWEB_API_KEY=TEST export POSTGRES_HOST=localhost export POSTGRES_DATABASE_NAME=postgres export POSTGRES_USER=postgres diff --git a/README.md b/README.md index 663a8c256..276dfb390 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,16 @@ The API defaults to `http://localhost:3005` +## Local Benchmarking + +As a way to support quantifying the robustness of our system, we have added benchmarking. Benchmark results may vary based on the machine that is being used. + +To run the benchmark: + +1. Run local server with `yarn dev` +1. Set-up `.env.benchmark` (For sensible defaults: `cp .env.benchmark.example .env.benchmark`) +1. Run benchmark in a separate terminal with `yarn benchmark` + ## Contributing We welcome contributions from all developers, regardless of experience level. If you are interested in contributing, please read our [Contributing Guide](./.github/contributing.md) where you'll learn how the repo works, how to test your changes, and how to submit a pull request. diff --git a/core/database/dbConnect.ts b/core/database/dbConnect.ts index 3e9c96f55..787e0e25c 100644 --- a/core/database/dbConnect.ts +++ b/core/database/dbConnect.ts @@ -89,7 +89,7 @@ export const connectWithDatabase = async (): Promise => { }; // Set the appropriate databse client package - let dbClientPackage: any; + let dbClientPackage: typeof pg; switch (dbClient) { case "pg": dbClientPackage = pg; diff --git a/core/database/sql-schemas/transactions.sql b/core/database/sql-schemas/transactions.sql index b2dcc1338..702ff9002 100644 --- a/core/database/sql-schemas/transactions.sql +++ b/core/database/sql-schemas/transactions.sql @@ -40,4 +40,6 @@ ALTER COLUMN "maxFeePerGas" TYPE VARCHAR(255), ALTER COLUMN "txType" TYPE VARCHAR(255), ADD COLUMN IF NOT EXISTS "deployedContractAddress" VARCHAR(255), ADD COLUMN IF NOT EXISTS "contractType" VARCHAR(255), -ADD COLUMN IF NOT EXISTS "errorMessage" TEXT DEFAULT NULL; \ No newline at end of file +ADD COLUMN IF NOT EXISTS "errorMessage" TEXT DEFAULT NULL, +ADD COLUMN IF NOT EXISTS "txMinedTimestamp" TIMESTAMP, +ADD COLUMN IF NOT EXISTS "blockNumber" BIGINT; \ No newline at end of file diff --git a/core/services/benchmark.ts b/core/services/benchmark.ts new file mode 100644 index 000000000..19b49322d --- /dev/null +++ b/core/services/benchmark.ts @@ -0,0 +1,41 @@ +// ! Winston Notes: This file is currently not in used. +// Keeping it around for when we might want to track specific function timings. +// Note that you cannot track functions in worker and output timings in server. +// Tracking and outputting timings must be done in the same docker image. +// Might release this as my own npm package and bring it in as a dependency in the future +import { randomUUID } from "crypto"; +import { PerformanceObserver, performance } from "perf_hooks"; + +export const watchPerformance = ( + onNewEntry: (entry: PerformanceEntry) => void, +) => { + const performanceObserver = new PerformanceObserver((items) => { + items.getEntries().forEach((entry) => { + onNewEntry(entry); + }); + }); + performanceObserver.observe({ entryTypes: ["measure"], buffered: true }); +}; + +export const timeFunction = < + F extends (...args: any) => any, + Args = F extends (args: infer A) => any ? A : never, +>( + fn: F, + resultTag: (args: Awaited>) => string, +) => { + const result = async (args: Args): Promise>> => { + const marker = randomUUID(); + const markerStart = `${marker}-start`; + const markerEnd = `${marker}-end`; + + performance.mark(markerStart); + const fnResult = await fn(args); + performance.mark(markerEnd); + + const measureName = resultTag(fnResult); + performance.measure(measureName, markerStart, markerEnd); + return fnResult; + }; + return result; +}; diff --git a/core/services/blockchain.ts b/core/services/blockchain.ts index 2e1f6574b..71a425639 100644 --- a/core/services/blockchain.ts +++ b/core/services/blockchain.ts @@ -14,3 +14,13 @@ export const getWalletNonce = async ( throw error; } }; + +export const getFeeData = async (provider: providers.Provider) => { + try { + const feeData = await provider.getFeeData(); + + return feeData; + } catch (error) { + throw error; + } +}; diff --git a/package.json b/package.json index 194fcf2a8..1f2e8593b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "lint": "eslint 'server/**/*.ts'", "lint:fix": "eslint --fix 'server/**/*.ts'", "test": "make test-evm", - "test:all": "mocha --exit" + "test:all": "mocha --exit", + "benchmark": "ts-node ./scripts/benchmark/index.ts" }, "dependencies": { "@fastify/cookie": "^8.3.0", @@ -59,6 +60,7 @@ "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.2.3", "@swc/core": "^1.3.41", + "@types/autocannon": "^7.9.1", "@types/chai": "^4.3.5", "@types/cookie": "^0.5.1", "@types/express": "^4.17.17", @@ -71,6 +73,7 @@ "@types/ws": "^8.5.5", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", + "autocannon": "^7.12.0", "chai": "^4.3.7", "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", diff --git a/scripts/benchmark/index.ts b/scripts/benchmark/index.ts new file mode 100644 index 000000000..fa7527976 --- /dev/null +++ b/scripts/benchmark/index.ts @@ -0,0 +1,367 @@ +import { Static } from "@sinclair/typebox"; +import { TypeCompiler } from "@sinclair/typebox/compiler"; +import autocannon from "autocannon"; +import * as dotenv from "dotenv"; +import { env } from "process"; +import { transactionResponseSchema } from "../../server/schemas/transaction"; + +dotenv.config({ + debug: true, + override: true, + path: ".env.benchmark", +}); + +function logInfo(msg: string) { + console.log(`[INFO] ${msg}`); +} +function logError(msg: string) { + console.error(`[ERROR] ${msg}`); +} + +function getBenchmarkOpts() { + if (!env.THIRDWEB_SDK_SECRET_KEY) { + throw new Error("THIRDWEB_SDK_SECRET_KEY is not set"); + } + const opts = { + THIRDWEB_SDK_SECRET_KEY: env.THIRDWEB_SDK_SECRET_KEY, + BENCHMARK_HOST: env.BENCHMARK_HOST ?? "http://127.0.0.1:3005", + BENCHMARK_URL_PATH: + env.BENCHMARK_URL_PATH ?? + "/contract/polygon/0x01De66609582B874FA34ab288859ACC4592aec04/write", + BENCHMARK_POST_BODY: + env.BENCHMARK_POST_BODY ?? + '{ "function_name": "mintTo", "args": ["0xCF3D06a19263976A540CFf8e7Be7b026801C52A6", "0","", "1"] }', + BENCHMARK_CONCURRENCY: parseInt(env.BENCHMARK_CONCURRENCY ?? "1"), + BENCHMARK_REQUESTS: parseInt(env.BENCHMARK_REQUESTS ?? "1"), + }; + return opts; +} + +async function sendTransaction(opts: ReturnType) { + const txnIds: string[] = []; + + return new Promise(async (resolve, reject) => { + const instance = autocannon({ + url: `${opts.BENCHMARK_HOST}`, + connections: opts.BENCHMARK_CONCURRENCY, + amount: opts.BENCHMARK_REQUESTS, + requests: [ + { + path: opts.BENCHMARK_URL_PATH, + headers: { + authorization: `Bearer ${opts.THIRDWEB_SDK_SECRET_KEY}`, + "content-type": "application/json", + }, + method: "POST", + body: opts.BENCHMARK_POST_BODY, + // @ts-ignore: autocannon types are 3 minor versions behind. + // This was one of the new field that was recently added + onResponse: (status: number, body: string) => { + if (status === 200) { + const parsedResult: { result?: string } = JSON.parse(body); + if (!parsedResult.result) { + logError( + `Response body does not contain a "result" field: ${body}`, + ); + return reject({ + error: "Response body does not contain a 'result' field", + }); + } + txnIds.push(parsedResult.result); + } else { + logError( + `Received status code ${status} from server. Body: ${body}`, + ); + return reject({ + error: `Received status code ${status} from server.`, + }); + } + }, + }, + ], + }); + + autocannon.track(instance, { + renderLatencyTable: false, + renderResultsTable: false, + }); + + const result = autocannon.printResult(await instance); + logInfo(result); + resolve(txnIds); + }); +} + +async function fetchStatus({ + txnId, + host, + apiKey, +}: { + txnId: string; + host: string; + apiKey: string; +}) { + const resp = await fetch(`${host}/transaction/status/${txnId}`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + const raw = await resp.json(); + return raw.result; +} + +async function tryUntilCompleted({ + txnId, + host, + apiKey, +}: { + txnId: string; + host: string; + apiKey: string; +}): Promise { + try { + const resp = await fetch(`${host}/transaction/status/${txnId}`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + const raw = await resp.json(); + // logInfo( + // `Got status: ${raw.result.status}, queueId: ${raw.result.queueId}. Retrying...`, + // ); + if (raw.result.status === "mined" || raw.result.status === "errored") { + return raw.result; + } + // logInfo("Sleeping for 10 second..."); + await sleep(10); + return tryUntilCompleted({ txnId, host, apiKey }); + } catch (error) { + console.error("tryUntilCompleted error", error); + } +} + +function parseStatus( + status: unknown, +): Static { + const C = TypeCompiler.Compile(transactionResponseSchema); + const isValue = C.Check(status); + if (!isValue) { + throw new Error(`Invalid response from server: ${status}`); + } + return status; +} + +function sleep(timeInSeconds: number) { + return new Promise((resolve) => setTimeout(resolve, timeInSeconds * 1_000)); +} + +async function processTransaction( + txnIds: string[], + opts: ReturnType, +) { + // give queue some time to process things + logInfo( + "Checking for status until all transactions are mined/errored. Can take upto 30 seconds or more...", + ); + // await sleep(30); + const statuses = await Promise.all( + txnIds.map((txnId) => { + return tryUntilCompleted({ + apiKey: opts.THIRDWEB_SDK_SECRET_KEY, + host: opts.BENCHMARK_HOST, + txnId, + }); + }), + ); + + type txn = { + timeTaken?: number; + txnHash?: string; + status: string; + }; + const erroredTransaction: txn[] = []; + const submittedTransaction: txn[] = []; + const processedTransaction: txn[] = []; + const minedTransaction: txn[] = []; + // logInfo("statuses", statuses); + statuses.map((status) => { + const parsedStatus = parseStatus(status); + switch (parsedStatus.status) { + case "errored": { + erroredTransaction.push({ + status: parsedStatus.status, + }); + break; + } + default: { + if ( + parsedStatus.txProcessedTimestamp && + parsedStatus.createdTimestamp + ) { + // throw new Error( + // `Invalid response from server for status transaction: ${JSON.stringify( + // parsedStatus, + // )}`, + // ); + processedTransaction.push({ + status: parsedStatus.status!, + timeTaken: + new Date(parsedStatus.txProcessedTimestamp).getTime() - + new Date(parsedStatus.createdTimestamp).getTime(), + txnHash: parsedStatus.txHash, + }); + } + + if ( + parsedStatus.txSubmittedTimestamp && + parsedStatus.createdTimestamp + ) { + // throw new Error( + // `Invalid response from server for submitted transaction: ${JSON.stringify( + // parsedStatus, + // )}`, + // ); + submittedTransaction.push({ + status: parsedStatus.status!, + timeTaken: + new Date(parsedStatus.txSubmittedTimestamp).getTime() - + new Date(parsedStatus.createdTimestamp).getTime(), + txnHash: parsedStatus.txHash, + }); + } + + if (parsedStatus.txMinedTimestamp && parsedStatus.createdTimestamp) { + // throw new Error( + // `Invalid response from server for mined transaction: ${JSON.stringify( + // parsedStatus, + // )}`, + minedTransaction.push({ + status: parsedStatus.status!, + timeTaken: + new Date(parsedStatus.txMinedTimestamp).getTime() - + new Date(parsedStatus.createdTimestamp).getTime(), + txnHash: parsedStatus.txHash, + }); + } + break; + } + } + }); + + console.table({ + error: erroredTransaction.length, + processing: processedTransaction.length, + submittedToMempool: submittedTransaction.length, + minedTransaction: minedTransaction.length, + }); + + const sortedProcessedTransaction = processedTransaction.sort( + (a, b) => (a.timeTaken ?? 0) - (b.timeTaken ?? 0), + ); + + const sortedSubmittedTransaction = submittedTransaction.sort( + (a, b) => (a.timeTaken ?? 0) - (b.timeTaken ?? 0), + ); + + const sortedMinedTransaction = minedTransaction.sort( + (a, b) => (a.timeTaken ?? 0) - (b.timeTaken ?? 0), + ); + + console.table({ + "Avg Processing Time": + processedTransaction.reduce( + (acc, curr) => acc + (curr.timeTaken ?? 0), + 0, + ) / + processedTransaction.length / + 1_000 + + " sec", + "Median Processing Time": + (sortedProcessedTransaction[ + Math.floor(sortedProcessedTransaction.length / 2) + ].timeTaken ?? 0) / + 1_000 + + " sec", + "Min Processing Time": + (sortedProcessedTransaction[0].timeTaken ?? 0) / 1_000 + " sec", + "Max Processing Time": + (sortedProcessedTransaction[sortedProcessedTransaction.length - 1] + .timeTaken ?? 0) / + 1_000 + + " sec", + }); + + console.table({ + "Avg Submission Time": + submittedTransaction.reduce( + (acc, curr) => acc + (curr.timeTaken ?? 0), + 0, + ) / + submittedTransaction.length / + 1_000 + + " sec", + "Median Submission Time": + (sortedSubmittedTransaction[Math.floor(submittedTransaction.length / 2)] + .timeTaken ?? 0) / + 1_000 + + " sec", + "Min Submission Time": + (sortedSubmittedTransaction[0].timeTaken ?? 0) / 1_000 + " sec", + "Max Submission Time": + (sortedSubmittedTransaction[sortedSubmittedTransaction.length - 1] + .timeTaken ?? 0) / + 1_000 + + " sec", + }); + + console.table({ + "Avg Mined Time": + minedTransaction.reduce((acc, curr) => acc + (curr.timeTaken ?? 0), 0) / + minedTransaction.length / + 1_000 + + " sec", + "Median Mined Time": + (sortedMinedTransaction[Math.floor(minedTransaction.length / 2)] + .timeTaken ?? 0) / + 1_000 + + " sec", + "Min Mined Time": + (sortedMinedTransaction[0].timeTaken ?? 0) / 1_000 + " sec", + "Max Mined Time": + (sortedMinedTransaction[sortedMinedTransaction.length - 1].timeTaken ?? + 0) / + 1_000 + + " sec", + }); + + return { + erroredTransaction, + submittedTransaction, + processedTransaction, + minedTransaction, + }; +} + +function confirmTransaction() {} + +function getBlockStats() {} + +// async/await +async function runBenchmark() { + const opts = getBenchmarkOpts(); + + logInfo( + `Benchmarking ${opts.BENCHMARK_HOST}${opts.BENCHMARK_URL_PATH} with ${opts.BENCHMARK_REQUESTS} requests and a concurrency of ${opts.BENCHMARK_CONCURRENCY}`, + ); + logInfo("Sending transactions..."); + const txnIds = await sendTransaction(opts); + + logInfo("Checking time taken for submission to mempool"); + await processTransaction(txnIds, opts); +} + +runBenchmark().catch((e) => { + console.error("Error while running benchmark:"); + console.error(e); + process.exit(1); +}); diff --git a/server/api/index.ts b/server/api/index.ts index 1b38737ee..e948b6026 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -1,18 +1,18 @@ import { FastifyInstance } from "fastify"; +import { getContractExtensions } from "./contract/metadata/extensions"; import { readContract } from "./contract/read/read"; import { writeToContract } from "./contract/write/write"; -import { getContractExtensions } from "./contract/metadata/extensions"; -import { checkTxStatus } from "./transaction/status"; import { getAllTx } from "./transaction/getAll"; import { getAllDeployedContracts } from "./transaction/getAllDeployedContracts"; +import { checkTxStatus } from "./transaction/status"; // Extensions +import { erc1155Routes } from "./contract/extensions/erc1155"; import { erc20Routes } from "./contract/extensions/erc20/index"; import { erc721Routes } from "./contract/extensions/erc721"; -import { erc1155Routes } from "./contract/extensions/erc1155"; -import { prebuiltsRoutes } from "./deployer"; import { marketplaceV3Routes } from "./contract/extensions/marketplaceV3/index"; +import { prebuiltsRoutes } from "./deployer"; // Chain import { getChainData } from "./network/get"; @@ -29,9 +29,9 @@ import { grantRole } from "./contract/roles/write/grant"; import { revokeRole } from "./contract/roles/write/revoke"; // Contract Metadata +import { getABI } from "./contract/metadata/abi"; import { extractEvents } from "./contract/metadata/events"; import { extractFunctions } from "./contract/metadata/functions"; -import { getABI } from "./contract/metadata/abi"; // Admin Wallet import { getBalance } from "./wallet/getBalance"; diff --git a/server/api/transaction/getAll.ts b/server/api/transaction/getAll.ts index 6bda7e7df..bb1f6b469 100644 --- a/server/api/transaction/getAll.ts +++ b/server/api/transaction/getAll.ts @@ -1,10 +1,10 @@ +import { Static, Type } from "@sinclair/typebox"; import { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { Static, Type } from "@sinclair/typebox"; import { createCustomError } from "../../../core/error/customError"; +import { getAllTxFromDB } from "../../helpers"; import { standardResponseSchema } from "../../helpers/sharedApiSchemas"; import { transactionResponseSchema } from "../../schemas/transaction"; -import { getAllTxFromDB } from "../../helpers"; // INPUT const requestQuerySchema = Type.Object({ @@ -72,6 +72,7 @@ responseBodySchema.example = { submittedTxNonce: 562, createdTimestamp: "2023-06-01T18:56:50.787Z", txSubmittedTimestamp: "2023-06-01T18:56:54.908Z", + txProcessedTimestamp: "2023-06-01T18:56:54.908Z", }, ], }; diff --git a/server/api/transaction/getAllDeployedContracts.ts b/server/api/transaction/getAllDeployedContracts.ts index 138b3856e..9182de136 100644 --- a/server/api/transaction/getAllDeployedContracts.ts +++ b/server/api/transaction/getAllDeployedContracts.ts @@ -72,6 +72,7 @@ responseBodySchema.example = { submittedTxNonce: 111, createdTimestamp: "2023-06-21T18:05:18.979Z", txSubmittedTimestamp: "2023-06-21T18:05:21.823Z", + txProcessedTimestamp: "2023-06-21T18:05:21.823Z", deployedContractAddress: "0x8dE0E40e8a5108Da3e0D65cFc908269fE083DfE7", contractType: "edition", }, diff --git a/server/api/transaction/status.ts b/server/api/transaction/status.ts index 8de9434c2..15b3b8b28 100644 --- a/server/api/transaction/status.ts +++ b/server/api/transaction/status.ts @@ -30,24 +30,30 @@ export const responseBodySchema = Type.Object({ responseBodySchema.example = { result: { - queueId: "d09e5849-a262-4f0f-84be-55389c6c7bce", - walletAddress: "0x1946267d81fb8adeeea28e6b98bcd446c8248473", + queueId: "a20ed4ce-301d-4251-a7af-86bd88f6c015", + walletAddress: "0x3ecdbf3b911d0e9052b64850693888b008e18373", contractAddress: "0x365b83d67d5539c6583b9c0266a548926bf216f4", chainId: "80001", extension: "non-extension", - status: "submitted", + status: "mined", encodedInputData: - "0xa9059cbb0000000000000000000000003ecdbf3b911d0e9052b64850693888b008e1837300000000000000000000000000000000000000000000000000000000000f4240", + "0xa9059cbb0000000000000000000000001946267d81fb8adeeea28e6b98bcd446c824847300000000000000000000000000000000000000000000000000000000000186a0", txType: 2, - gasPrice: "", + gasPrice: "1500000017", gasLimit: "46512", maxPriorityFeePerGas: "1500000000", - maxFeePerGas: "1500000032", + maxFeePerGas: "1500000034", txHash: - "0x6b63bbe29afb2813e8466c0fc48b22f6c2cc835de8b5fd2d9815c28f63b2b701", - submittedTxNonce: 562, - createdTimestamp: "2023-06-01T18:56:50.787Z", - txSubmittedTimestamp: "2023-06-01T18:56:54.908Z", + "0x6de86da898fa4beb13d965c42bf331ad46cfa061cadf75f69791f31c9d8a4f66", + submittedTxNonce: 698, + createdTimestamp: "2023-08-25T22:42:26.910Z", + txProcessedTimestamp: "2023-08-25T22:42:27.302Z", + txSubmittedTimestamp: "2023-08-25T22:42:28.743Z", + deployedContractAddress: "", + contractType: "", + errorMessage: "", + txMinedTimestamp: "2023-08-25T22:42:33.000Z", + blockNumber: 39398545, }, }; diff --git a/server/helpers/dbOperations.ts b/server/helpers/dbOperations.ts index 03c4a3081..b3acb9d38 100644 --- a/server/helpers/dbOperations.ts +++ b/server/helpers/dbOperations.ts @@ -114,6 +114,7 @@ export const queueTransaction = async ( encodedInputData: encodedData, deployedContractAddress, contractType, + createdTimestamp: new Date(), }; if (!txDataToInsert.identifier) { diff --git a/server/helpers/server.ts b/server/helpers/server.ts index ab3487e00..a81a75569 100644 --- a/server/helpers/server.ts +++ b/server/helpers/server.ts @@ -44,11 +44,11 @@ const createServer = async (serverName: string): Promise => { request.headers.upgrade && request.headers.upgrade.toLowerCase() === "websocket" ) { - server.log.info("WebSocket connection attempt"); + server.log.debug("WebSocket connection attempt"); // ToDo: Uncomment WebSocket Authentication post Auth SDK is implemented // await performWSAuthentication(request, reply); } else { - server.log.info("Regular HTTP request"); + server.log.debug("Regular HTTP request"); await performHTTPAuthentication(request, reply); } }); diff --git a/server/schemas/transaction/index.ts b/server/schemas/transaction/index.ts index eb04bf638..b643825c1 100644 --- a/server/schemas/transaction/index.ts +++ b/server/schemas/transaction/index.ts @@ -77,6 +77,12 @@ export const transactionResponseSchema = Type.Object({ description: "Transaction Request Creation Timestamp", }), ), + txProcessedTimestamp: Type.Optional( + Type.String({ + description: + "Transaction Processed Timestamp (happens right before submission timestamp)", + }), + ), txSubmittedTimestamp: Type.Optional( Type.String({ description: "Transaction Submission Timestamp", @@ -97,6 +103,16 @@ export const transactionResponseSchema = Type.Object({ description: "Error Message", }), ), + txMinedTimestamp: Type.Optional( + Type.String({ + description: "Transaction Mined Status Update Timestamp", + }), + ), + blockNumber: Type.Optional( + Type.Number({ + description: "Block Number where the transaction was mined", + }), + ), }); export enum TransactionStatusEnum { @@ -127,9 +143,13 @@ export interface TransactionSchema { maxFeePerGas?: string; txHash?: string; status?: string; - createdTimestamp?: string; - txSubmittedTimestamp?: string; + createdTimestamp?: Date; + txSubmittedTimestamp?: Date; + txProcessedTimestamp?: Date; submittedTxNonce?: number; deployedContractAddress?: string; contractType?: string; + errorMessage?: string; + txMinedTimestamp?: Date; + blockNumber?: number; } diff --git a/worker/controller/blockchainReader.ts b/worker/controller/blockchainReader.ts index 3331f52e8..4c7ce59dd 100644 --- a/worker/controller/blockchainReader.ts +++ b/worker/controller/blockchainReader.ts @@ -1,7 +1,8 @@ import { BigNumber } from "ethers"; import { FastifyInstance } from "fastify"; import { Knex } from "knex"; -import { connectWithDatabase, env, getSDK } from "../../core"; +import { connectWithDatabase, env } from "../../core"; +import { getTransactionReceiptWithBlockDetails } from "../services/blockchain"; import { getSubmittedTransactions, updateTransactionState, @@ -24,47 +25,56 @@ export const checkForMinedTransactionsOnBlockchain = async ( "Running Cron to check for mined transactions on blockchain", ); trx = await knex.transaction(); - const transactions = await getSubmittedTransactions(knex); + const transactions = await getSubmittedTransactions(knex, trx); + if (transactions.length === 0) { server.log.warn("No transactions to check for mined status"); - await trx.commit(); + await trx.rollback(); await trx.destroy(); await knex.destroy(); return; } - const txReceipts = await Promise.all( - transactions.map(async (txData) => { - server.log.debug( - `Getting receipt for tx: ${txData.txHash} on chain: ${txData.chainId} for queueId: ${txData.identifier}`, - ); - const sdk = await getSDK(txData.chainId!); - return sdk.getProvider().getTransactionReceipt(txData.txHash!); - }), + const blockNumbers: { + blockNumber: number; + chainId: string; + queueId: string; + }[] = []; + const txReceiptsWithChainId = await getTransactionReceiptWithBlockDetails( + server, + transactions, ); - for (let txReceipt of txReceipts) { - if (!txReceipt) { - continue; - } - const txData = transactions.find( - (tx) => tx.txHash === txReceipt.transactionHash, - ); - if (txData) { + for (let txReceiptData of txReceiptsWithChainId) { + if ( + txReceiptData.blockNumber != -1 && + txReceiptData.chainId && + txReceiptData.queueId && + txReceiptData.txHash != "" && + txReceiptData.effectiveGasPrice != BigNumber.from(-1) && + txReceiptData.timestamp != -1 + ) { server.log.debug( - `Got receipt for tx: ${txData.txHash}, queueId: ${txData.identifier}, effectiveGasPrice: ${txReceipt.effectiveGasPrice}`, + `Got receipt for tx: ${txReceiptData.txHash}, queueId: ${txReceiptData.queueId}, effectiveGasPrice: ${txReceiptData.effectiveGasPrice}`, ); await updateTransactionState( knex, - txData.identifier!, + txReceiptData.queueId, "mined", trx, undefined, undefined, - { gasPrice: BigNumber.from(txReceipt.effectiveGasPrice).toString() }, + { + gasPrice: BigNumber.from( + txReceiptData.effectiveGasPrice, + ).toString(), + txMinedTimestamp: new Date(txReceiptData.timestamp), + blockNumber: txReceiptData.blockNumber, + }, ); } } + await trx.commit(); await trx.destroy(); await knex.destroy(); diff --git a/worker/controller/listener.ts b/worker/controller/listener.ts index 9205ae6fe..4bf0dfb6e 100644 --- a/worker/controller/listener.ts +++ b/worker/controller/listener.ts @@ -3,6 +3,14 @@ import { connectWithDatabase } from "../../core"; import { queue } from "../services/pQueue"; import { processTransaction } from "./processTransaction"; +const beginTransactionProcessing = (server: FastifyInstance) => { + return async () => { + server.log.info(`--- processing Q request started at ${new Date()} ---`); + await processTransaction(server); + server.log.info(`--- processing Q request ended at ${new Date()} ---`); + }; +}; + export const startNotificationListener = async ( server: FastifyInstance, ): Promise => { @@ -12,26 +20,14 @@ export const startNotificationListener = async ( const knex = await connectWithDatabase(); const connection = await knex.client.acquireConnection(); connection.query("LISTEN new_transaction_data"); - // Adding to Queue to Process Requests - queue.add(async () => { - server.log.info(`--- processing Q request started at ${new Date()} ---`); - await processTransaction(server); - server.log.info(`--- processing Q request ended at ${new Date()} ---`); - }); + // Adding to Queue to Process Requests + queue.add(beginTransactionProcessing(server)); connection.on( "notification", async (msg: { channel: string; payload: string }) => { - queue.add(async () => { - server.log.info( - `--- processing Q request started at ${new Date()} ---`, - ); - await processTransaction(server); - server.log.info( - `--- processing Q request ended at ${new Date()} ---`, - ); - }); + queue.add(beginTransactionProcessing(server)); }, ); diff --git a/worker/controller/processTransaction.ts b/worker/controller/processTransaction.ts index bafdfe066..33d05c98a 100644 --- a/worker/controller/processTransaction.ts +++ b/worker/controller/processTransaction.ts @@ -1,4 +1,4 @@ -import { BigNumber, ethers } from "ethers"; +import { BigNumber, ethers, providers } from "ethers"; import { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { Knex } from "knex"; @@ -8,7 +8,6 @@ import { env, getSDK, } from "../../core"; -import { getWalletNonce } from "../../core/services/blockchain"; import { getTransactionsToProcess, getWalletDetailsWithTrx, @@ -20,9 +19,10 @@ const MIN_TRANSACTION_TO_PROCESS = env.MIN_TRANSACTION_TO_PROCESS; export const processTransaction = async ( server: FastifyInstance, -): Promise => { +): Promise => { let knex: Knex | null = null; let trx: Knex.Transaction | null = null; + let processedIds: string[] = []; try { // Connect to the DB knex = await connectWithDatabase(); @@ -49,11 +49,13 @@ export const processTransaction = async ( await trx.commit(); await trx.destroy(); await knex.destroy(); - return; + return []; } + processedIds = data.rows.map((row: any) => row.identifier); for (const tx of data.rows) { server.log.info(`Processing Transaction: ${tx.identifier}`); + const walletData = await getWalletDetailsWithTrx( tx.walletAddress, tx.chainId, @@ -62,10 +64,7 @@ export const processTransaction = async ( ); const sdk = await getSDK(tx.chainId); - let blockchainNonce = await getWalletNonce( - tx.walletAddress, - sdk.getProvider(), - ); + let blockchainNonce = await sdk.wallet.getNonce("pending"); let lastUsedNonce = BigNumber.from(walletData?.lastUsedNonce ?? -1); let txSubmittedNonce = BigNumber.from(0); @@ -81,7 +80,8 @@ export const processTransaction = async ( // Submit transaction to the blockchain // Create transaction object - const txObject = { + + const txObject: providers.TransactionRequest = { to: tx.contractAddress ?? tx.toAddress, from: tx.walletAddress, data: tx.encodedInputData, @@ -150,6 +150,6 @@ export const processTransaction = async ( if (knex) { await knex.destroy(); } + return processedIds; } - return; }; diff --git a/worker/services/blockchain.ts b/worker/services/blockchain.ts new file mode 100644 index 000000000..c64744d46 --- /dev/null +++ b/worker/services/blockchain.ts @@ -0,0 +1,74 @@ +import { getBlock } from "@thirdweb-dev/sdk"; +import { BigNumber } from "ethers"; +import { FastifyInstance } from "fastify"; +import { getSDK } from "../../core"; +import { TransactionSchema } from "../../server/schemas/transaction"; + +type TransactionReceiptWithBlockDetails = { + txHash: string; + blockNumber: number; + chainId: string; + queueId: string; + timestamp: number; + effectiveGasPrice: BigNumber; +}; + +export const getTransactionReceiptWithBlockDetails = async ( + server: FastifyInstance, + transactions: TransactionSchema[], +): Promise => { + try { + const txReceiptData = await Promise.all( + transactions.map(async (txData) => { + server.log.debug( + `Getting receipt for tx: ${txData.txHash} on chain: ${txData.chainId} for queueId: ${txData.identifier}`, + ); + const sdk = await getSDK(txData.chainId!); + const receipt = await sdk + .getProvider() + .getTransactionReceipt(txData.txHash!); + return { + receipt, + chainId: txData.chainId!, + queueId: txData.identifier!, + }; + }), + ); + + const txDatawithBlockDetails = await Promise.all( + txReceiptData.map(async (dt) => { + if (!dt.receipt) { + server.log.debug( + `Receipt not found for tx: ${dt.queueId} on chain: ${dt.chainId}`, + ); + return { + txHash: "", + blockNumber: -1, + timestamp: -1, + chainId: dt.chainId!, + queueId: dt.queueId!, + effectiveGasPrice: BigNumber.from(0), + }; + } + const sdk = await getSDK(dt.chainId!); + const blockNumberDetails = await getBlock({ + block: dt.receipt.blockNumber, + network: sdk.getProvider(), + }); + return { + txHash: dt.receipt.transactionHash, + blockNumber: dt.receipt.blockNumber, + timestamp: blockNumberDetails.timestamp * 1000, + chainId: dt.chainId!, + queueId: dt.queueId!, + effectiveGasPrice: dt.receipt.effectiveGasPrice, + }; + }), + ); + + return txDatawithBlockDetails; + } catch (error) { + server.log.error(error, "Error in getTransactionReceiptFromChain"); + return []; + } +}; diff --git a/worker/services/dbOperations.ts b/worker/services/dbOperations.ts index 126304cb3..094ba07bc 100644 --- a/worker/services/dbOperations.ts +++ b/worker/services/dbOperations.ts @@ -171,16 +171,32 @@ export const getWalletDetailsWithoutTrx = async ( export const getSubmittedTransactions = async ( database: Knex, + trx: Knex.Transaction, ): Promise => { - const data = await database.raw( - `select * from transactions - where "txProcessed" = true - and "txSubmitted" = true - and "txMined" = false - and "txErrored" = false - and "txHash" is not null - order by "txSubmittedTimestamp" ASC - limit ${MIN_TX_TO_CHECK_FOR_MINED_STATUS}`, - ); - return data.rows; + const now = new Date(); // Current date and time + now.setSeconds(now.getSeconds() - 15); + + const data = await database("transactions") + .where(function () { + this.whereNotNull("txHash") + .andWhere("txSubmitted", true) + .andWhere("txMined", false) + .andWhere("txErrored", false) + .andWhere("txProcessed", true) + .orWhere(function () { + this.whereNotNull("txHash") + .andWhere("txSubmitted", true) + .andWhere("txMined", true) + .andWhere("txErrored", false) + .andWhere("txProcessed", true) + .whereNull("blockNumber"); + }); + }) + .andWhere("txSubmittedTimestamp", "<", now) + .orderBy("txSubmittedTimestamp", "asc") + .limit(MIN_TX_TO_CHECK_FOR_MINED_STATUS) + .forUpdate() + .skipLocked() + .transacting(trx); + return data; }; diff --git a/yarn.lock b/yarn.lock index df8b18e71..bf6c67894 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,11 @@ debug "^4.3.4" ethers "^5.7.0" +"@assemblyscript/loader@^0.19.21": + version "0.19.23" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72" + integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw== + "@babel/runtime@^7.17.2", "@babel/runtime@^7.22.3": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" @@ -111,6 +116,11 @@ stream-browserify "^3.0.0" util "^0.12.4" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -1617,6 +1627,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/autocannon@^7.9.1": + version "7.9.1" + resolved "https://registry.yarnpkg.com/@types/autocannon/-/autocannon-7.9.1.tgz#7e99da2cecbe7a07309a662de47d64dbbe4418be" + integrity sha512-WCzI7ecTeoT/YVZC1UcYnBundUCdomFNvfyK9ayu384cb2MizJIQYLMYExPDB7L46jW5fX2iHjKebrzAKKsuiQ== + dependencies: + "@types/node" "*" + "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.5": version "4.11.6" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" @@ -2513,6 +2530,35 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +autocannon@^7.12.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.12.0.tgz#d85cf81cd4fdfb9b5b6a49756c613fa33cb61993" + integrity sha512-SZwtwykFZaoz5pKg7WY7gw1Dayqv9buXSjvc99sSzZIfguUc4FmFEFJr3INtfXJ7o9Xyn5bJM093wxipVV59ZA== + dependencies: + chalk "^4.1.0" + char-spinner "^1.0.1" + cli-table3 "^0.6.0" + color-support "^1.1.1" + cross-argv "^2.0.0" + form-data "^4.0.0" + has-async-hooks "^1.0.0" + hdr-histogram-js "^3.0.0" + hdr-histogram-percentiles-obj "^3.0.0" + http-parser-js "^0.5.2" + hyperid "^3.0.0" + lodash.chunk "^4.2.0" + lodash.clonedeep "^4.5.0" + lodash.flatten "^4.4.0" + manage-path "^2.0.0" + on-net-listen "^1.1.1" + pretty-bytes "^5.4.1" + progress "^2.0.3" + reinterval "^1.1.0" + retimer "^3.0.0" + semver "^7.3.2" + subarg "^1.0.0" + timestring "^6.0.0" + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2580,7 +2626,7 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== -base64-js@^1.0.2, base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2890,6 +2936,11 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +char-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" + integrity sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -2962,6 +3013,15 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-table3@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -3009,6 +3069,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colorette@2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -3139,6 +3204,11 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-argv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-2.0.0.tgz#2e7907ba3246f82c967623a3e8525925bbd6c0ad" + integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg== + cross-fetch@^3.1.4, cross-fetch@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -4323,6 +4393,11 @@ hardhat@^2.1.2: uuid "^8.3.2" ws "^7.4.6" +has-async-hooks@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-async-hooks/-/has-async-hooks-1.0.0.tgz#3df965ade8cd2d9dbfdacfbca3e0a5152baaf204" + integrity sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4374,6 +4449,20 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hdr-histogram-js@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.0.tgz#8e2d9a68e3313147804c47d85a9c22a93f85e24b" + integrity sha512-/EpvQI2/Z98mNFYEnlqJ8Ogful8OpArLG/6Tf2bPnkutBVLIeMVNHjk1ZDfshF2BUweipzbk+dB1hgSB7SIakw== + dependencies: + "@assemblyscript/loader" "^0.19.21" + base64-js "^1.2.0" + pako "^1.0.3" + +hdr-histogram-percentiles-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" + integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -4422,6 +4511,11 @@ http-https@^1.0.0: resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" integrity sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg== +http-parser-js@^0.5.2: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + http-status-codes@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be" @@ -4442,6 +4536,14 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +hyperid@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-3.1.1.tgz#50fe8a75ff3ada74dacaf5a3761fb031bdf541c7" + integrity sha512-RveV33kIksycSf7HLkq1sHB5wW0OwuX8ot8MYnY++gaaPXGFfKpBncHrAWxdpuEeRlazUMGWefwP1w6o6GaumA== + dependencies: + uuid "^8.3.2" + uuid-parse "^1.1.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4899,6 +5001,21 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + lodash.isequal@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -4963,6 +5080,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +manage-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" + integrity sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A== + mcl-wasm@^0.7.1: version "0.7.9" resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f" @@ -5100,7 +5222,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -5349,6 +5471,11 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-net-listen@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/on-net-listen/-/on-net-listen-1.1.2.tgz#671e55a81c910fa7e5b1e4d506545e9ea0f2e11c" + integrity sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5455,6 +5582,11 @@ packet-reader@1.0.0: resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== +pako@^1.0.3: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -5723,6 +5855,11 @@ prettier@^2.8.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +pretty-bytes@^5.4.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5743,6 +5880,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + proxy-addr@^2.0.7, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -5940,6 +6082,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +reinterval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" + integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5986,6 +6133,11 @@ ret@~0.2.0: resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== +retimer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df" + integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -6135,7 +6287,7 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.7, semver@^7.3.8: +semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -6404,6 +6556,13 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1. resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + integrity sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg== + dependencies: + minimist "^1.1.0" + superagent@^8.0.5: version "8.0.9" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" @@ -6516,6 +6675,11 @@ timed-out@^4.0.1: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== +timestring@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" + integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== + tiny-invariant@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -6774,6 +6938,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid-parse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" + integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== + uuid@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"