Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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