Skip to content

Commit 1ef3e16

Browse files
refactor(probe): enforce the absence meaning on the trait
`ClassifyRpcOutcome::Response` now requires `HasAbsenceMeaning`, so a transport cannot classify a response type that never declared what a "not found" answer means for it. Also restores the `#[from]` conversions on `AptosRpcError` and sweeps the comments this stack added.
1 parent cd302d0 commit 1ef3e16

4 files changed

Lines changed: 15 additions & 26 deletions

File tree

crates/foreign-chain-inspector/src/aptos/inspector.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl HasAbsenceMeaning for TransactionResponse {
104104
const ABSENCE: AbsenceMeaning = AbsenceMeaning::TransactionIsAbsent;
105105
}
106106

107-
/// The ledger info is the REST API root, which every Aptos node serves.
107+
/// Every Aptos node serves the ledger info at its REST API base.
108108
impl HasAbsenceMeaning for LedgerInfoResponse {
109109
const ABSENCE: AbsenceMeaning = AbsenceMeaning::ApiIsNotServed;
110110
}
@@ -137,8 +137,7 @@ impl<T: HasAbsenceMeaning> ClassifyRpcOutcome for Result<T, AptosRpcError> {
137137
AptosRpcError::ApiError { status, .. } if status >= 500 => {
138138
ForeignChainInspectionError::RpcRequestFailed(message)
139139
}
140-
// Remaining 4xx (400/401/403/410, …) are deterministic rejections —
141-
// retrying cannot change them, so they count as substantive verdicts.
140+
// Retrying cannot change a deterministic 4xx, so it counts as a substantive verdict.
142141
AptosRpcError::ApiError { .. } => {
143142
ForeignChainInspectionError::RpcRequestRejected(message)
144143
}
@@ -292,7 +291,6 @@ mod tests {
292291
};
293292
use rstest::rstest;
294293

295-
/// Mainnet, as the docs and the config templates ship it.
296294
const MAINNET_CHAIN_ID: u64 = 1;
297295

298296
struct MockAptosClient {
@@ -878,7 +876,7 @@ mod tests {
878876
#[case::internal_error(500)]
879877
#[case::bad_request(400)]
880878
#[case::unauthorized(401)]
881-
fn classified__should_read_every_status_but_404_alike(#[case] status: u16) {
879+
fn classified__should_treat_404_as_the_only_resource_dependent_status(#[case] status: u16) {
882880
// Given
883881
let read_as_transaction: Result<TransactionResponse, _> =
884882
Err(MockAptosClient::error(status));

crates/foreign-chain-inspector/src/lib.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -470,27 +470,23 @@ fn is_retryable_status(status_code: u16) -> bool {
470470
matches!(status_code, REQUEST_TIMEOUT | TOO_MANY_REQUESTS) || status_code >= SERVER_ERROR
471471
}
472472

473-
/// What a provider's "not found" answer means for the resource that was read: the one wire
474-
/// condition whose verdict depends on what was asked for rather than on the status itself.
473+
/// The meaning of a provider's "not found" answer
475474
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476475
pub(crate) enum AbsenceMeaning {
477-
/// The chain may legitimately not hold it.
478476
TransactionIsAbsent,
479-
/// Every node serving this chain's API holds it.
480477
ApiIsNotServed,
481478
}
482479

483480
/// The [`AbsenceMeaning`] of the resource a response carries. Required by
484-
/// [`ClassifyRpcOutcome::classified`], so a response that never answered cannot be classified.
481+
/// [`ClassifyRpcOutcome::classified`], so an undeclared response type cannot be classified.
485482
pub(crate) trait HasAbsenceMeaning {
486483
const ABSENCE: AbsenceMeaning;
487484
}
488485

489-
/// Reads a chain client's outcome as an inspection outcome: the response type supplies what
490-
/// absence means, so the call site supplies nothing. One implementation per transport, each
491-
/// holding that chain's status table.
486+
/// Reads a chain client's outcome as an inspection outcome. The absence meaning comes from the
487+
/// response type.
492488
pub(crate) trait ClassifyRpcOutcome {
493-
type Response;
489+
type Response: HasAbsenceMeaning;
494490

495491
fn classified(self) -> Result<Self::Response, ForeignChainInspectionError>;
496492
}

crates/foreign-chain-inspector/tests/aptos_inspector.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async fn extract__should_classify_http_errors_by_status(
211211

212212
#[tokio::test]
213213
async fn extract__should_reject_a_response_that_does_not_carry_the_resource() {
214-
// Given — a URL that answers but serves something other than the Aptos REST API.
214+
// Given
215215
let server = MockServer::start();
216216
server.mock(|when, then| {
217217
when.method(GET).path(tx_path());
@@ -226,7 +226,7 @@ async fn extract__should_reject_a_response_that_does_not_carry_the_resource() {
226226
.extract(tx_id(), AptosFinality::Committed, vec![])
227227
.await;
228228

229-
// Then — the endpoint is wrong, not slow, so retrying it cannot help.
229+
// Then
230230
let error = response.expect_err("extract should fail");
231231
assert_matches!(error, ForeignChainInspectionError::MalformedRpcResponse(_));
232232
assert!(!error.is_transient());

crates/foreign-chain-rpc-interfaces/src/aptos.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ pub struct EventGuid {
4444
#[derive(Debug, thiserror::Error)]
4545
pub enum AptosRpcError {
4646
#[error("HTTP request failed: {0}")]
47-
Http(reqwest::Error),
47+
Http(#[from] reqwest::Error),
4848
#[error("Aptos API returned HTTP {status}: {body}")]
4949
ApiError { status: u16, body: String },
5050
#[error("failed to decode the Aptos API response: {0}")]
51-
MalformedBody(serde_json::Error),
51+
MalformedBody(#[from] serde_json::Error),
5252
}
5353

5454
/// Partial response of the API root: the ledger info every Aptos node reports.
@@ -110,12 +110,7 @@ impl ReqwestAptosClient {
110110
}
111111

112112
async fn get_json<T: DeserializeOwned>(&self, url: Url) -> Result<T, AptosRpcError> {
113-
let response = self
114-
.client
115-
.get(url)
116-
.send()
117-
.await
118-
.map_err(AptosRpcError::Http)?;
113+
let response = self.client.get(url).send().await?;
119114
let status = response.status();
120115
if !status.is_success() {
121116
let body = response.text().await.unwrap_or_default();
@@ -125,8 +120,8 @@ impl ReqwestAptosClient {
125120
});
126121
}
127122

128-
let body = response.bytes().await.map_err(AptosRpcError::Http)?;
129-
serde_json::from_slice(&body).map_err(AptosRpcError::MalformedBody)
123+
let body = response.bytes().await?;
124+
Ok(serde_json::from_slice(&body)?)
130125
}
131126
}
132127

0 commit comments

Comments
 (0)