Skip to content

feat: validate that operator account has positive balance #3930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

acuarica
Copy link
Contributor

@acuarica acuarica commented Jul 11, 2025

Description:

This PR validates that the operator account OPERATOR_ID_MAIN has positive balance when starting up the Relay/WebSocket server in Read-Write mode. If the operator account has no balance (or does not exist), the Relay/WebSocket will fail on start up.

Related issue(s):

Fixes #3929.

Notes for reviewer:

Note

This PR introduces an async start up check. It needs to be async because it needs to make a request to the Mirror Node to get the operator's balance. This check needs to be placed directly in main (for both Relay and WebSocket) because the ConfigService loading process is sync (no Promises involved). It cannot be placed in the Relay's constructor either because this will lead to an async constructor.

Note

Initial discussion can be found here #3896 (comment).

Note

In packages/relay/tests/lib/eth/eth-helpers.ts, some @ts-ignore comments were removed because they were not exercised. We should use @ts-expect-error instead.

Note

In packages/relay/tests/lib/eth/eth_getBalance.spec.ts, code related to SDKClient was removed because it's not used anymore.

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

@acuarica acuarica self-assigned this Jul 11, 2025
@acuarica acuarica added the enhancement New feature or request label Jul 11, 2025
@acuarica acuarica added this to the 0.70.0 milestone Jul 11, 2025
Copy link

github-actions bot commented Jul 11, 2025

Test Results

 20 files  ±0  277 suites  ±0   18m 2s ⏱️ +5s
702 tests  - 1  697 ✅ ±0  5 💤 ±0  0 ❌  - 1 
718 runs  ±0  713 ✅ +1  5 💤 ±0  0 ❌  - 1 

Results for commit c1224f4. ± Comparison against base commit f82f220.

This pull request removes 1 test.
"after all" hook in "@web-socket-batch-1 eth_call" ‑ RPC Server Acceptance Tests Acceptance tests @web-socket-batch-1 eth_call "after all" hook in "@web-socket-batch-1 eth_call"

♻️ This comment has been updated with latest results.

@acuarica acuarica marked this pull request as ready for review July 15, 2025 13:49
@acuarica acuarica requested review from a team as code owners July 15, 2025 13:49
acuarica added 4 commits July 15, 2025 17:06
Signed-off-by: Luis Mastrangelo <[email protected]>
Signed-off-by: Luis Mastrangelo <[email protected]>
Signed-off-by: Luis Mastrangelo <[email protected]>
@acuarica acuarica force-pushed the 3929-operator-has-balance-in-read-write-mode branch from 8fa49fb to c1224f4 Compare July 15, 2025 15:07
Copy link

codecov bot commented Jul 15, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

@@            Coverage Diff             @@
##             main    #3930      +/-   ##
==========================================
- Coverage   86.86%   86.75%   -0.11%     
==========================================
  Files          87       87              
  Lines        5039     5045       +6     
  Branches     1020     1022       +2     
==========================================
  Hits         4377     4377              
- Misses        400      407       +7     
+ Partials      262      261       -1     
Flag Coverage Δ
config-service 95.78% <ø> (ø)
relay 81.32% <100.00%> (+0.05%) ⬆️
server 80.81% <100.00%> (ø)
ws-server 61.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/relay/src/lib/relay.ts 94.87% <100.00%> (+0.42%) ⬆️
packages/server/src/server.ts 67.88% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@quiet-node quiet-node left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG left some thought

