From de2f5544e664de8efa380c0b8fc4f13f0d6c890e Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Tue, 8 Oct 2024 15:27:29 +0300 Subject: [PATCH] Merge branch 'main' into add-helper-method-for-overriding-env-variables Signed-off-by: Victor Yanev # Conflicts: # packages/relay/tests/lib/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.spec.ts # packages/relay/tests/lib/repositories/hbarLimiter/hbarSpendingPlanRepository.spec.ts --- docs/configuration.md | 77 +++--- docs/design/hbar-limiter.md | 136 +++++++++- packages/relay/src/lib/clients/sdkClient.ts | 1 - .../entities/hbarLimiter/hbarSpendingPlan.ts | 4 +- .../ethAddressHbarSpendingPlanRepository.ts | 7 +- .../hbarLimiter/hbarSpendingPlanRepository.ts | 74 +++--- .../ipAddressHbarSpendingPlanRepository.ts | 8 +- .../db/types/hbarLimiter/hbarSpendingPlan.ts | 2 +- packages/relay/src/lib/relay.ts | 2 +- .../lib/services/hbarLimitService/index.ts | 128 +++++----- packages/relay/tests/lib/eth/eth-helpers.ts | 6 +- .../tests/lib/eth/eth_estimateGas.spec.ts | 4 +- .../tests/lib/eth/eth_feeHistory.spec.ts | 4 +- ...eth_getBlockTransactionCountByHash.spec.ts | 4 +- .../relay/tests/lib/eth/eth_getLogs.spec.ts | 4 +- .../tests/lib/eth/eth_getStorageAt.spec.ts | 4 +- ...etTransactionByBlockNumberAndIndex.spec.ts | 4 +- .../lib/eth/eth_getTransactionCount.spec.ts | 4 +- .../lib/eth/eth_getTransactionReceipt.spec.ts | 4 +- .../lib/eth/eth_sendRawTransaction.spec.ts | 6 +- .../relay/tests/lib/ethGetBlockBy.spec.ts | 8 +- .../relay/tests/lib/mirrorNodeClient.spec.ts | 15 +- packages/relay/tests/lib/openrpc.spec.ts | 4 +- packages/relay/tests/lib/poller.spec.ts | 240 +++++++++--------- ...pAddressHbarSpendingPlanRepository.spec.ts | 49 +++- packages/relay/tests/lib/sdkClient.spec.ts | 4 +- .../hbarLimitService/hbarLimitService.spec.ts | 203 ++++++++------- .../metricService/metricService.spec.ts | 2 +- packages/server/src/index.ts | 2 +- .../server/tests/acceptance/index.spec.ts | 1 + .../tests/acceptance/serverConfig.spec.ts | 2 +- .../server/tests/integration/server.spec.ts | 29 +++ packages/ws-server/src/index.ts | 5 +- 33 files changed, 629 insertions(+), 418 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8661f1c9eb..8d5d655067 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,23 +24,22 @@ These properties are noted below and should be custom set per deployment. The following table lists the available properties along with their default values for the [Server package](/packages/server/). Unless you need to set a non-default value, it is recommended to only populate overridden properties in the custom `.env`. -| Name | Default | Description | -| --------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests. | -| `BATCH_REQUESTS_MAX_SIZE` | "100" | Maximum number of requests allowed in a batch. | -| `CHAIN_ID` | "" | The network chain id. Local and previewnet envs should use `0x12a` (298). Previewnet, Testnet and Mainnet should use `0x129` (297), `0x128` (296) and `0x127` (295) respectively. | -| `HBAR_RATE_LIMIT_DURATION` | "80000" | hbar budget limit duration. This creates a timestamp, which resets all limits, when it's reached. Default is to 80000 (80 seconds). | -| `HBAR_RATE_LIMIT_TINYBAR` | "11_000_000_000" | total hbar budget in tinybars (110 hbars). | -| `HEDERA_NETWORK` | "" | Which network to connect to. Automatically populates the main node & mirror node endpoints. Can be `previewnet`, `testnet`, `mainnet` or a map of network IPs -> node accountIds e.g. `{"127.0.0.1:50211":"0.0.3"}` | -| `INPUT_SIZE_LIMIT` | "1mb" | The [koa-jsonrpc](https://github.com/Bitclimb/koa-jsonrpc) maximum size allowed for requests | -| `MAX_BLOCK_RANGE` | "5" | The maximum block number greater than the mirror node's latest block to query for | -| `OPERATOR_ID_MAIN` | "" | Operator account ID used to pay for transactions. In `S.R.N` format, e.g. `0.0.1001`. | -| `OPERATOR_KEY_FORMAT` | "DER" | Operator private key format. Valid types are `DER`, `HEX_ECDSA`, or `HEX_ED25519` | -| `OPERATOR_KEY_MAIN` | "" | Operator private key used to sign transactions in hex encoded DER format. This may be either an ED22519 private key or an ECDSA private key. The private key must be associated with/used to derive `OPERATOR_ID_MAIN`. | -| `RATE_LIMIT_DISABLED` | "false" | Flag to disable IP based rate limiting. | -| `REQUEST_ID_IS_OPTIONAL` | "" | Flag to set it the JSON RPC request id field in the body should be optional. Note, this breaks the API spec and is not advised and is provided for test purposes only where some wallets may be non compliant | -| `SERVER_PORT` | "7546" | The RPC server port number to listen for requests on. Currently a static value defaulting to 7546. See [#955](https://github.com/hashgraph/hedera-json-rpc-relay/issues/955) | -| `SERVER_REQUEST_TIMEOUT_MS` | "60000" | The time of inactivity allowed before a timeout is triggered and the socket is closed. See [NodeJs Server Timeout](https://nodejs.org/api/http.html#serversettimeoutmsecs-callback) | +| Name | Default | Description | +| --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests. | +| `BATCH_REQUESTS_MAX_SIZE` | "100" | Maximum number of requests allowed in a batch. | +| `CHAIN_ID` | "" | The network chain id. Local and previewnet envs should use `0x12a` (298). Previewnet, Testnet and Mainnet should use `0x129` (297), `0x128` (296) and `0x127` (295) respectively. | +| `HEDERA_NETWORK` | "" | Which network to connect to. Automatically populates the main node & mirror node endpoints. Can be `previewnet`, `testnet`, `mainnet` or a map of network IPs -> node accountIds e.g. `{"127.0.0.1:50211":"0.0.3"}` | +| `INPUT_SIZE_LIMIT` | "1mb" | The [koa-jsonrpc](https://github.com/Bitclimb/koa-jsonrpc) maximum size allowed for requests | +| `MAX_BLOCK_RANGE` | "5" | The maximum block number greater than the mirror node's latest block to query for | +| `OPERATOR_ID_MAIN` | "" | Operator account ID used to pay for transactions. In `S.R.N` format, e.g. `0.0.1001`. | +| `OPERATOR_KEY_FORMAT` | "DER" | Operator private key format. Valid types are `DER`, `HEX_ECDSA`, or `HEX_ED25519` | +| `OPERATOR_KEY_MAIN` | "" | Operator private key used to sign transactions in hex encoded DER format. This may be either an ED22519 private key or an ECDSA private key. The private key must be associated with/used to derive `OPERATOR_ID_MAIN`. | +| `RATE_LIMIT_DISABLED` | "false" | Flag to disable IP based rate limiting. | +| `REQUEST_ID_IS_OPTIONAL` | "" | Flag to set it the JSON RPC request id field in the body should be optional. Note, this breaks the API spec and is not advised and is provided for test purposes only where some wallets may be non compliant | +| `SERVER_PORT` | "7546" | The RPC server port number to listen for requests on. Currently a static value defaulting to 7546. See [#955](https://github.com/hashgraph/hedera-json-rpc-relay/issues/955) | +| `SERVER_HOST` | undefined | The hostname or IP address on which the server listens for incoming connections. If `SERVER_HOST` is not configured or left undefined (same as `0.0.0.0`), it permits external connections by default, offering more flexibility. | +| `SERVER_REQUEST_TIMEOUT_MS` | "60000" | The time of inactivity allowed before a timeout is triggered and the socket is closed. See [NodeJs Server Timeout](https://nodejs.org/api/http.html#serversettimeoutmsecs-callback) | ## Relay @@ -48,7 +47,7 @@ The following table lists the available properties along with their default valu Unless you need to set a non-default value, it is recommended to only populate overridden properties in the custom `.env`. | Name | Default | Description | -| ------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +|---------------------------------------------|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `CACHE_MAX` | "1000" | The maximum number (or size) of items that remain in the cache (assuming no TTL pruning or explicit deletions). | | `CACHE_TTL` | "3_600_000" | Max time to live in ms, for items before they are considered stale. Default is one hour in milliseconds | | `CLIENT_TRANSPORT_SECURITY` | "false" | Flag to enable or disable TLS for both networks. | @@ -68,10 +67,15 @@ Unless you need to set a non-default value, it is recommended to only populate o | `ETH_FEE_HISTORY_FIXED` | "true" | Flag to set if eth_feeHistory should return a fixed fee for the set of results. | | `GAS_PRICE_PERCENTAGE_BUFFER` | "0" | The additional buffer that adds a percentage on top of the calculated network gasPrice. This may be used by operators to reduce the chances of `INSUFFICIENT_TX_FEE` errors experienced by users caused by minor fluctuations in the exchange rate. | | `GAS_PRICE_TINY_BAR_BUFFER` | "10000000000" | The additional buffer range to allow during a relay precheck of gas price. This supports slight fluctuations in network gasprice calculations. | +| `HBAR_RATE_LIMIT_DURATION` | "80000" | HBar budget limit duration. This creates a timestamp, which resets all limits, when it's reached. Default is to 80000 (80 seconds). | +| `HBAR_RATE_LIMIT_TINYBAR` | "11_000_000_000" | Total hbar budget in tinybars (110 hbars per 80 seconds). | +| `HBAR_RATE_LIMIT_BASIC` | "92_592_592" | Individual limit in tinybars for spending plans with a BASIC tier. (equivalent of 1_000 HBARs per day for a duration of 80 seconds) | +| `HBAR_RATE_LIMIT_EXTENDED` | "925_925_925" | Individual limit in tinybars for spending plans with a EXTENDED tier. (equivalent of 10_000 HBARs per day for a duration of 80 seconds) | +| `HBAR_RATE_LIMIT_PRIVILEGED` | "1_851_851_850" | Individual limit in tinybars for spending plans with a PRIVILEGED tier. (equivalent of 20_000 HBARs per day for a duration of 80 seconds) | | `HAPI_CLIENT_DURATION_RESET` | "3600000" | Time until client reinitialization. (ms) | | `HAPI_CLIENT_ERROR_RESET` | [21, 50] | Array of status codes, which when encountered will trigger a reinitialization. Status codes are availble [here](https://github.com/hashgraph/hedera-protobufs/blob/main/services/response_code.proto). | | `HAPI_CLIENT_TRANSACTION_RESET` | "50" | Number of transaction executions, until client reinitialization. | -| `TEST_INITIAL_ACCOUNT_STARTING_BALANCE` | "2000" | The number of HBars to allocate to the initial account in acceptance test runs. This account is responsible for the gas payment of tests within the suite run session and needs to be adequately funded. | +| `TEST_INITIAL_ACCOUNT_STARTING_BALANCE` | "2000" | The number of HBars to allocate to the initial account in acceptance test runs. This account is responsible for the gas payment of tests within the suite run session and needs to be adequately funded. | | `LIMIT_DURATION` | "60000" | The maximum duration in ms applied to IP-method based rate limits. | | `MIRROR_NODE_CONTRACT_RESULTS_PG_MAX` | "25" | The maximum number of pages to be requested for contract results from the mirror node. | | `MIRROR_NODE_CONTRACT_RESULTS_LOGS_PG_MAX` | "200" | The maximum number of pages to be requested for contract results logs from the mirror node. (each page will contain a max of 100 results) | @@ -108,23 +112,24 @@ Unless you need to set a non-default value, it is recommended to only populate o The following table lists the available properties along with their default values for the [Ws-server package](/packages/ws-server/). Unless you need to set a non-default value, it is recommended to only populate overridden properties in the custom `.env`. -| Name | Default | Description | -| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `WS_BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests on the websocket server. | -| `WS_BATCH_REQUESTS_MAX_SIZE` | "20" | Maximum number of requests allowed in a batch on websocket server. | -| `SUBSCRIPTIONS_ENABLED` | "false" | If enabled eth_subscribe will be enabled using WebSockets. | -| `WS_MAX_INACTIVITY_TTL` | "300000" | Time in ms that the web socket connection is allowed to stay open without any messages sent or received, currently 5 minutes. | -| `WS_CONNECTION_LIMIT` | "10" | Maximum amount of concurrent web socket connections allowed. | -| `WS_POLLING_INTERVAL` | "500" | Time in ms in between each poll to mirror node while there are subscriptions. | -| `WEB_SOCKET_PORT` | "8546" | Port for the web socket connections | -| `WEB_SOCKET_HTTP_PORT` | "8547" | Port for standard http server, used for metrics and health status endpoints | -| `WS_SUBSCRIPTION_LIMIT` | "10" | Maximum amount of subscriptions per single connection | -| `WS_CONNECTION_LIMIT_PER_IP` | "10" | Maximum amount of connections from a single IP address | -| `WS_MULTIPLE_ADDRESSES_ENABLED` | "false" | If enabled eth_subscribe will allow subscription to multiple contract address. | -| `WS_CACHE_TTL` | "20000" | The time to live for cached entries. | -| `WS_NEW_HEADS_ENABLED`. | "true" | Enables subscriptions for the latest blocks, `newHeads`. | -| `WS_PING_INTERVAL` | "100000" | Interval between ping messages. Set to `0` to disable pinger. | -| `WS_SAME_SUB_FOR_SAME_EVENT` | "true" | The relay will return the same subscription ID when a client subscribes to the same event multiple times using a single connection. When set to false, the relay will always create a new subscription ID for each `eth_subscribe' request. | +| Name | Default | Description | +| ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `WS_BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests on the websocket server. | +| `WS_BATCH_REQUESTS_MAX_SIZE` | "20" | Maximum number of requests allowed in a batch on websocket server. | +| `SUBSCRIPTIONS_ENABLED` | "false" | If enabled eth_subscribe will be enabled using WebSockets. | +| `WS_MAX_INACTIVITY_TTL` | "300000" | Time in ms that the web socket connection is allowed to stay open without any messages sent or received, currently 5 minutes. | +| `WS_CONNECTION_LIMIT` | "10" | Maximum amount of concurrent web socket connections allowed. | +| `WS_POLLING_INTERVAL` | "500" | Time in ms in between each poll to mirror node while there are subscriptions. | +| `WEB_SOCKET_PORT` | "8546" | Port for the web socket connections | +| `WEB_SOCKET_HOST` | "localhost" | The hostname or IP address on which the server will listen for incoming connections. | +| `WEB_SOCKET_HTTP_PORT` | "8547" | Port for standard http server, used for metrics and health status endpoints | +| `WS_SUBSCRIPTION_LIMIT` | "10" | Maximum amount of subscriptions per single connection | +| `WS_CONNECTION_LIMIT_PER_IP` | "10" | Maximum amount of connections from a single IP address | +| `WS_MULTIPLE_ADDRESSES_ENABLED` | "false" | If enabled eth_subscribe will allow subscription to multiple contract address. | +| `WS_CACHE_TTL` | "20000" | The time to live for cached entries. | +| `WS_NEW_HEADS_ENABLED`. | "true" | Enables subscriptions for the latest blocks, `newHeads`. | +| `WS_PING_INTERVAL` | "100000" | Interval between ping messages. Set to `0` to disable pinger. | +| `WS_SAME_SUB_FOR_SAME_EVENT` | "true" | The relay will return the same subscription ID when a client subscribes to the same event multiple times using a single connection. When set to false, the relay will always create a new subscription ID for each `eth_subscribe` request. | ## Sample for connecting to Hedera Environments diff --git a/docs/design/hbar-limiter.md b/docs/design/hbar-limiter.md index 44864ddbc2..9eedc48242 100644 --- a/docs/design/hbar-limiter.md +++ b/docs/design/hbar-limiter.md @@ -19,6 +19,10 @@ - [HBar Allocation Strategy](#hbar-allocation-strategy) - [Metrics to Track](#metrics-to-track) - [Allocation Algorithm](#allocation-algorithm) + - [Configurations](#configurations) + - [Pre-populating the Cache with Spending Plans for Supported Projects and Partner Projects](#pre-populating-the-cache-with-spending-plans-for-supported-projects-and-partner-projects) + - [Spending Limits of Different Tiers](#spending-limits-of-different-tiers) + - [Total Budget and Limit Duration](#total-budget-and-limit-duration) - [Additional Considerations](#additional-considerations) - [Performance](#performance) - [Monitoring and logging](#monitoring-and-logging) @@ -77,20 +81,43 @@ The purpose of the HBar Limiter is to track and control the spending of HBars in The HBar limiter will be implemented as a separate service, used by other services/classes that need it. It will have two main purposes - to capture the gas fees for different operation and to check if an operation needs to be paused, due to an exceeded HBar limit. +### General Users (BASIC tier): + +**NOTE:** Each general user will have a unique spending plan, linked both to their ETH and IP addresses. Each new user will be automatically assigned a BASIC spending plan when they send their first transaction and this plan will remain linked to them for any subsequent requests. + +```mermaid +flowchart TD + A[User] -->|sends transaction| B[JSON-RPC Relay] + B --> C[Estimate fees which will be paid by the relay operator] + C --> D{HBAR Limiter} + D -->|new user, i.e., who is not linked to a spending plan| E[Create a new BASIC spending plan] + E --> F[Link user's ETH & IP addresses to plan] + D -->|existing user, i.e., who is linked to a spending plan| G[Retrieve spending plan linked to user] + F --> H{Plan has enough balance to cover fees} + G --> H + H --> |no| I[Limit request] + H --> |yes| J[Execute transaction] + J --> K[Capture all fees the operator has been charged during execution] + K --> L[Update the spending plan's remaining balance] +``` + +### Supported Projects (EXTENDED tier) and Trusted Partners (PRIVILEGED tier): + +**NOTE:** There will be one spending plan per project/partner with a total spending limit, shared amongst a group of users (IP and ETH addresses) linked to that plan. This means that they will share a common total spending limit for the project/partner. + +All users associated with a project/partner will be pre-configured in the relay as shown in the + ```mermaid flowchart TD A[User] -->|sends transaction| B[JSON-RPC Relay] - B --> C{HbarLimitService} - C -->|new user, i.e., who is not linked to a spending plan| D[Create a BASIC HbarSpendingPlan] - D --> E[Link user's ETH & IP addresses to plan] - E --> F[Estimate fees of any additional HFS transactions which need to be executed by the operator] - C -->|existing user, i.e., who is linked to a spending plan| G[Retrieve HbarSpendingPlan linked to user] - G --> F - F --> H{The plan exceeds its daily HBar allowance?} - H --> |yes| I[Limit request] - H --> |no| J[Execute transaction] - J --> K[Capture fees the operator has been charged] - K --> L[Update spending plan] + B --> C[Estimate fees which will be paid by the relay operator] + C --> D{HBAR Limiter} + D --> E[Retrieve spending plan linked to user's ETH and/or IP address] + E --> F{Plan has enough balance to cover fees} + F --> |no| G[Limit request] + F --> |yes| H[Execute transaction] + H --> I[Capture all fees the operator has been charged during execution] + I --> J[Update the spending plan's remaining balance] ``` ### Class Diagram @@ -149,7 +176,7 @@ classDiagram -createdAt: Date -active: boolean -spendingHistory: HbarSpendingRecord[] - -spentToday: number + -amountSpent: number } class HbarSpendingRecord { @@ -188,8 +215,8 @@ classDiagram +checkExistsAndActive(id: string): Promise +getSpendingHistory(id: string): Promise +addAmountToSpendingHistory(id: string, amount: number): Promise - +getSpentToday(id: string): Promise - +addAmountToSpentToday(id: string, amount: number): Promise + +getAmountSpent(id: string): Promise + +addToAmountSpent(id: string, amount: number): Promise } class EthAddressHbarSpendingPlanRepository { @@ -271,6 +298,87 @@ c. Current day's usage (increase limits if overall usage is low) - Keep a small portion of the daily budget (e.g., 10%) as a reserve - Use this to accommodate unexpected spikes or high-priority users +## Configurations + +### Pre-populating the Cache with Spending Plans for Supported Projects and Partner Projects + +The following configurations will be used to automatically populate the cache with `HbarSpendingPlan`, `EthAddressHbarSpendingPlan`, and `IPAddressHbarSpendingPlan` entries for the outlined supported projects and partner projects on every start-up of the relay. + +All other users (ETH and IP addresses which are not specified in the configuration file) will be treated as "general users" and will be assigned a basic `HbarSpendingPlan` on their first request and their ETH address and IP address will be linked to that plan for all subsequent requests. + +```json +[ + { + "name": "partner name", + "ethAddresses": ["0x123", "0x124"], + "ipAddresses": ["127.0.0.1", "128.0.0.1"], + "subscriptionType": "PRIVILEGED" + }, + { + "name": "some other partner that has given us only eth addresses", + "ethAddresses": ["0x125", "0x126"], + "subscriptionType": "PRIVILEGED" + }, + { + "name": "supported project name", + "ethAddresses": ["0x127", "0x128"], + "ipAddresses": ["129.0.0.1", "130.0.0.1"], + "subscriptionType": "EXTENDED" + }, + { + "name": "some other supported project that has given us only ip addresses", + "ipAddresses": ["131.0.0.1", "132.0.0.1"], + "subscriptionType": "EXTENDED" + } +] +``` + +On every start-up, the relay will check if these entries are already populated in the cache. If not, it will populate them accordingly. + +The JSON file can also be updated over time to add new supported projects or partner projects, and it will populate only the new entries on the next start-up. + +```json +[ + ..., + { + "name": "new partner name", + "ethAddresses": ["0x129", "0x130"], + "ipAddresses": ["133.0.0.1"], + "subscriptionType": "PRIVILEGED" + } +] +``` + +### Spending Limits of Different Tiers + +The spending limits for different tiers are defined as environment variables: +- `HBAR_RATE_LIMIT_BASIC`: The spending limit (in tinybars) for general users (tier 3) +- `HBAR_RATE_LIMIT_EXTENDED`: The spending limit (in tinybars) for supported projects (tier 2) +- `HBAR_RATE_LIMIT_PRIVILEGED`: The spending limit (in tinybars) for trusted partners (tier 1) + +Example configuration for tiered spending limits: +```dotenv +HBAR_RATE_LIMIT_BASIC=92592592 +HBAR_RATE_LIMIT_EXTENDED=925925925 +HBAR_RATE_LIMIT_PRIVILEGED=1851851850 +``` + +### Total Budget and Limit Duration + +The total budget and the limit duration are defined as environment variables: +- `HBAR_RATE_LIMIT_DURATION`: The time window (in milliseconds) for which both the total budget and the spending limits are applicable. + - On initialization of `HbarLimitService`, a reset timestamp is calculated by adding the `HBAR_RATE_LIMIT_DURATION` to the current timestamp. + - The total budget and spending limits are reset when the current timestamp exceeds the reset timestamp. +- `HBAR_RATE_LIMIT_TINYBAR`: The ceiling (in tinybars) on the total amount of HBARs that can be spent in the limit duration. + - This is the largest bucket from which others pull from. + - If the total amount spent exceeds this limit, all spending is paused until the next reset. + +Example configuration for a total budget of 110 HBARs (11_000_000_000 tinybars) per 80 seconds: +```dotenv +HBAR_RATE_LIMIT_TINYBAR=11000000000 +HBAR_RATE_LIMIT_DURATION=80000 +``` + ## Additional Considerations ### Performance diff --git a/packages/relay/src/lib/clients/sdkClient.ts b/packages/relay/src/lib/clients/sdkClient.ts index 063e4bcf96..cdbb3a7f21 100644 --- a/packages/relay/src/lib/clients/sdkClient.ts +++ b/packages/relay/src/lib/clients/sdkClient.ts @@ -397,7 +397,6 @@ export class SDKClient { * @param {string} originalCallerAddress - The address of the original caller making the request. * @param {number} networkGasPriceInWeiBars - The predefined gas price of the network in weibar. * @param {number} currentNetworkExchangeRateInCents - The exchange rate in cents of the current network. - * @param {string} requestId - The unique identifier for the request. * @returns {Promise<{ txResponse: TransactionResponse; fileId: FileId | null }>} * @throws {SDKClientError} Throws an error if no file ID is created or if the preemptive fee check fails. */ diff --git a/packages/relay/src/lib/db/entities/hbarLimiter/hbarSpendingPlan.ts b/packages/relay/src/lib/db/entities/hbarLimiter/hbarSpendingPlan.ts index 3ec2632e73..e1c72c5c60 100644 --- a/packages/relay/src/lib/db/entities/hbarLimiter/hbarSpendingPlan.ts +++ b/packages/relay/src/lib/db/entities/hbarLimiter/hbarSpendingPlan.ts @@ -28,7 +28,7 @@ export class HbarSpendingPlan implements IDetailedHbarSpendingPlan { createdAt: Date; active: boolean; spendingHistory: HbarSpendingRecord[]; - spentToday: number; + amountSpent: number; constructor(data: IDetailedHbarSpendingPlan) { this.id = data.id; @@ -36,6 +36,6 @@ export class HbarSpendingPlan implements IDetailedHbarSpendingPlan { this.createdAt = new Date(data.createdAt); this.active = data.active; this.spendingHistory = data.spendingHistory?.map((spending) => new HbarSpendingRecord(spending)) || []; - this.spentToday = data.spentToday ?? 0; + this.amountSpent = data.amountSpent ?? 0; } } diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts index c7d39bac5e..00022c1625 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts @@ -27,7 +27,7 @@ import { RequestDetails } from '../../../types'; export class EthAddressHbarSpendingPlanRepository { private readonly collectionKey = 'ethAddressHbarSpendingPlan'; - private readonly threeMonthsInMillis = 90 * 24 * 60 * 60 * 1000; + private readonly oneDayInMillis = 24 * 60 * 60 * 1000; /** * The cache service used for storing data. @@ -68,11 +68,12 @@ export class EthAddressHbarSpendingPlanRepository { * * @param {IEthAddressHbarSpendingPlan} addressPlan - The plan to save. * @param {RequestDetails} requestDetails - The request details for logging and tracking. + * @param {number} ttl - The time-to-live for the cache entry. * @returns {Promise} - A promise that resolves when the ETH address is linked to the plan. */ - async save(addressPlan: IEthAddressHbarSpendingPlan, requestDetails: RequestDetails): Promise { + async save(addressPlan: IEthAddressHbarSpendingPlan, requestDetails: RequestDetails, ttl: number): Promise { const key = this.getKey(addressPlan.ethAddress); - await this.cache.set(key, addressPlan, 'save', requestDetails, this.threeMonthsInMillis); + await this.cache.set(key, addressPlan, 'save', requestDetails, ttl); this.logger.trace(`Saved EthAddressHbarSpendingPlan with address ${addressPlan.ethAddress}`); } diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts index 13a9a46160..10b16f3cd2 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts @@ -31,8 +31,6 @@ import { RequestDetails } from '../../../types'; export class HbarSpendingPlanRepository { private readonly collectionKey = 'hbarSpendingPlan'; - private readonly oneDayInMillis = 24 * 60 * 60 * 1000; - private readonly threeMonthsInMillis = this.oneDayInMillis * 90; /** * The cache service used for storing data. @@ -71,7 +69,7 @@ export class HbarSpendingPlanRepository { } /** - * Gets an HBar spending plan by ID with detailed information (spendingHistory and spentToday). + * Gets an HBar spending plan by ID with detailed information (spendingHistory and amountSpent). * @param {string} id - The ID of the plan. * @param {RequestDetails} requestDetails - The request details for logging and tracking. * @returns {Promise} - The detailed HBar spending plan object. @@ -81,7 +79,7 @@ export class HbarSpendingPlanRepository { return new HbarSpendingPlan({ ...plan, spendingHistory: [], - spentToday: await this.getSpentToday(id, requestDetails), + amountSpent: await this.getAmountSpent(id, requestDetails), }); } @@ -89,20 +87,25 @@ export class HbarSpendingPlanRepository { * Creates a new HBar spending plan. * @param {SubscriptionType} subscriptionType - The subscription type of the plan to create. * @param {RequestDetails} requestDetails - The request details for logging and tracking. + * @param {number} ttl - The time-to-live for the plan in milliseconds. * @returns {Promise} - The created HBar spending plan object. */ - async create(subscriptionType: SubscriptionType, requestDetails: RequestDetails): Promise { + async create( + subscriptionType: SubscriptionType, + requestDetails: RequestDetails, + ttl: number, + ): Promise { const plan: IDetailedHbarSpendingPlan = { id: uuidV4(randomBytes(16)), subscriptionType, createdAt: new Date(), active: true, spendingHistory: [], - spentToday: 0, + amountSpent: 0, }; this.logger.trace(`Creating HbarSpendingPlan with ID ${plan.id}...`); const key = this.getKey(plan.id); - await this.cache.set(key, plan, 'create', requestDetails, this.threeMonthsInMillis); + await this.cache.set(key, plan, 'create', requestDetails, ttl); return new HbarSpendingPlan(plan); } @@ -157,48 +160,51 @@ export class HbarSpendingPlanRepository { } /** - * Gets the amount spent today for an HBar spending plan. - * @param {string} id - The ID of the plan. - * @param {RequestDetails} requestDetails - The request details for logging and tracking. - * @returns {Promise} - A promise that resolves with the amount spent today. + * Gets the amount spent for an HBar spending plan. + * @param id - The ID of the plan. + @param {RequestDetails} requestDetails - The request details for logging and tracking. + * @returns {Promise} - A promise that resolves with the amount spent. */ - async getSpentToday(id: string, requestDetails: RequestDetails): Promise { + async getAmountSpent(id: string, requestDetails: RequestDetails): Promise { await this.checkExistsAndActive(id, requestDetails); - this.logger.trace(`Retrieving spentToday for HbarSpendingPlan with ID ${id}...`); - const key = this.getSpentTodayKey(id); - return this.cache.getAsync(key, 'getSpentToday', requestDetails).then((spentToday) => parseInt(spentToday ?? '0')); + this.logger.trace(`Retrieving amountSpent for HbarSpendingPlan with ID ${id}...`); + const key = this.getAmountSpentKey(id); + return this.cache + .getAsync(key, 'getAmountSpent', requestDetails) + .then((amountSpent) => parseInt(amountSpent ?? '0')); } /** - * Resets the amount spent today for all hbar spending plans. + * Resets the amount spent for all hbar spending plans. * @returns {Promise} - A promise that resolves when the operation is complete. */ - async resetAllSpentTodayEntries(requestDetails: RequestDetails): Promise { - this.logger.trace('Resetting the spentToday entries for all HbarSpendingPlans...'); - const callerMethod = this.resetAllSpentTodayEntries.name; - const keys = await this.cache.keys(`${this.collectionKey}:*:spentToday`, callerMethod, requestDetails); + async resetAmountSpentOfAllPlans(requestDetails: RequestDetails): Promise { + this.logger.trace('Resetting the `amountSpent` entries for all HbarSpendingPlans...'); + const callerMethod = this.resetAmountSpentOfAllPlans.name; + const keys = await this.cache.keys(this.getAmountSpentKey('*'), callerMethod, requestDetails); await Promise.all(keys.map((key) => this.cache.delete(key, callerMethod, requestDetails))); - this.logger.trace(`Successfully reset ${keys.length} spentToday entries for HbarSpendingPlans.`); + this.logger.trace(`Successfully reset ${keys.length} "amountSpent" entries for HbarSpendingPlans.`); } /** - * Adds an amount to the amount spent today for a plan. + * Adds an amount to the amount spent for a plan. * @param {string} id - The ID of the plan. * @param {number} amount - The amount to add. * @param {RequestDetails} requestDetails - The request details for logging and tracking. + * @param {number} ttl - The time-to-live for the amountSpent entry in milliseconds. * @returns {Promise} - A promise that resolves when the operation is complete. */ - async addAmountToSpentToday(id: string, amount: number, requestDetails: RequestDetails): Promise { + async addToAmountSpent(id: string, amount: number, requestDetails: RequestDetails, ttl: number): Promise { await this.checkExistsAndActive(id, requestDetails); - const key = this.getSpentTodayKey(id); - if (!(await this.cache.getAsync(key, 'addAmountToSpentToday', requestDetails))) { - this.logger.trace(`No spending yet for HbarSpendingPlan with ID ${id}, setting spentToday to ${amount}...`); - await this.cache.set(key, amount, 'addAmountToSpentToday', requestDetails, this.oneDayInMillis); + const key = this.getAmountSpentKey(id); + if (!(await this.cache.getAsync(key, 'addToAmountSpent', requestDetails))) { + this.logger.trace(`No spending yet for HbarSpendingPlan with ID ${id}, setting amountSpent to ${amount}...`); + await this.cache.set(key, amount, 'addToAmountSpent', requestDetails, ttl); } else { - this.logger.trace(`Adding ${amount} to spentToday for HbarSpendingPlan with ID ${id}...`); - await this.cache.incrBy(key, amount, 'addAmountToSpentToday', requestDetails); + this.logger.trace(`Adding ${amount} to amountSpent for HbarSpendingPlan with ID ${id}...`); + await this.cache.incrBy(key, amount, 'addToAmountSpent', requestDetails); } } @@ -213,7 +219,7 @@ export class HbarSpendingPlanRepository { requestDetails: RequestDetails, ): Promise { const callerMethod = this.findAllActiveBySubscriptionType.name; - const keys = await this.cache.keys(`${this.collectionKey}:*`, callerMethod, requestDetails); + const keys = await this.cache.keys(this.getKey('*'), callerMethod, requestDetails); const plans = await Promise.all( keys.map((key) => this.cache.getAsync(key, callerMethod, requestDetails)), ); @@ -226,7 +232,7 @@ export class HbarSpendingPlanRepository { ...plan, createdAt: new Date(plan.createdAt), spendingHistory: [], - spentToday: await this.getSpentToday(plan.id, requestDetails), + amountSpent: await this.getAmountSpent(plan.id, requestDetails), }), ), ); @@ -242,12 +248,12 @@ export class HbarSpendingPlanRepository { } /** - * Gets the cache key for the amount spent today for an {@link IHbarSpendingPlan}. + * Gets the cache key for the amount spent for an {@link IHbarSpendingPlan}. * @param id - The ID of the plan to get the key for. * @private */ - private getSpentTodayKey(id: string): string { - return `${this.collectionKey}:${id}:spentToday`; + private getAmountSpentKey(id: string): string { + return `${this.collectionKey}:${id}:amountSpent`; } /** diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts index 82849ec2ed..75957defe1 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts @@ -27,7 +27,6 @@ import { RequestDetails } from '../../../types'; export class IPAddressHbarSpendingPlanRepository { private readonly collectionKey = 'ipAddressHbarSpendingPlan'; - private readonly threeMonthsInMillis = 90 * 24 * 60 * 60 * 1000; /** * The cache service used for storing data. @@ -50,6 +49,7 @@ export class IPAddressHbarSpendingPlanRepository { * Finds an {@link IPAddressHbarSpendingPlan} for an IP address. * * @param {string} ipAddress - The IP address to search for. + * @param {RequestDetails} requestDetails - The request details for logging and tracking. * @returns {Promise} - The associated plan for the IP address. */ async findByAddress(ipAddress: string, requestDetails: RequestDetails): Promise { @@ -66,11 +66,13 @@ export class IPAddressHbarSpendingPlanRepository { * Saves an {@link IPAddressHbarSpendingPlan} to the cache, linking the plan to the IP address. * * @param {IIPAddressHbarSpendingPlan} addressPlan - The plan to save. + * @param {RequestDetails} requestDetails - The request details used for logging and tracking. + * @param {number} ttl - The time-to-live for the cache entry. * @returns {Promise} - A promise that resolves when the IP address is linked to the plan. */ - async save(addressPlan: IIPAddressHbarSpendingPlan, requestDetails: RequestDetails): Promise { + async save(addressPlan: IIPAddressHbarSpendingPlan, requestDetails: RequestDetails, ttl: number): Promise { const key = this.getKey(addressPlan.ipAddress); - await this.cache.set(key, addressPlan, 'save', requestDetails, this.threeMonthsInMillis); + await this.cache.set(key, addressPlan, 'save', requestDetails, ttl); this.logger.trace(`Linked new IP address to HbarSpendingPlan with ID ${addressPlan.planId}`); } diff --git a/packages/relay/src/lib/db/types/hbarLimiter/hbarSpendingPlan.ts b/packages/relay/src/lib/db/types/hbarLimiter/hbarSpendingPlan.ts index 49821ca595..cdf4a5806d 100644 --- a/packages/relay/src/lib/db/types/hbarLimiter/hbarSpendingPlan.ts +++ b/packages/relay/src/lib/db/types/hbarLimiter/hbarSpendingPlan.ts @@ -30,5 +30,5 @@ export interface IHbarSpendingPlan { export interface IDetailedHbarSpendingPlan extends IHbarSpendingPlan { spendingHistory: IHbarSpendingRecord[]; - spentToday: number; + amountSpent: number; } diff --git a/packages/relay/src/lib/relay.ts b/packages/relay/src/lib/relay.ts index 92597e2cfe..48d603d9d5 100644 --- a/packages/relay/src/lib/relay.ts +++ b/packages/relay/src/lib/relay.ts @@ -123,7 +123,7 @@ export class RelayImpl implements Relay { const chainId = prepend0x(Number(configuredChainId).toString(16)); const duration = constants.HBAR_RATE_LIMIT_DURATION; - const total = constants.HBAR_RATE_LIMIT_TINYBAR; + const total = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); const hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, register); this.eventEmitter = new EventEmitter(); diff --git a/packages/relay/src/lib/services/hbarLimitService/index.ts b/packages/relay/src/lib/services/hbarLimitService/index.ts index a791c75024..1758796f22 100644 --- a/packages/relay/src/lib/services/hbarLimitService/index.ts +++ b/packages/relay/src/lib/services/hbarLimitService/index.ts @@ -26,15 +26,15 @@ import { IDetailedHbarSpendingPlan } from '../../db/types/hbarLimiter/hbarSpendi import { HbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/hbarSpendingPlanRepository'; import { EthAddressHbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository'; import { IPAddressHbarSpendingPlanRepository } from '../../db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository'; -import { RequestDetails } from '../../types/RequestDetails'; +import { RequestDetails } from '../../types'; +import constants from '../../constants'; +import { Hbar } from '@hashgraph/sdk'; export class HbarLimitService implements IHbarLimitService { - static readonly ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; - // TODO: Replace with actual values - static readonly DAILY_LIMITS: Record = { - BASIC: parseInt(process.env.HBAR_DAILY_LIMIT_BASIC ?? '1000'), - EXTENDED: parseInt(process.env.HBAR_DAILY_LIMIT_EXTENDED ?? '10000'), - PRIVILEGED: parseInt(process.env.HBAR_DAILY_LIMIT_PRIVILEGED ?? '100000'), + static readonly TIER_LIMITS: Record = { + BASIC: Hbar.fromTinybars(constants.HBAR_RATE_LIMIT_BASIC), + EXTENDED: Hbar.fromTinybars(constants.HBAR_RATE_LIMIT_EXTENDED), + PRIVILEGED: Hbar.fromTinybars(constants.HBAR_RATE_LIMIT_PRIVILEGED), }; /** @@ -70,7 +70,7 @@ export class HbarLimitService implements IHbarLimitService { * The remaining budget for the rate limiter. * @private */ - private remainingBudget: number; + private remainingBudget: Hbar; /** * The reset timestamp for the rate limiter. @@ -84,8 +84,12 @@ export class HbarLimitService implements IHbarLimitService { private readonly ipAddressHbarSpendingPlanRepository: IPAddressHbarSpendingPlanRepository, private readonly logger: Logger, private readonly register: Registry, - private readonly totalBudget: number, + private readonly totalBudget: Hbar, + private readonly limitDuration: number, ) { + this.reset = this.getResetTimestamp(); + this.remainingBudget = this.totalBudget; + const metricCounterName = 'rpc_relay_hbar_rate_limit'; this.register.removeSingleMetric(metricCounterName); this.hbarLimitCounter = new Counter({ @@ -103,8 +107,7 @@ export class HbarLimitService implements IHbarLimitService { help: 'Relay Hbar rate limit remaining budget', registers: [register], }); - this.hbarLimitRemainingGauge.set(this.totalBudget); - this.remainingBudget = this.totalBudget; + this.hbarLimitRemainingGauge.set(this.remainingBudget.toTinybars().toNumber()); this.dailyUniqueSpendingPlansCounter = Object.values(SubscriptionType).reduce( (acc, type) => { @@ -133,21 +136,17 @@ export class HbarLimitService implements IHbarLimitService { }, {} as Record, ); - - // Reset the rate limiter at the start of the next day - this.reset = this.getResetTimestamp(); } /** - * Resets the {@link HbarSpendingPlan#spentToday} field for all existing plans. - * @param {string} [requestId] - An optional unique request ID for tracking the request. + * Resets the {@link HbarSpendingPlan#amountSpent} field for all existing plans. + * @param {RequestDetails} requestDetails - The request details used for logging and tracking. * @returns {Promise} - A promise that resolves when the operation is complete. */ async resetLimiter(requestDetails: RequestDetails): Promise { this.logger.trace(`${requestDetails.formattedRequestId} Resetting HBAR rate limiter...`); - await this.hbarSpendingPlanRepository.resetAllSpentTodayEntries(requestDetails); + await this.hbarSpendingPlanRepository.resetAmountSpentOfAllPlans(requestDetails); this.resetBudget(); - this.resetMetrics(); this.reset = this.getResetTimestamp(); this.logger.trace( `${requestDetails.formattedRequestId} HBAR Rate Limit reset: remainingBudget=${this.remainingBudget}, newResetTimestamp=${this.reset}`, @@ -159,9 +158,8 @@ export class HbarLimitService implements IHbarLimitService { * @param {string} mode - The mode of the transaction or request. * @param {string} methodName - The name of the method being invoked. * @param {string} ethAddress - The eth address to check. - * @param {string} [ipAddress] - The ip address to check. - * @param {number} [estimatedTxFee] - The total estimated transaction fee, default to 0. * @param {RequestDetails} requestDetails The request details for logging and tracking. + * @param {number} [estimatedTxFee] - The total estimated transaction fee, default to 0. * @returns {Promise} - A promise that resolves with a boolean indicating if the address should be limited. */ async shouldLimit( @@ -187,13 +185,14 @@ export class HbarLimitService implements IHbarLimitService { spendingPlan = await this.createBasicSpendingPlan(ethAddress, requestDetails); } - const dailyLimit = HbarLimitService.DAILY_LIMITS[spendingPlan.subscriptionType]; + const spendingLimit = HbarLimitService.TIER_LIMITS[spendingPlan.subscriptionType].toTinybars(); - const exceedsLimit = spendingPlan.spentToday >= dailyLimit || spendingPlan.spentToday + estimatedTxFee > dailyLimit; + const exceedsLimit = + spendingLimit.lte(spendingPlan.amountSpent) || spendingLimit.lt(spendingPlan.amountSpent + estimatedTxFee); this.logger.trace( - `${requestDetails.formattedRequestId} ${user} ${exceedsLimit ? 'should' : 'should not'} be limited, spentToday=${ - spendingPlan.spentToday - }, estimatedTxFee=${estimatedTxFee}, dailyLimit=${dailyLimit}`, + `${requestDetails.formattedRequestId} ${user} ${exceedsLimit ? 'should' : 'should not'} be limited: amountSpent=${ + spendingPlan.amountSpent + }, estimatedTxFee=${estimatedTxFee} tℏ, spendingLimit=${spendingLimit.toString()} tℏ`, ); return exceedsLimit; } @@ -202,7 +201,6 @@ export class HbarLimitService implements IHbarLimitService { * Add expense to the remaining budget. * @param {number} cost - The cost of the expense. * @param {string} ethAddress - The Ethereum address to add the expense to. - * @param {string} ipAddress - The optional IP address to add the expense to. * @param {RequestDetails} requestDetails The request details for logging and tracking. * @returns {Promise} - A promise that resolves when the expense has been added. */ @@ -221,23 +219,23 @@ export class HbarLimitService implements IHbarLimitService { this.logger.trace( `${requestDetails.formattedRequestId} Adding expense of ${cost} to spending plan with ID ${ spendingPlan.id - }, new spentToday=${spendingPlan.spentToday + cost}`, + }, new amountSpent=${spendingPlan.amountSpent + cost}`, ); // Check if the spending plan is being used for the first time today - if (spendingPlan.spentToday === 0) { + if (spendingPlan.amountSpent === 0) { this.dailyUniqueSpendingPlansCounter[spendingPlan.subscriptionType].inc(1); } - await this.hbarSpendingPlanRepository.addAmountToSpentToday(spendingPlan.id, cost, requestDetails); - this.remainingBudget -= cost; - this.hbarLimitRemainingGauge.set(this.remainingBudget); + await this.hbarSpendingPlanRepository.addToAmountSpent(spendingPlan.id, cost, requestDetails, this.limitDuration); + this.remainingBudget = Hbar.fromTinybars(this.remainingBudget.toTinybars().sub(cost)); + this.hbarLimitRemainingGauge.set(this.remainingBudget.toTinybars().toNumber()); // Done asynchronously in the background this.updateAverageDailyUsagePerSubscriptionType(spendingPlan.subscriptionType, requestDetails).then(); this.logger.trace( - `${requestDetails.formattedRequestId} HBAR rate limit expense update: cost=${cost}, remainingBudget=${this.remainingBudget}`, + `${requestDetails.formattedRequestId} HBAR rate limit expense update: cost=${cost} tℏ, remainingBudget=${this.remainingBudget}`, ); } @@ -259,15 +257,15 @@ export class HbarLimitService implements IHbarLimitService { if (this.shouldResetLimiter()) { await this.resetLimiter(requestDetails); } - if (this.remainingBudget <= 0 || this.remainingBudget - estimatedTxFee < 0) { + if (this.remainingBudget.toTinybars().lte(0) || this.remainingBudget.toTinybars().sub(estimatedTxFee).lt(0)) { this.hbarLimitCounter.labels(mode, methodName).inc(1); this.logger.warn( - `${requestDetails.formattedRequestId} HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${this.totalBudget}, resetTimestamp=${this.reset}`, + `${requestDetails.formattedRequestId} HBAR rate limit incoming call: remainingBudget=${this.remainingBudget}, totalBudget=${this.totalBudget}, estimatedTxFee=${estimatedTxFee} tℏ, resetTimestamp=${this.reset}`, ); return true; } else { this.logger.trace( - `${requestDetails.formattedRequestId} HBAR rate limit not reached. ${this.remainingBudget} out of ${this.totalBudget} tℏ left in relay budget until ${this.reset}.`, + `${requestDetails.formattedRequestId} HBAR rate limit not reached: remainingBudget=${this.remainingBudget}, totalBudget=${this.totalBudget}, estimatedTxFee=${estimatedTxFee} tℏ, resetTimestamp=${this.reset}.`, ); return false; } @@ -287,7 +285,7 @@ export class HbarLimitService implements IHbarLimitService { subscriptionType, requestDetails, ); - const totalUsage = plans.reduce((total, plan) => total + plan.spentToday, 0); + const totalUsage = plans.reduce((total, plan) => total + plan.amountSpent, 0); const averageUsage = Math.round(totalUsage / plans.length); this.averageDailySpendingPlanUsagesGauge[subscriptionType].set(averageUsage); } @@ -307,34 +305,37 @@ export class HbarLimitService implements IHbarLimitService { */ private resetBudget(): void { this.remainingBudget = this.totalBudget; - this.hbarLimitRemainingGauge.set(this.remainingBudget); + this.hbarLimitRemainingGauge.set(this.remainingBudget.toTinybars().toNumber()); } /** - * Resets the metrics that track daily unique spending plans and average daily spending plan usages. - * @private + * Calculates the next reset timestamp for the rate limiter. + * + * This method determines the next reset timestamp based on the current reset timestamp + * and the limit duration. If the current reset timestamp is not defined, it initializes + * the reset timestamp to midnight of the current day. It then iteratively adds the limit + * duration to the reset timestamp until it is in the future. + * + * @returns {Date} - The next reset timestamp. */ - private resetMetrics(): void { - for (const subscriptionType of Object.values(SubscriptionType)) { - this.dailyUniqueSpendingPlansCounter[subscriptionType].reset(); - this.averageDailySpendingPlanUsagesGauge[subscriptionType].reset(); + private getResetTimestamp(): Date { + const todayAtMidnight = new Date().setHours(0, 0, 0, 0); + + let resetDate = this.reset ? new Date(this.reset.getTime()) : new Date(todayAtMidnight); + while (resetDate.getTime() < Date.now()) { + // 1. Calculate the difference between the current time and the reset time. + // 2. Determine how many intervals of size `limitDuration` have passed since the last reset. + // 3. Calculate the new reset date by adding the required intervals to the original reset date. + const intervalsPassed = Math.ceil((Date.now() - resetDate.getTime()) / this.limitDuration); + resetDate = new Date(resetDate.getTime() + intervalsPassed * this.limitDuration); } - } - /** - * Gets a new reset timestamp for the rate limiter. - * @returns {Date} - The new reset timestamp. - * @private - */ - private getResetTimestamp(): Date { - const tomorrow = new Date(Date.now() + HbarLimitService.ONE_DAY_IN_MILLIS); - return new Date(tomorrow.setHours(0, 0, 0, 0)); + return resetDate; } /** * Gets the spending plan for the given eth address or ip address. * @param {string} ethAddress - The eth address to get the spending plan for. - * @param {string} ipAddress - The ip address to get the spending plan for. * @param {RequestDetails} requestDetails - The request details for logging and tracking. * @returns {Promise} - A promise that resolves with the spending plan or null if none exists. * @private @@ -384,7 +385,7 @@ export class HbarLimitService implements IHbarLimitService { /** * Gets the spending plan for the given IP address. - * @param {string} ipAddress - The IP address to get the spending plan for. + * @param {RequestDetails} requestDetails - The request details for logging and tracking. * @returns {Promise} - A promise that resolves with the spending plan. * @private */ @@ -400,7 +401,6 @@ export class HbarLimitService implements IHbarLimitService { /** * Creates a basic spending plan for the given eth address. * @param {string} ethAddress - The eth address to create the spending plan for. - * @param {string} ipAddress - The ip address to create the spending plan for. * @param {RequestDetails} requestDetails - The request details for logging and tracking. * @returns {Promise} - A promise that resolves with the created spending plan. * @private @@ -414,18 +414,30 @@ export class HbarLimitService implements IHbarLimitService { throw new Error('Cannot create a spending plan without an associated eth address or ip address'); } - const spendingPlan = await this.hbarSpendingPlanRepository.create(SubscriptionType.BASIC, requestDetails); + const spendingPlan = await this.hbarSpendingPlanRepository.create( + SubscriptionType.BASIC, + requestDetails, + this.limitDuration, + ); if (ethAddress) { this.logger.trace( `${requestDetails.formattedRequestId} Linking spending plan with ID ${spendingPlan.id} to eth address ${ethAddress}`, ); - await this.ethAddressHbarSpendingPlanRepository.save({ ethAddress, planId: spendingPlan.id }, requestDetails); + await this.ethAddressHbarSpendingPlanRepository.save( + { ethAddress, planId: spendingPlan.id }, + requestDetails, + this.limitDuration, + ); } if (ipAddress) { this.logger.trace( `${requestDetails.formattedRequestId} Linking spending plan with ID ${spendingPlan.id} to ip address ${ipAddress}`, ); - await this.ipAddressHbarSpendingPlanRepository.save({ ipAddress, planId: spendingPlan.id }, requestDetails); + await this.ipAddressHbarSpendingPlanRepository.save( + { ipAddress, planId: spendingPlan.id }, + requestDetails, + this.limitDuration, + ); } return spendingPlan; } diff --git a/packages/relay/tests/lib/eth/eth-helpers.ts b/packages/relay/tests/lib/eth/eth-helpers.ts index 68c8b99066..8301421bef 100644 --- a/packages/relay/tests/lib/eth/eth-helpers.ts +++ b/packages/relay/tests/lib/eth/eth-helpers.ts @@ -27,6 +27,7 @@ import HAPIService from '../../../src/lib/services/hapiService/hapiService'; import MockAdapter from 'axios-mock-adapter'; import { MirrorNodeClient } from '../../../src/lib/clients/mirrorNodeClient'; import { EthImpl } from '../../../src/lib/eth'; +import EventEmitter from 'events'; export function contractResultsByNumberByIndexURL(number: number, index: number): string { return `contracts/results?block.number=${number}&transaction.index=${index}&limit=100&order=asc`; @@ -60,10 +61,11 @@ export function generateEthTestEnv(fixedFeeHistory = false) { const web3Mock = new MockAdapter(mirrorNodeInstance.getMirrorNodeWeb3Instance(), { onNoMatch: 'throwException' }); const duration = constants.HBAR_RATE_LIMIT_DURATION; - const total = constants.HBAR_RATE_LIMIT_TINYBAR; + const total = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); const hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, registry); + const eventEmitter = new EventEmitter(); - const hapiServiceInstance = new HAPIService(logger, registry, hbarLimiter, cacheService); + const hapiServiceInstance = new HAPIService(logger, registry, hbarLimiter, cacheService, eventEmitter); // @ts-ignore const ethImpl = new EthImpl(hapiServiceInstance, mirrorNodeInstance, logger, '0x12a', registry, cacheService); diff --git a/packages/relay/tests/lib/eth/eth_estimateGas.spec.ts b/packages/relay/tests/lib/eth/eth_estimateGas.spec.ts index ea9de7dcbc..228cd7cce4 100644 --- a/packages/relay/tests/lib/eth/eth_estimateGas.spec.ts +++ b/packages/relay/tests/lib/eth/eth_estimateGas.spec.ts @@ -83,9 +83,9 @@ describe('@ethEstimateGas Estimate Gas spec', async function () { TX_DEFAULT_GAS: defaultGasOverride.toString(), }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = createStubInstance(SDKClient); getSdkClientStub = stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub); diff --git a/packages/relay/tests/lib/eth/eth_feeHistory.spec.ts b/packages/relay/tests/lib/eth/eth_feeHistory.spec.ts index 53323cd97f..7b6c1a76a4 100644 --- a/packages/relay/tests/lib/eth/eth_feeHistory.spec.ts +++ b/packages/relay/tests/lib/eth/eth_feeHistory.spec.ts @@ -55,9 +55,9 @@ describe('@ethFeeHistory using MirrorNode', async function () { overrideEnvsInMochaDescribe({ ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE: '1' }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); getSdkClientStub = sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub); diff --git a/packages/relay/tests/lib/eth/eth_getBlockTransactionCountByHash.spec.ts b/packages/relay/tests/lib/eth/eth_getBlockTransactionCountByHash.spec.ts index e626de8bbb..bcecf9cdbb 100644 --- a/packages/relay/tests/lib/eth/eth_getBlockTransactionCountByHash.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getBlockTransactionCountByHash.spec.ts @@ -50,9 +50,9 @@ describe('@ethGetBlockTransactionCountByHash using MirrorNode', async function ( ipAddress: '0.0.0.0', }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); diff --git a/packages/relay/tests/lib/eth/eth_getLogs.spec.ts b/packages/relay/tests/lib/eth/eth_getLogs.spec.ts index fa66fc7853..90e39e3cf4 100644 --- a/packages/relay/tests/lib/eth/eth_getLogs.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getLogs.spec.ts @@ -97,9 +97,9 @@ describe('@ethGetLogs using MirrorNode', async function () { overrideEnvsInMochaDescribe({ ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE: '1' }); - beforeEach(() => { + beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); diff --git a/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts b/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts index fd170e72f8..1f69221c55 100644 --- a/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts @@ -74,9 +74,9 @@ describe('@ethGetStorageAt eth_getStorageAt spec', async function () { overrideEnvsInMochaDescribe({ ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE: '1' }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); diff --git a/packages/relay/tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts b/packages/relay/tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts index 82c274e719..26483cf71b 100644 --- a/packages/relay/tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getTransactionByBlockNumberAndIndex.spec.ts @@ -81,9 +81,9 @@ describe('@ethGetTransactionByBlockNumberAndIndex using MirrorNode', async funct ipAddress: '0.0.0.0', }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); getSdkClientStub = sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub); diff --git a/packages/relay/tests/lib/eth/eth_getTransactionCount.spec.ts b/packages/relay/tests/lib/eth/eth_getTransactionCount.spec.ts index 9892929384..4fd4cf5ab0 100644 --- a/packages/relay/tests/lib/eth/eth_getTransactionCount.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getTransactionCount.spec.ts @@ -95,11 +95,11 @@ describe('@ethGetTransactionCount eth_getTransactionCount spec', async function .reply(200, { transactions: [{ transaction_id: transactionId }, {}] }); }); - this.afterEach(() => { + this.afterEach(async () => { getSdkClientStub.restore(); restMock.resetHandlers(); // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); }); diff --git a/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts b/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts index 4676593171..44df551711 100644 --- a/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts @@ -140,10 +140,10 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func sandbox.stub(ethImpl, 'getFeeWeibars').resolves(gasPrice); }; - this.afterEach(() => { + this.afterEach(async () => { restMock.resetHandlers(); sandbox.restore(); - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); }); it('returns `null` for non-existent hash', async function () { diff --git a/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts b/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts index 9744675fc7..6ac2c3ed0d 100644 --- a/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts +++ b/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts @@ -43,13 +43,13 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () this.timeout(10000); let { restMock, hapiServiceInstance, ethImpl, cacheService } = generateEthTestEnv(); - const requestDetails = new RequestDetails({ requestId: 'testId', ipAddress: '0.0.0.0' }); + const requestDetails = new RequestDetails({ requestId: 'eth_sendRawTransactionTest', ipAddress: '0.0.0.0' }); overrideEnvsInMochaDescribe({ ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE: '1' }); - this.beforeEach(() => { + this.beforeEach(async () => { // reset cache and restMock - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); restMock.reset(); sdkClientStub = sinon.createStubInstance(SDKClient); getSdkClientStub = sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub); diff --git a/packages/relay/tests/lib/ethGetBlockBy.spec.ts b/packages/relay/tests/lib/ethGetBlockBy.spec.ts index 5de94696bb..e7cbf87037 100644 --- a/packages/relay/tests/lib/ethGetBlockBy.spec.ts +++ b/packages/relay/tests/lib/ethGetBlockBy.spec.ts @@ -36,6 +36,7 @@ import { nanOrNumberTo0x, nullableNumberTo0x, numberTo0x, toHash32 } from '../.. import { CacheService } from '../../src/lib/services/cacheService/cacheService'; import { defaultDetailedContractResults, overrideEnvsInMochaDescribe, useInMemoryRedisServer } from '../helpers'; import { EventEmitter } from 'events'; +import { RequestDetails } from '../../src/lib/types'; dotenv.config({ path: path.resolve(__dirname, '../test.env') }); @@ -117,8 +118,9 @@ describe('eth_getBlockBy', async function () { this.timeout(10000); let ethImpl: EthImpl; - useInMemoryRedisServer(logger, 5031); + const requestDetails = new RequestDetails({ requestId: 'ethGetBlockByTest', ipAddress: '0.0.0.0' }); + useInMemoryRedisServer(logger, 5031); overrideEnvsInMochaDescribe({ ETH_FEE_HISTORY_FIXED: 'false' }); this.beforeAll(async () => { @@ -145,8 +147,8 @@ describe('eth_getBlockBy', async function () { ethImpl = new EthImpl(hapiServiceInstance, mirrorNodeInstance, logger, '0x12a', registry, cacheService); }); - this.beforeEach(() => { - cacheService.clear(); + this.beforeEach(async () => { + await cacheService.clear(requestDetails); restMock.reset(); }); diff --git a/packages/relay/tests/lib/mirrorNodeClient.spec.ts b/packages/relay/tests/lib/mirrorNodeClient.spec.ts index 06a5f50d67..78d1ca9faf 100644 --- a/packages/relay/tests/lib/mirrorNodeClient.spec.ts +++ b/packages/relay/tests/lib/mirrorNodeClient.spec.ts @@ -37,15 +37,14 @@ import { BigNumber } from 'bignumber.js'; dotenv.config({ path: path.resolve(__dirname, '../test.env') }); -const registry = new Registry(); - -const logger = pino(); -const noTransactions = '?transactions=false'; -const requestDetails = new RequestDetails({ requestId: getRequestId(), ipAddress: '0.0.0.0' }); - describe('MirrorNodeClient', async function () { this.timeout(20000); + const registry = new Registry(); + const logger = pino(); + const noTransactions = '?transactions=false'; + const requestDetails = new RequestDetails({ requestId: 'mirrorNodeClientTest', ipAddress: '0.0.0.0' }); + let instance: AxiosInstance, mock: MockAdapter, mirrorNodeInstance: MirrorNodeClient, cacheService: CacheService; before(() => { @@ -68,9 +67,9 @@ describe('MirrorNodeClient', async function () { ); }); - beforeEach(() => { + beforeEach(async () => { mock = new MockAdapter(instance); - cacheService.clear(requestDetails); + await cacheService.clear(requestDetails); }); describe('handleError', async () => { diff --git a/packages/relay/tests/lib/openrpc.spec.ts b/packages/relay/tests/lib/openrpc.spec.ts index 3d755aa5e9..a335c17315 100644 --- a/packages/relay/tests/lib/openrpc.spec.ts +++ b/packages/relay/tests/lib/openrpc.spec.ts @@ -96,7 +96,7 @@ describe('Open RPC Specification', function () { let methodsResponseSchema: { [method: string]: any }; let ethImpl: EthImpl; - const requestDetails = new RequestDetails({ requestId: 'testId', ipAddress: '0.0.0.0' }); + const requestDetails = new RequestDetails({ requestId: 'openRpcTest', ipAddress: '0.0.0.0' }); this.beforeAll(async () => { rpcDocument = await parseOpenRPCDocument(JSON.stringify(openRpcSchema)); @@ -130,7 +130,7 @@ describe('Open RPC Specification', function () { instance, ); const duration = constants.HBAR_RATE_LIMIT_DURATION; - const total = constants.HBAR_RATE_LIMIT_TINYBAR; + const total = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); const hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, registry); const eventEmitter = new EventEmitter(); diff --git a/packages/relay/tests/lib/poller.spec.ts b/packages/relay/tests/lib/poller.spec.ts index 82681de4b4..8169ff9e1f 100644 --- a/packages/relay/tests/lib/poller.spec.ts +++ b/packages/relay/tests/lib/poller.spec.ts @@ -34,132 +34,128 @@ describe('Polling', async function () { 'Poller: Fetching data for tag: {"event":"logs","filters":{"address":"0x23f5e49569A835d7bf9AefD30e4f60CdD570f225","topics":["0xc8b501cbd8e69c98c535894661d25839eb035b096adfde2bba416f04cc7ce987"]}}'; const logs = '[{"address":"0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69","blockHash":"0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b","blockNumber":"0x3","data":"0x","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000000000000000000000000000000208fa13","0x0000000000000000000000000000000000000000000000000000000000000005"],"transactionHash":"0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392","transactionIndex":"0x1"},{"address":"0x0000000000000000000000000000000002131952","blockHash":"0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b","blockNumber":"0x3","data":"0x","logIndex":"0x1","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000000000000000000000000000000208fa13","0x0000000000000000000000000000000000000000000000000000000000000005"],"transactionHash":"0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392","transactionIndex":"0x1"},{"address":"0x0000000000000000000000000000000002131953","blockHash":"0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b","blockNumber":"0x4","data":"0x","logIndex":"0x0","removed":false,"topics":[],"transactionHash":"0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393","transactionIndex":"0x1"},{"address":"0x0000000000000000000000000000000002131954","blockHash":"0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b","blockNumber":"0x5","data":"0x","logIndex":"0x0","removed":false,"topics":[],"transactionHash":"0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394","transactionIndex":"0x1"}]'; - const logsArray = new Array([ - [ - { - address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x3', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [ - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x000000000000000000000000000000000000000000000000000000000208fa13', - '0x0000000000000000000000000000000000000000000000000000000000000005', - ], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131952', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x3', - data: '0x', - logIndex: '0x1', - removed: false, - topics: [ - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x000000000000000000000000000000000000000000000000000000000208fa13', - '0x0000000000000000000000000000000000000000000000000000000000000005', - ], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131953', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x4', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131954', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x5', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394', - transactionIndex: '0x1', - }, - ], - [ - { - address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x3', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [ - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x000000000000000000000000000000000000000000000000000000000208fa13', - '0x0000000000000000000000000000000000000000000000000000000000000005', - ], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131952', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x3', - data: '0x', - logIndex: '0x1', - removed: false, - topics: [ - '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x000000000000000000000000000000000000000000000000000000000208fa13', - '0x0000000000000000000000000000000000000000000000000000000000000005', - ], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131953', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x4', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393', - transactionIndex: '0x1', - }, - { - address: '0x0000000000000000000000000000000002131954', - blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', - blockNumber: '0x5', - data: '0x', - logIndex: '0x0', - removed: false, - topics: [], - transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394', - transactionIndex: '0x1', - }, - ], - ]); + const logsArray = [ + { + address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x3', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000208fa13', + '0x0000000000000000000000000000000000000000000000000000000000000005', + ], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131952', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x3', + data: '0x', + logIndex: '0x1', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000208fa13', + '0x0000000000000000000000000000000000000000000000000000000000000005', + ], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131953', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x4', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131954', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x5', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394', + transactionIndex: '0x1', + }, + { + address: '0x67D8d32E9Bf1a9968a5ff53B87d777Aa8EBBEe69', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x3', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000208fa13', + '0x0000000000000000000000000000000000000000000000000000000000000005', + ], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131952', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x3', + data: '0x', + logIndex: '0x1', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000208fa13', + '0x0000000000000000000000000000000000000000000000000000000000000005', + ], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131953', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x4', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393', + transactionIndex: '0x1', + }, + { + address: '0x0000000000000000000000000000000002131954', + blockHash: '0x3c08bbbee74d287b1dcd3f0ca6d1d2cb92c90883c4acf9747de9f3f3162ad25b', + blockNumber: '0x5', + data: '0x', + logIndex: '0x0', + removed: false, + topics: [], + transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6394', + transactionIndex: '0x1', + }, + ]; const SINGLE_LINE = 'Called notifySubscriber with single line of log data!'; const tag = '{"event":"logs","filters":{"address":"0x23f5e49569A835d7bf9AefD30e4f60CdD570f225","topics":["0xc8b501cbd8e69c98c535894661d25839eb035b096adfde2bba416f04cc7ce987"]}}'; - let ethImplStub: EthImpl; + let ethImplStub: sinon.SinonStubbedInstance; let poller: Poller; - let sandbox; + let sandbox: sinon.SinonSandbox; this.beforeEach(() => { ethImplStub = sinon.createStubInstance(EthImpl); - ethImplStub.blockNumber.returns('0x1b177b'); - ethImplStub.getLogs.returns(logs); + ethImplStub.blockNumber.resolves('0x1b177b'); + ethImplStub.getLogs.resolves(JSON.parse(logs)); const registry = new Registry(); poller = new Poller(ethImplStub, logger, registry); @@ -172,10 +168,10 @@ describe('Polling', async function () { describe('Poller', () => { it('should start polling', async () => { - ethImplStub.blockNumber.returns('0x1b177b'); + ethImplStub.blockNumber.resolves('0x1b177b'); // eslint-disable-next-line @typescript-eslint/no-empty-function const notifySubscriber = (tag, logs) => {}; - ethImplStub.getLogs.returns(logs); + ethImplStub.getLogs.resolves(JSON.parse(logs)); const loggerSpy = sandbox.spy(logger, 'info'); expect(poller.hasPoll(tag)).to.be.false; @@ -214,7 +210,7 @@ describe('Polling', async function () { return; }; - ethImplStub.getLogs.returns(logs); + ethImplStub.getLogs.resolves(JSON.parse(logs)); poller.add(tag, notifySubscriber); const loggerSpy = sandbox.spy(logger, 'debug'); @@ -236,7 +232,7 @@ describe('Polling', async function () { return; }; - ethImplStub.getLogs.returns(logsArray); + ethImplStub.getLogs.resolves(logsArray); poller.add(tag, notifySubscriber); const loggerSpy = sandbox.spy(logger, 'debug'); diff --git a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts index c3661ba7fe..44d8e649ef 100644 --- a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts +++ b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts @@ -20,6 +20,7 @@ import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; import { IPAddressHbarSpendingPlanRepository } from '../../../../src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository'; import { CacheService } from '../../../../src/lib/services/cacheService/cacheService'; import pino from 'pino'; @@ -35,28 +36,38 @@ chai.use(chaiAsPromised); describe('IPAddressHbarSpendingPlanRepository', function () { const logger = pino(); const registry = new Registry(); - const requestDetails = new RequestDetails({ requestId: 'testId', ipAddress: '0.0.0.0' }); + const requestDetails = new RequestDetails({ + requestId: 'ipAddressHbarSpendingPlanRepositoryTest', + ipAddress: '0.0.0.0', + }); + const ttl = 86_400_000; // 1 day + const ipAddress = '555.555.555.555'; + const nonExistingIpAddress = 'xxx.xxx.xxx.xxx'; const tests = (isSharedCacheEnabled: boolean) => { let cacheService: CacheService; + let cacheServiceSpy: sinon.SinonSpiedInstance; let repository: IPAddressHbarSpendingPlanRepository; - const ipAddress = '555.555.555.555'; - const nonExistingIpAddress = 'xxx.xxx.xxx.xxx'; - - if (isSharedCacheEnabled) { - useInMemoryRedisServer(logger, 6383); - } else { - overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' }); - } before(() => { cacheService = new CacheService(logger.child({ name: 'CacheService' }), registry); + cacheServiceSpy = sinon.spy(cacheService); repository = new IPAddressHbarSpendingPlanRepository( cacheService, logger.child({ name: 'IPAddressHbarSpendingPlanRepository' }), ); }); + if (isSharedCacheEnabled) { + useInMemoryRedisServer(logger, 6383); + } else { + overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' }); + } + + afterEach(async () => { + await cacheService.clear(requestDetails); + }); + after(async () => { await cacheService.disconnectRedisClient(); }); @@ -82,13 +93,21 @@ describe('IPAddressHbarSpendingPlanRepository', function () { it('saves an address plan successfully', async () => { const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) }; - await repository.save(addressPlan, requestDetails); + await repository.save(addressPlan, requestDetails, ttl); const result = await cacheService.getAsync( `${repository['collectionKey']}:${ipAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(addressPlan); + sinon.assert.calledWith( + cacheServiceSpy.set, + `${repository['collectionKey']}:${ipAddress}`, + addressPlan, + 'save', + requestDetails, + ttl, + ); }); it('overwrites an existing address plan', async () => { @@ -97,13 +116,21 @@ describe('IPAddressHbarSpendingPlanRepository', function () { const newPlanId = uuidV4(randomBytes(16)); const newAddressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: newPlanId }; - await repository.save(newAddressPlan, requestDetails); + await repository.save(newAddressPlan, requestDetails, ttl); const result = await cacheService.getAsync( `${repository['collectionKey']}:${ipAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(newAddressPlan); + sinon.assert.calledWith( + cacheServiceSpy.set, + `${repository['collectionKey']}:${ipAddress}`, + newAddressPlan, + 'save', + requestDetails, + ttl, + ); }); }); diff --git a/packages/relay/tests/lib/sdkClient.spec.ts b/packages/relay/tests/lib/sdkClient.spec.ts index a29fd02310..74d74cda5c 100644 --- a/packages/relay/tests/lib/sdkClient.spec.ts +++ b/packages/relay/tests/lib/sdkClient.spec.ts @@ -80,7 +80,7 @@ describe('SdkClient', async function () { let metricService: MetricService; let mirrorNodeClient: MirrorNodeClient; - const requestDetails = new RequestDetails({ requestId: 'testId', ipAddress: '0.0.0.0' }); + const requestDetails = new RequestDetails({ requestId: 'sdkClientTest', ipAddress: '0.0.0.0' }); const feeSchedules = { current: { transactionFeeSchedule: [ @@ -113,7 +113,7 @@ describe('SdkClient', async function () { Utils.createPrivateKeyBasedOnFormat(process.env.OPERATOR_KEY_MAIN!), ); const duration = constants.HBAR_RATE_LIMIT_DURATION; - const total = constants.HBAR_RATE_LIMIT_TINYBAR; + const total = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, registry); eventEmitter = new EventEmitter(); sdkClient = new SDKClient( diff --git a/packages/relay/tests/lib/services/hbarLimitService/hbarLimitService.spec.ts b/packages/relay/tests/lib/services/hbarLimitService/hbarLimitService.spec.ts index 3b22fb313b..5bcbc1e403 100644 --- a/packages/relay/tests/lib/services/hbarLimitService/hbarLimitService.spec.ts +++ b/packages/relay/tests/lib/services/hbarLimitService/hbarLimitService.spec.ts @@ -23,7 +23,6 @@ import pino, { Logger } from 'pino'; import chai, { expect } from 'chai'; import { randomBytes, uuidV4 } from 'ethers'; import chaiAsPromised from 'chai-as-promised'; -import { getRequestId } from '../../../helpers'; import constants from '../../../../src/lib/constants'; import { Counter, Gauge, Registry } from 'prom-client'; import { HbarLimitService } from '../../../../src/lib/services/hbarLimitService'; @@ -39,22 +38,26 @@ import { IPAddressHbarSpendingPlanNotFoundError, } from '../../../../src/lib/db/types/hbarLimiter/errors'; import { RequestDetails } from '../../../../src/lib/types'; +import { Hbar } from '@hashgraph/sdk'; +import { Long } from 'long'; chai.use(chaiAsPromised); describe('HbarLimitService', function () { const logger = pino(); const register = new Registry(); - const totalBudget = 100_000; + const totalBudget = Hbar.fromTinybars(constants.HBAR_RATE_LIMIT_TOTAL); + const totalBudgetInTinybars = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); + const limitDuration = constants.HBAR_RATE_LIMIT_DURATION; const mode = constants.EXECUTION_MODE.TRANSACTION; const methodName = 'testMethod'; const mockEthAddress = '0x123'; const mockIpAddress = 'x.x.x'; const mockEstimatedTxFee = 300; - const mockRequestId = getRequestId(); const mockPlanId = uuidV4(randomBytes(16)); + const todayAtMidnight = new Date().setHours(0, 0, 0, 0); - const requestDetails = new RequestDetails({ requestId: 'hbarLimterTest', ipAddress: mockIpAddress }); + const requestDetails = new RequestDetails({ requestId: 'hbarLimitServiceTest', ipAddress: mockIpAddress }); let hbarLimitService: HbarLimitService; let hbarSpendingPlanRepositoryStub: sinon.SinonStubbedInstance; @@ -74,6 +77,7 @@ describe('HbarLimitService', function () { logger, register, totalBudget, + limitDuration, ); }); @@ -81,14 +85,14 @@ describe('HbarLimitService', function () { sinon.restore(); }); - function createSpendingPlan(id: string, spentToday: number = 0) { + function createSpendingPlan(id: string, amountSpent: number | Long | Hbar = 0) { return new HbarSpendingPlan({ id, subscriptionType: SubscriptionType.BASIC, createdAt: new Date(), active: true, spendingHistory: [], - spentToday, + amountSpent: amountSpent instanceof Hbar ? Number(amountSpent.toTinybars()) : Number(amountSpent), }); } @@ -101,51 +105,71 @@ describe('HbarLimitService', function () { }); }); - describe('resetLimiter', function () { - const createSpiesForMetricsReset = (fieldName: string) => - Object.values(SubscriptionType).map((subscriptionType) => - sinon.spy(hbarLimitService[fieldName][subscriptionType], 'reset'), - ); + it('should set the reset date properly', () => { + const times = Math.ceil((Date.now() - todayAtMidnight) / limitDuration); + const expectedDate = new Date(todayAtMidnight + limitDuration * times); + const actualDate = hbarLimitService['reset']; + expect(new Date(actualDate)).to.deep.equal(new Date(expectedDate)); + }); + describe('getResetTimestamp', function () { + it('should return the current timestamp plus the limit duration', function () { + const times = Math.ceil((Date.now() - todayAtMidnight) / limitDuration); + const expectedDate = new Date(todayAtMidnight + limitDuration * times); + const actualDate = hbarLimitService['reset']; + expect(new Date(actualDate)).to.deep.equal(new Date(expectedDate)); + expect(hbarLimitService['getResetTimestamp']()).to.deep.equal(expectedDate); + }); + + describe('given a limit duration that is 1 day', function () { + const limitDuration = 24 * 60 * 60 * 1000; // one day + + it('should return tomorrow at midnight', function () { + const hbarLimitService = new HbarLimitService( + hbarSpendingPlanRepositoryStub, + ethAddressHbarSpendingPlanRepositoryStub, + ipAddressHbarSpendingPlanRepositoryStub, + logger, + register, + totalBudget, + limitDuration, + ); + const tomorrow = new Date(Date.now() + limitDuration); + const tomorrowAtMidnight = new Date(tomorrow.setHours(0, 0, 0, 0)); + expect(hbarLimitService['getResetTimestamp']()).to.deep.equal(tomorrowAtMidnight); + }); + }); + }); + + describe('resetLimiter', function () { beforeEach(() => { - hbarSpendingPlanRepositoryStub.resetAllSpentTodayEntries.resolves(); + hbarSpendingPlanRepositoryStub.resetAmountSpentOfAllPlans.resolves(); }); afterEach(() => { - hbarSpendingPlanRepositoryStub.resetAllSpentTodayEntries.restore(); + hbarSpendingPlanRepositoryStub.resetAmountSpentOfAllPlans.restore(); }); - it('should reset the spentToday field of all spending plans', async function () { + it('should reset the amountSpent field of all spending plans', async function () { await hbarLimitService.resetLimiter(requestDetails); - expect(hbarSpendingPlanRepositoryStub.resetAllSpentTodayEntries.called).to.be.true; + expect(hbarSpendingPlanRepositoryStub.resetAmountSpentOfAllPlans.called).to.be.true; }); it('should reset the remaining budget and update the gauge', async function () { // @ts-ignore - hbarLimitService.remainingBudget = 1000; + hbarLimitService.remainingBudget = Hbar.fromTinybars(1000); const setSpy = sinon.spy(hbarLimitService['hbarLimitRemainingGauge'], 'set'); await hbarLimitService.resetLimiter(requestDetails); - expect(hbarLimitService['remainingBudget']).to.equal(totalBudget); - expect(setSpy.calledOnceWith(totalBudget)).to.be.true; - }); - - it('should reset the daily unique spending plans counter', async function () { - const spies = createSpiesForMetricsReset('dailyUniqueSpendingPlansCounter'); - await hbarLimitService.resetLimiter(requestDetails); - spies.forEach((spy) => sinon.assert.calledOnce(spy)); - }); - - it('should reset the average daily spending plan usages gauge', async function () { - const spies = createSpiesForMetricsReset('averageDailySpendingPlanUsagesGauge'); - await hbarLimitService.resetLimiter(requestDetails); - spies.forEach((spy) => sinon.assert.calledOnce(spy)); + expect(hbarLimitService['remainingBudget'].toTinybars().toNumber()).to.eq(totalBudgetInTinybars); + expect(setSpy.calledOnceWith(totalBudgetInTinybars)).to.be.true; }); - it('should set the reset date to the next day at midnight', async function () { - const tomorrow = new Date(Date.now() + HbarLimitService.ONE_DAY_IN_MILLIS); - const expectedResetDate = new Date(tomorrow.setHours(0, 0, 0, 0)); + it('should set the reset date to the current timestamp plus the limit duration', async function () { + const times = Math.ceil((Date.now() - todayAtMidnight) / limitDuration); + const expectedDate = new Date(todayAtMidnight + limitDuration * times); await hbarLimitService.resetLimiter(requestDetails); - expect(hbarLimitService['reset']).to.deep.equal(expectedResetDate); + const resetDate = hbarLimitService['reset']; + expect(new Date(resetDate)).to.deep.equal(new Date(expectedDate)); }); }); @@ -153,14 +177,14 @@ describe('HbarLimitService', function () { describe('based on ethAddress', async function () { it('should return true if the total daily budget is exceeded', async function () { // @ts-ignore - hbarLimitService.remainingBudget = 0; + hbarLimitService.remainingBudget = Hbar.fromTinybars(0); const result = await hbarLimitService.shouldLimit(mode, methodName, mockEthAddress, requestDetails); expect(result).to.be.true; }); it('should return true when remainingBudget < estimatedTxFee ', async function () { // @ts-ignore - hbarLimitService.remainingBudget = mockEstimatedTxFee - 1; + hbarLimitService.remainingBudget = Hbar.fromTinybars(mockEstimatedTxFee - 1); const result = await hbarLimitService.shouldLimit( mode, methodName, @@ -197,8 +221,8 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return true if spentToday is exactly at the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC]); + it('should return true if amountSpent is exactly at the limit', async function () { + const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC]); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, planId: mockPlanId, @@ -210,8 +234,11 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return false if spentToday is just below the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - 1); + it('should return false if amountSpent is just below the limit', async function () { + const spendingPlan = createSpendingPlan( + mockPlanId, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(1), + ); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, planId: mockPlanId, @@ -223,8 +250,11 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return true if spentToday is just above the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] + 1); + it('should return true if amountSpent is just above the limit', async function () { + const spendingPlan = createSpendingPlan( + mockPlanId, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().add(1), + ); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, planId: mockPlanId, @@ -236,10 +266,10 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return true if spentToday + estimatedTxFee is above the limit', async function () { + it('should return true if amountSpent + estimatedTxFee is above the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee + 1, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee).add(1), ); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, @@ -258,10 +288,10 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return false if spentToday + estimatedTxFee is below the limit', async function () { + it('should return false if amountSpent + estimatedTxFee is below the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee - 1, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee).sub(1), ); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, @@ -274,10 +304,10 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return false if spentToday + estimatedTxFee is at the limit', async function () { + it('should return false if amountSpent + estimatedTxFee is at the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee), ); ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ethAddress: mockEthAddress, @@ -294,14 +324,14 @@ describe('HbarLimitService', function () { describe('based on ipAddress', async function () { it('should return true if the total daily budget is exceeded', async function () { // @ts-ignore - hbarLimitService.remainingBudget = 0; + hbarLimitService.remainingBudget = Hbar.fromTinybars(0); const result = await hbarLimitService.shouldLimit(mode, methodName, '', requestDetails); expect(result).to.be.true; }); it('should return true when remainingBudget < estimatedTxFee ', async function () { // @ts-ignore - hbarLimitService.remainingBudget = mockEstimatedTxFee - 1; + hbarLimitService.remainingBudget = Hbar.fromTinybars(mockEstimatedTxFee - 1); const result = await hbarLimitService.shouldLimit(mode, methodName, '', requestDetails, mockEstimatedTxFee); expect(result).to.be.true; }); @@ -333,8 +363,8 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return true if spentToday is exactly at the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC]); + it('should return true if amountSpent is exactly at the limit', async function () { + const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC]); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, planId: mockPlanId, @@ -346,8 +376,11 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return false if spentToday is just below the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - 1); + it('should return false if amountSpent is just below the limit', async function () { + const spendingPlan = createSpendingPlan( + mockPlanId, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(1), + ); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, planId: mockPlanId, @@ -359,8 +392,11 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return true if spentToday is just above the limit', async function () { - const spendingPlan = createSpendingPlan(mockPlanId, HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] + 1); + it('should return true if amountSpent is just above the limit', async function () { + const spendingPlan = createSpendingPlan( + mockPlanId, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().add(1), + ); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, planId: mockPlanId, @@ -372,10 +408,10 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return true if spentToday + estimatedTxFee is above the limit', async function () { + it('should return true if amountSpent + estimatedTxFee is above the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee + 1, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee).add(1), ); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, @@ -388,10 +424,10 @@ describe('HbarLimitService', function () { expect(result).to.be.true; }); - it('should return false if spentToday + estimatedTxFee is below the limit', async function () { + it('should return false if amountSpent + estimatedTxFee is below the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee - 1, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee).sub(1), ); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, @@ -404,10 +440,10 @@ describe('HbarLimitService', function () { expect(result).to.be.false; }); - it('should return false if spentToday + estimatedTxFee is at the limit', async function () { + it('should return false if amountSpent + estimatedTxFee is at the limit', async function () { const spendingPlan = createSpendingPlan( mockPlanId, - HbarLimitService.DAILY_LIMITS[SubscriptionType.BASIC] - mockEstimatedTxFee, + HbarLimitService.TIER_LIMITS[SubscriptionType.BASIC].toTinybars().sub(mockEstimatedTxFee), ); ipAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ ipAddress: mockIpAddress, @@ -530,8 +566,7 @@ describe('HbarLimitService', function () { describe('createBasicSpendingPlan', function () { const testCreateBasicSpendingPlan = async (ethAddress: string, ipAddress?: string) => { - const requestDetails = new RequestDetails({ requestId: 'hbarLimterTest', ipAddress: ipAddress ? ipAddress : '' }); - console.log('requestDetails', requestDetails); + const requestDetails = new RequestDetails({ requestId: 'hbarLimitServiceTest', ipAddress: ipAddress ?? '' }); const newSpendingPlan = createSpendingPlan(mockPlanId); hbarSpendingPlanRepositoryStub.create.resolves(newSpendingPlan); ethAddressHbarSpendingPlanRepositoryStub.save.resolves(); @@ -567,7 +602,7 @@ describe('HbarLimitService', function () { describe('addExpense', function () { const testAddExpense = async (ethAddress: string, ipAddress: string, expense: number = 100) => { - const otherPlanUsedToday = createSpendingPlan(uuidV4(randomBytes(16)), 200); + const otherPlanOfTheSameTier = createSpendingPlan(uuidV4(randomBytes(16)), 200); const existingSpendingPlan = createSpendingPlan(mockPlanId, 0); if (ethAddress) { ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ @@ -583,12 +618,12 @@ describe('HbarLimitService', function () { hbarSpendingPlanRepositoryStub.create.resolves(existingSpendingPlan); } hbarSpendingPlanRepositoryStub.findByIdWithDetails.resolves(existingSpendingPlan); - hbarSpendingPlanRepositoryStub.addAmountToSpentToday.resolves(); + hbarSpendingPlanRepositoryStub.addToAmountSpent.resolves(); hbarSpendingPlanRepositoryStub.findAllActiveBySubscriptionType.resolves([ - otherPlanUsedToday, + otherPlanOfTheSameTier, { ...existingSpendingPlan, - spentToday: expense, + amountSpent: expense, spendingHistory: [{ amount: expense, timestamp: new Date() }], }, ]); @@ -607,16 +642,15 @@ describe('HbarLimitService', function () { await hbarLimitService.addExpense(expense, ethAddress, requestDetails); - expect(hbarSpendingPlanRepositoryStub.addAmountToSpentToday.calledOnceWith(mockPlanId, expense)).to.be.true; - // @ts-ignore - expect(hbarLimitService.remainingBudget).to.equal(hbarLimitService.totalBudget - expense); - // @ts-ignore - expect((await hbarLimitService.hbarLimitRemainingGauge.get()).values[0].value).to.equal( - // @ts-ignore - hbarLimitService.totalBudget - expense, + expect(hbarSpendingPlanRepositoryStub.addToAmountSpent.calledOnceWith(mockPlanId, expense)).to.be.true; + expect(hbarLimitService['remainingBudget'].toTinybars().toNumber()).to.eq( + hbarLimitService['totalBudget'].toTinybars().sub(expense).toNumber(), + ); + expect((await hbarLimitService['hbarLimitRemainingGauge'].get()).values[0].value).to.equal( + hbarLimitService['totalBudget'].toTinybars().sub(expense).toNumber(), ); await Promise.all(updateAverageDailyUsagePerSubscriptionTypeSpy.returnValues); - const expectedAverageUsage = Math.round((otherPlanUsedToday.spentToday + expense) / 2); + const expectedAverageUsage = Math.round((otherPlanOfTheSameTier.amountSpent + expense) / 2); sinon.assert.calledOnceWithExactly(setAverageDailySpendingPlanUsagesGaugeSpy, expectedAverageUsage); sinon.assert.calledOnceWithExactly(incDailyUniqueSpendingPlansCounterSpy, 1); }; @@ -668,7 +702,7 @@ describe('HbarLimitService', function () { planId: mockPlanId, }); hbarSpendingPlanRepositoryStub.findByIdWithDetails.resolves(createSpendingPlan(mockPlanId)); - hbarSpendingPlanRepositoryStub.addAmountToSpentToday.rejects(new Error('Failed to add expense')); + hbarSpendingPlanRepositoryStub.addToAmountSpent.rejects(new Error('Failed to add expense')); await expect(hbarLimitService.addExpense(100, mockEthAddress, requestDetails)).to.be.eventually.rejectedWith( 'Failed to add expense', @@ -679,7 +713,7 @@ describe('HbarLimitService', function () { describe('isDailyBudgetExceeded', function () { const testIsDailyBudgetExceeded = async (remainingBudget: number, expected: boolean) => { // @ts-ignore - hbarLimitService.remainingBudget = remainingBudget; + hbarLimitService.remainingBudget = Hbar.fromTinybars(remainingBudget); await expect( hbarLimitService['isDailyBudgetExceeded'](mode, methodName, undefined, requestDetails), ).to.eventually.equal(expected); @@ -693,19 +727,6 @@ describe('HbarLimitService', function () { await testIsDailyBudgetExceeded(-1, true); }); - it('should handle errors when adding expense fails', async function () { - ethAddressHbarSpendingPlanRepositoryStub.findByAddress.resolves({ - ethAddress: mockEthAddress, - planId: mockPlanId, - }); - hbarSpendingPlanRepositoryStub.findByIdWithDetails.resolves(createSpendingPlan(mockPlanId)); - hbarSpendingPlanRepositoryStub.addAmountToSpentToday.rejects(new Error('Failed to add expense')); - - await expect(hbarLimitService.addExpense(100, mockEthAddress, requestDetails)).to.be.eventually.rejectedWith( - 'Failed to add expense', - ); - }); - it('should return false when the remaining budget is greater than zero', async function () { await testIsDailyBudgetExceeded(100, false); }); diff --git a/packages/relay/tests/lib/services/metricService/metricService.spec.ts b/packages/relay/tests/lib/services/metricService/metricService.spec.ts index f3704f2864..49aabbfc03 100644 --- a/packages/relay/tests/lib/services/metricService/metricService.spec.ts +++ b/packages/relay/tests/lib/services/metricService/metricService.spec.ts @@ -137,7 +137,7 @@ describe('Metric Service', function () { mock = new MockAdapter(instance); const duration = constants.HBAR_RATE_LIMIT_DURATION; - const total = constants.HBAR_RATE_LIMIT_TINYBAR; + const total = constants.HBAR_RATE_LIMIT_TOTAL.toNumber(); hbarLimiter = new HbarLimit(logger.child({ name: 'hbar-rate-limit' }), Date.now(), total, duration, registry); eventEmitter = new EventEmitter(); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index dd93cc2d06..55a18cc8be 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -22,7 +22,7 @@ import app from './server'; import { setServerTimeout } from './koaJsonRpc/lib/utils'; // Import the 'setServerTimeout' function from the correct location async function main() { - const server = await app.listen({ port: process.env.SERVER_PORT || 7546 }); + const server = app.listen({ port: process.env.SERVER_PORT || 7546, host: process.env.SERVER_HOST }); // set request timeout to ensure sockets are closed after specified time of inactivity setServerTimeout(server); diff --git a/packages/server/tests/acceptance/index.spec.ts b/packages/server/tests/acceptance/index.spec.ts index fa539f7b04..fe74a3edc0 100644 --- a/packages/server/tests/acceptance/index.spec.ts +++ b/packages/server/tests/acceptance/index.spec.ts @@ -210,6 +210,7 @@ describe('RPC Server Acceptance Tests', function () { // start local relay, relay instance in local should not be running logger.info(`Start relay on port ${constants.RELAY_PORT}`); + logger.info(`Start relay on host ${constants.RELAY_HOST}`); const relayServer = app.listen({ port: constants.RELAY_PORT }); global.relayServer = relayServer; setServerTimeout(relayServer); diff --git a/packages/server/tests/acceptance/serverConfig.spec.ts b/packages/server/tests/acceptance/serverConfig.spec.ts index 3edc034b42..ff7197d266 100644 --- a/packages/server/tests/acceptance/serverConfig.spec.ts +++ b/packages/server/tests/acceptance/serverConfig.spec.ts @@ -24,7 +24,7 @@ describe('@server-config Server Configuration Options Coverage', function () { describe('Koa Server Timeout', () => { it('should timeout a request after the specified time', async () => { const requestTimeoutMs: number = parseInt(process.env.SERVER_REQUEST_TIMEOUT_MS || '3000'); - const host = 'localhost'; + const host = process.env.SERVER_HOST || 'localhost'; const port = parseInt(process.env.SERVER_PORT || '7546'); const method = 'eth_blockNumber'; const params: any[] = []; diff --git a/packages/server/tests/integration/server.spec.ts b/packages/server/tests/integration/server.spec.ts index f2c50817b7..875f76ae88 100644 --- a/packages/server/tests/integration/server.spec.ts +++ b/packages/server/tests/integration/server.spec.ts @@ -70,6 +70,35 @@ describe('RPC Server', function () { this.timeout(5000); + it('should verify that the server is running with the correct host and port', async function () { + const CUSTOMIZE_PORT = '7545'; + const CUSTOMIZE_HOST = '127.0.0.1'; + const configuredServer = app.listen({ port: CUSTOMIZE_PORT, host: CUSTOMIZE_HOST }); + + return new Promise((resolve, reject) => { + configuredServer.on('listening', () => { + const address = configuredServer.address(); + + try { + expect(address).to.not.be.null; + if (address && typeof address === 'object') { + expect(address.address).to.equal(CUSTOMIZE_HOST); + expect(address.port.toString()).to.equal(CUSTOMIZE_PORT); + } else { + throw new Error('Server address is not an object'); + } + configuredServer.close(() => resolve()); + } catch (error) { + configuredServer.close(() => reject(error)); + } + }); + + configuredServer.on('error', (error) => { + reject(error); + }); + }); + }); + it('should execute "eth_chainId"', async function () { const res = await testClient.post('/', { id: '2', diff --git a/packages/ws-server/src/index.ts b/packages/ws-server/src/index.ts index 6f5e70ab73..b4566e9692 100644 --- a/packages/ws-server/src/index.ts +++ b/packages/ws-server/src/index.ts @@ -22,8 +22,9 @@ import { app, httpApp } from './webSocketServer'; import constants from '@hashgraph/json-rpc-relay/dist/lib/constants'; async function main() { - app.listen({ port: constants.WEB_SOCKET_PORT }); - httpApp.listen({ port: constants.WEB_SOCKET_HTTP_PORT }); + const host = process.env.SERVER_HOST; + app.listen({ port: constants.WEB_SOCKET_PORT, host }); + httpApp.listen({ port: constants.WEB_SOCKET_HTTP_PORT, host }); } (async () => {