Skip to content

feat: make chain-gateways monitornig logic generic for other back-ends - #4266

Open
kevindeforth wants to merge 3 commits into
mainfrom
kd/improve-chain-gateway
Open

feat: make chain-gateways monitornig logic generic for other back-ends#4266
kevindeforth wants to merge 3 commits into
mainfrom
kd/improve-chain-gateway

Conversation

@kevindeforth

@kevindeforth kevindeforth commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Resolves #4253

Main Changes
With #3693 we started unifying calls to our mpc-contract across our various back-ends (e2e-test, sandbox, devnet, node).
As of now, the near-contract-transport crate allows one to call a contract and execute one-shot state-views.
The node and other components (like the backup-service, which currently uses an RPC endpoint), require not only the ability to view contract state, but also, to monitor contract state for changes.

The chain-gateway offers this capability, but three shortcomings prevent us from moving its traits to the near-contract-transport crate, where every back-end could implement them:

  1. It hard-codes the error type to ChainGatewayError. Other back-ends will have their own error-types. This PR makes the error type a generic that can be set by different back-ends.
  2. By default a contract subscription and view call resut is deserialized to lson. But contracts may return a variety of serialization formats and users may want to provide their own deserializers. This PR allows a user to set their deserializer.
  3. The chain-gateway traits can be simplified. We currently have four traits for viewing contract state from chain: ViewContract (the backend query), ViewRaw (one-shot raw view), ViewMethod (one-shot call viewing and deserializing state) and SubscribeToContractMethod (monitoring and deserializing state). A user needs to know several different trait-names and what differentiates them:
let raw = viewer.view_contract(&contract_id, view_args).await;
let res = viewer.view_raw(&contract_id, view_args).await;
let val = viewer.view_method::<String>(contract_id, view_args).await;
let mut sub = viewer.subscribe_to_contract_method::<String>(contract_id, view_args).await;

This bears an unnecessary cognitive load. Any struct that provides the ViewContract interface should be able to provide state viewing and monitoring for free. In this PR, we introduce the following API:

let raw = viewer.view_contract(&contract_id, view_args).await; // SerializedObservation
let res = viewer.view_json::<String>(contract_id, view_args).await;
let mut sub = viewer.view_json::<String>(contract_id, view_args).subscribe().await;

This PR deletes the SubscribeToContractMethod, ViewMethod, and ViewRaw traits, as they are no longer needed. Instead, we introduce a ViewCall struct that can either be .await-ed (to yield the one-shot view response), or .subscribe()-ed to (to yield a continuous view of the contract). The ViewCall allows the caller to define what deserializer to use for deserializing the chain response.

Additional changes in this PR:

  • A small bug-fix. The ViewContract implementation of the ChainGateway struct didn't await full-sync before calling the near client actor. This PR fixes that.
  • This PR introduces a HasPollInterval trait, which allows the consumer to customize how often the monitoring task should query the back-end for changes. This was introduced because the backup-cli relies on a user-provided poll-interval, likely to avoid overwhelming an RPC endpoint with requests.
  • Instead of hard-coding the poll-interval at 200 millisecond, the chain-gateway now reads the expected block time from the near node config and sets its poll-interval proportional to that (with a floor). This is helpful when running localnets, where we often have a much shorter block-time than in testnet or mainnet.

