Skip to content
Open
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) {
// 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