@@ -50,6 +50,7 @@ jobs:
-e MIRROR_NODE_URL='http://127.0.0.1:5551' \
-e OPERATOR_ID_MAIN='0.0.1002' \
-e OPERATOR_KEY_MAIN='302e020100300506032b65700422042077d69b53642d0000000000000000000000000000000000000000000000000000' \
-e READ_ONLY='true' \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq why the need to add READ_ONLY it already has operator key and ID right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because otherwise it fails to properly start due to balance check (there is no MN behind http://127.0.0.1:5551).

https://github.com/hiero-ledger/hiero-json-rpc-relay/actions/runs/16227556140/job/45822904839#step:9:13

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see thanks for the explanation

Comment on lines +332 to 344

async ensureOperatorHasBalance() {
if (ConfigService.get('READ_ONLY')) return;

const operator = this.clientMain.operatorAccountId!.toString();
const balance = BigInt(await this.ethImpl.getBalance(operator, 'latest', {} as RequestDetails));
if (balance === BigInt(0)) {
throw new Error(`Operator account '${operator}' has no balance`);
} else {
this.logger.info(`Operator account '${operator}' has balance: ${balance}`);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the ensureOperatorHasBalance() function could be done a little differently as it seems to pull in quite a few components. Like relying on both clientMain and ethImpl makes it feel somewhat fragile, since it depends on both being properly set up. On top of that, using ethImpl.getBalance, an app-level API that itself relies on MirrorNodeClient, cacheService, Logger, eventEmitter, etc. being correctly initialized — just to validate a config value feels a bit circular.

So would it make more sense, or be possible, to:
• Get the operator ID directly from ConfigService via ConfigService.get('OPERATOR_ID')
• Fetch the balance straight from the Mirror Node using the /balances endpoint via ConfigService.get('MIRROR_NODE_URL')

That way, we could skip involving ethImpl altogether and avoid any extra logic or validation tied to eth_getBalance. I know it’s a lightweight call, but since it’s an app-level API and could be programmatically updated, it makes the function feel a bit less stable for this job.

At the end I just think the fewer moving parts we rely on, the better at this stage of validation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also when I first heard about validating the operator balance, I initially thought it could be done inside hapiService during client initialization or some sort since that’s where we set up the operator keys and IDs. It felt like a natural place to include a balance check as part of that process.

Maybe it’s not feasible because it would require the function to be async or maybe we could just use .then() or something along those lines. Either way, it feels like the balance validation belongs there. We’re initializing the SDK client, and if the operator doesn’t have enough balance, it makes sense to catch that early and fail right at that level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an app-level API that itself relies on MirrorNodeClient, cacheService, Logger, eventEmitter, etc. being correctly initialized — just to validate a config value feels a bit circular.

The ensureOperatorHasBalance is just part of the public API of the Relay. There is nothing circular here, that's why it's being called in main.

• Get the operator ID directly from ConfigService via ConfigService.get('OPERATOR_ID')
• Fetch the balance straight from the Mirror Node using the /balances endpoint via ConfigService.get('MIRROR_NODE_URL')

Well, the idea is to avoid duplication. If we have an API to make requests to the MN, we should use it, regardless if it's for RPC calls or initial checks. The same for the operator, usingoperatorAccountId ensures that we are reusing any logic that is already put in clientMain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It felt like a natural place to include a balance check as part of that process.

In that case now you have a dependency from hapiService to the Mirror Node client, which to me does not seem right. MN client and HAPI service should be independent.

On the other hand, I understand that maybe this validation could have been put somewhere else. That's where async comes in. Having async constructors will cause more problems than solutions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool thanks for the explanation it makes sense! However, I still think it might be more stable and decoupled if we used configService to get the operator ID and called the /balances endpoint directly. That way, we can avoid relying on all the extra properties coming from hapiService and ethImpl. Just a thought I wanted to put out there not opposed to your solution at all, especially since it shouldn’t affect the app’s performance either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's the problem on relying on an object being properly constructed. Note that given this check is async, we need a proper Relay constructed anyways.

Regarding the /balances/ endpoint, there is some additional logic in AccountService.getBalance like converting to weibars, formatting, and deconstructing the balance result. This would have to be repeated if we call the endpoint directly.

Copy link
Contributor

@quiet-node quiet-node left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Great work as always!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ensure the operator account has positive balance in Read-Write mode
2 participants