Next steps:
We will want to move a lot of stuff from the chain-gateway crate to the near-contract-transport crate. This is purposely left for a follow-up PR, as otherwise, reviewing the changes would become quite cumbersome (c.f. #4275)

@kevindeforth
kevindeforth force-pushed the kd/improve-chain-gateway branch from 0858244 to 0c67104 Compare August 27, 2026 20:35
@near near deleted a comment from claude Bot Aug 27, 2026
@near near deleted a comment from claude Bot Aug 27, 2026
@near near deleted a comment from claude Bot Aug 27, 2026
@near near deleted a comment from claude Bot Aug 27, 2026
@near near deleted a comment from claude Bot Aug 27, 2026
@near near deleted a comment from claude Bot Aug 27, 2026

@kevindeforth kevindeforth left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I left some comments that I thought might be helpful to reviewers. Review recommendation:

  1. start with the integration tests in chain-gateway/tests - they should give a good idea of where this is headed on a high-level and show that this doesn't break anything.
  2. The only new stuff this PR adds is the ViewCall struct in crates/chain-gateway/src/state_viewer/view_call.rs.


impl ViewContract for NearViewClientActorHandle {
type Error = NearViewClientError;
type ObservedAt = BlockHeight;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was kind of a bug. Implementing ViewContract directly on NearViewClientActor makes it seem like it's okay to call view_contract on the client actor.
But it isn't. It's important we first ensure that the node is fully synced. Otherwise, we risk getting a valid, but historic view, which may lead to all sorts of issues.

In this PR, we preserve the ViewContract implementation over the ChainGateway struct, but delegate this one to a chain-gateway internal method.

Comment on lines -17 to -19
impl Drop for MonitoringTask {
fn drop(&mut self) {
self.cancel_token.cancel();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We got rid of this cancel-on-drop implementation by replacing the CancellationToken with its DropGuard.

Comment on lines -9 to -15
/// Holds a Monitoring task and the latest cached value.
/// This is useful such that we don't unnecessarily deserialize the same state multiple times.
pub(crate) struct ContractMethodSubscription<Res> {
inner: MonitoringTask,
cached: Result<ObservedState<Res>, ChainGatewayError>,
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This struct was kind of pointless. When I initially introduced this struct, I thought it was needed for testing, but without it, we test just fine, so, simplification wins.

This PR moves the cached field into MontiringTask.

use near_contract_transport::{ObservedState, TransportError};

impl<Res> WatchContractState<Res> for ContractMethodSubscription<Res>
impl<T, ViewError> WatchContractState<T, ViewError> for MonitoringTask<T, ViewError>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note that this is not the correct place to implement this trait, but I left it here to reduce the diff-size for reviewers. We will move a lot of this to the near-contract-transport crate in a follow-up PR (#4275)

.changed()
.await
.map_err(|_| ChainGatewayError::MonitoringClosed)?;
.map_err(|_| TransportError::MonitoringClosed)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And for that, we are now no longer returning a ChainGatewayError, but a TransportError, because the error here is back-end agnostic.

Comment on lines -76 to -80
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ObservedState<T = Vec<u8>, H = BlockHeight> {
pub observed_at: H,
pub value: T,
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved to crates/near-contract-transport/src/views/observation.rs

@kevindeforth kevindeforth Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file contains the only innovation in this PR. Everything else is kind of just moving stuff around in a minimally intrusive way to reduce diff size.

Note: I would have preferred to have this file already in the near-contract-transport crate, but that would have required to already move the MonitoringTask, i.e. state_viewer/subscription.rs and state_viewer/monitoring.rs files too, which would have made the diff way bigger.

Comment on lines +180 to +185
let expected_block_time: std::time::Duration = near_config
.config
.consensus
.min_block_production_delay
.try_into()
.expect("block time must be non-negative");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay, this is also an improvement over the status quo. Instead of hard-coding the poll interval to 200 milliseconds, we read the expected block time and compute a good poll interval from the config value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The MockViewContract struct in this file will be moved to the near-contract-transport crate with #4275. That's the reason for breaking it out of the MockChainState.

Comment on lines +42 to +48
pub(crate) async fn view_when_synced<T, F: Future<Output = T>>(
sync: &impl IsSyncing,
view: impl FnOnce() -> F,
) -> T {
sync.wait_for_full_sync().await;
view().await
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude insisted on this, such that we can test the sync-view logic. I agree with claude.

@kevindeforth

Copy link
Copy Markdown
Contributor Author

@claude review

@kevindeforth
kevindeforth marked this pull request as ready for review August 27, 2026 21:56
@claude

This comment was marked as low quality.

@kevindeforth

Copy link
Copy Markdown
Contributor Author

Finally, Claude didn't find anything of substance, we are good to review 💪

@claude

This comment was marked as low quality.

@gilcu3
gilcu3 self-requested a review August 28, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

move chain-gateways subscriber logic to the mpc-contract-transport crate

1 participant