Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ describe('Platform', () => {
expect(broadcastError.getCause()).to.be.an.instanceOf(IdentityNotFoundError);
});

it('should expose validation error when document property positions are not contiguous', async () => {
// Additional wait time to mitigate testnet latency
await waitForSTPropagated();

const identityNonce = await client.platform.nonceManager
.bumpIdentityNonce(identity.getId());
const invalidDataContract = await getDataContractFixture(identityNonce, identity.getId());

const documentSchema = invalidDataContract.getDocumentSchema('niceDocument');
documentSchema.properties.name.position = 5;
invalidDataContract.setDocumentSchema('niceDocument', documentSchema, { skipValidation: true });

let broadcastError;

try {
await client.platform.contracts.publish(invalidDataContract, identity);
} catch (e) {
broadcastError = e;
}

expect(broadcastError).to.be.an.instanceOf(StateTransitionBroadcastError);
expect(broadcastError.getCode()).to.equal(10411);
expect(broadcastError.getMessage()).to.equal('position field is not present for document type "niceDocument"');
});

it('should create new data contract with previously created identity as an owner', async () => {
// Additional wait time to mitigate testnet latency
await waitForSTPropagated();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::error::DapiError;
use crate::services::PlatformServiceImpl;
use crate::services::platform_service::TenderdashStatus;
use crate::services::platform_service::error_mapping::decode_consensus_error;
use crate::services::platform_service::error_mapping::map_tenderdash_message;
use base64::prelude::*;
use dapi_grpc::platform::v0::{BroadcastStateTransitionRequest, BroadcastStateTransitionResponse};
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -217,34 +218,11 @@ fn map_broadcast_error(code: u32, error_message: &str, info: Option<&str>) -> Da
code,
error_message
);
if error_message == "tx already exists in cache" {
return DapiError::AlreadyExists(error_message.to_string());
}

if error_message.starts_with("Tx too large.") {
let message = error_message.replace("Tx too large. ", "");
return DapiError::InvalidArgument(
"state transition is too large. ".to_string() + &message,
);
}

if error_message.starts_with("mempool is full") {
return DapiError::ResourceExhausted(error_message.to_string());
}

if error_message.contains("context deadline exceeded") {
return DapiError::Timeout("broadcasting state transition is timed out".to_string());
}

if error_message.contains("too_many_requests") {
return DapiError::ResourceExhausted(
"tenderdash is not responding: too many requests".to_string(),
);
if let Some(mapped_error) = map_tenderdash_message(error_message) {
return mapped_error;
}

if error_message.starts_with("broadcast confirmation not received:") {
return DapiError::Timeout(error_message.to_string());
}
let consensus_error = info.and_then(|x| decode_consensus_error(x.to_string()));
let message = if error_message.is_empty() {
None
Expand Down
68 changes: 63 additions & 5 deletions packages/rs-dapi/src/services/platform_service/error_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ use dpp::{consensus::ConsensusError, serialization::PlatformDeserializable};
use std::{fmt::Debug, str::FromStr};
use tonic::{Code, metadata::MetadataValue};

use crate::DapiError;

#[derive(Clone, serde::Serialize)]
pub struct TenderdashStatus {
pub code: i64,
// human-readable error message; will be put into `data` field
/// human-readable error message; will be put into `data` field
/// Access using [`TenderdashStatus::grpc_message()`].
pub message: Option<String>,
// CBOR-encoded dpp ConsensusError
/// CBOR-encoded dpp ConsensusError
pub consensus_error: Option<Vec<u8>>,
}

Expand Down Expand Up @@ -39,6 +42,14 @@ impl TenderdashStatus {
let status_code = self.grpc_code();
let status_message = self.grpc_message();

// check if we can map to a DapiError first
if let Some(dapi_error) = map_tenderdash_message(&status_message) {
Copy link
Member

Choose a reason for hiding this comment

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

can we not do this by code? This seems very brittle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We would need to also refactor tenderdash. Right now, it returns -32603 which is CodeInternalError for multiple error types.

I'd prefer to leave it behind and fix it separately.

// avoid infinite recursion
if !matches!(dapi_error, DapiError::TenderdashClientError(_)) {
return dapi_error.to_status();
}
}

let mut status: tonic::Status = tonic::Status::new(status_code, status_message);

self.write_grpc_metadata(status.metadata_mut());
Expand Down Expand Up @@ -141,7 +152,7 @@ impl From<TenderdashStatus> for StateTransitionBroadcastError {
fn from(err: TenderdashStatus) -> Self {
StateTransitionBroadcastError {
code: err.code.clamp(0, u32::MAX as i64) as u32,
message: err.message.unwrap_or_else(|| "Unknown error".to_string()),
message: err.grpc_message(),
data: err.consensus_error.clone().unwrap_or_default(),
}
}
Expand Down Expand Up @@ -273,10 +284,22 @@ impl From<serde_json::Value> for TenderdashStatus {
tracing::debug!("Tenderdash error missing 'code' field, defaulting to 0");
0
});
let message = object
let raw_message = object
.get("message")
.and_then(|m| m.as_str())
.map(|s| s.to_string());
.map(|m| m.trim());
// empty message or "Internal error" is not very informative, so we try to check `data` field
let message = if raw_message
.is_none_or(|m| m.is_empty() || m.eq_ignore_ascii_case("Internal error"))
{
object
.get("data")
.and_then(|d| d.as_str())
.filter(|s| s.is_ascii())
} else {
raw_message
}
.map(|s| s.to_string());

// info contains additional error details, possibly including consensus error
let consensus_error = object
Expand All @@ -300,6 +323,41 @@ impl From<serde_json::Value> for TenderdashStatus {
}
}

// Map some common Tenderdash error messages to DapiError variants
pub(super) fn map_tenderdash_message(message: &str) -> Option<DapiError> {
let msg = message.trim().to_lowercase();
if msg == "tx already exists in cache" {
return Some(DapiError::AlreadyExists(msg.to_string()));
}

if msg.starts_with("tx too large.") {
let message = msg.replace("tx too large.", "").trim().to_string();
return Some(DapiError::InvalidArgument(
"state transition is too large. ".to_string() + &message,
));
}

if msg.starts_with("mempool is full") {
return Some(DapiError::ResourceExhausted(msg.to_string()));
}

if msg.contains("context deadline exceeded") {
return Some(DapiError::Timeout(
"broadcasting state transition is timed out".to_string(),
));
}

if msg.contains("too_many_requests") {
return Some(DapiError::ResourceExhausted(
"tenderdash is not responding: too many requests".to_string(),
));
}

if msg.starts_with("broadcast confirmation not received:") {
return Some(DapiError::Timeout(msg.to_string()));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading