Skip to content
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ async function createGrpcErrorFromDriveResponse(code, info) {
const message = decodedInfo.message;
const data = decodedInfo.data || {};

const serializedConsensusError = data.serializedError;
delete data.serializedError;

// gRPC error codes
if (code <= 16) {
const CommonErrorClass = COMMON_ERROR_CLASSES[code.toString()];
Expand Down Expand Up @@ -111,9 +114,15 @@ async function createGrpcErrorFromDriveResponse(code, info) {

// DPP errors
if (code >= 10000 && code < 50000) {
const consensusMetadata = {
...createRawMetadata(data),
code,
'dash-serialized-consensus-error-bin': Buffer.from(serializedConsensusError),
};

let consensusError;
try {
consensusError = deserializeConsensusError(data.serializedError || []);
consensusError = deserializeConsensusError(serializedConsensusError);
} catch (e) {
logger.error({
err: e,
Expand All @@ -128,7 +137,7 @@ async function createGrpcErrorFromDriveResponse(code, info) {
if (code >= 10000 && code < 20000) {
return new InvalidArgumentGrpcError(
consensusError.message,
{ code, ...createRawMetadata(data) },
consensusMetadata,
);
}

Expand All @@ -137,23 +146,23 @@ async function createGrpcErrorFromDriveResponse(code, info) {
return new GrpcError(
GrpcErrorCodes.UNAUTHENTICATED,
consensusError.message,
{ code, ...createRawMetadata(data) },
consensusMetadata,
);
}

// Fee
if (code >= 30000 && code < 40000) {
return new FailedPreconditionGrpcError(
consensusError.message,
{ code, ...createRawMetadata(data) },
consensusMetadata,
);
}

// State
if (code >= 40000 && code < 50000) {
return new InvalidArgumentGrpcError(
consensusError.message,
{ code, ...createRawMetadata(data) },
consensusMetadata,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ describe('createGrpcErrorFromDriveResponse', () => {
it('should throw basic consensus error if error code = 10000', async () => {
const consensusError = new ProtocolVersionParsingError('test');

const data = { serializedError: consensusError.serialize() };
const serializedError = consensusError.serialize();
const data = { serializedError };
info = { data };

const error = await createGrpcErrorFromDriveResponse(10000, cbor.encode(info).toString('base64'));
Expand All @@ -78,7 +79,7 @@ describe('createGrpcErrorFromDriveResponse', () => {
expect(error.message).to.be.equals(consensusError.message);
expect(error.getRawMetadata()).to.deep.equal({
code: 10000,
'drive-error-data-bin': cbor.encode(data),
'dash-serialized-consensus-error-bin': serializedError,
});
});

Expand All @@ -87,7 +88,8 @@ describe('createGrpcErrorFromDriveResponse', () => {

const consensusError = new IdentityNotFoundError(id);

const data = { serializedError: consensusError.serialize() };
const serializedError = consensusError.serialize();
const data = { serializedError };
info = { data };

const error = await createGrpcErrorFromDriveResponse(
Expand All @@ -100,22 +102,23 @@ describe('createGrpcErrorFromDriveResponse', () => {
expect(error.getCode()).to.equal(GrpcErrorCodes.UNAUTHENTICATED);
expect(error.getRawMetadata()).to.deep.equal({
code: 20000,
'drive-error-data-bin': cbor.encode(data),
'dash-serialized-consensus-error-bin': serializedError,
});
});

it('should throw fee consensus error if error code = 30000', async () => {
const consensusError = new BalanceIsNotEnoughError(BigInt(20), BigInt(10));

const data = { serializedError: consensusError.serialize() };
const serializedError = consensusError.serialize();
const data = { serializedError };
info = { data };

const error = await createGrpcErrorFromDriveResponse(30000, cbor.encode(info).toString('base64'));

expect(error).to.be.an.instanceOf(FailedPreconditionGrpcError);
expect(error.getRawMetadata()).to.deep.equal({
code: 30000,
'drive-error-data-bin': cbor.encode(data),
'dash-serialized-consensus-error-bin': serializedError,
});
});

Expand All @@ -124,7 +127,8 @@ describe('createGrpcErrorFromDriveResponse', () => {

const consensusError = new DataContractAlreadyPresentError(dataContractId);

const data = { serializedError: consensusError.serialize() };
const serializedError = consensusError.serialize();
const data = { serializedError };
info = { data };

const error = await createGrpcErrorFromDriveResponse(
Expand All @@ -135,7 +139,7 @@ describe('createGrpcErrorFromDriveResponse', () => {
expect(error).to.be.an.instanceOf(InvalidArgumentGrpcError);
expect(error.getRawMetadata()).to.deep.equal({
code: 40000,
'drive-error-data-bin': cbor.encode(data),
'dash-serialized-consensus-error-bin': serializedError,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,13 @@ async function createGrpcTransportError(grpcError, dapiAddress) {

// DPP consensus errors
if (code >= 10000 && code < 50000) {
const consensusError = deserializeConsensusError(data.serializedError || []);
const consensusErrorString = metadata['dash-serialized-consensus-error-bin'];
if (!consensusErrorString) {
throw new Error(`Can't deserialize consensus error ${code}: serialized data is missing`);
}

const consensusErrorBytes = Buffer.from(consensusErrorString, 'base64');
const consensusError = deserializeConsensusError(consensusErrorBytes);

delete data.serializedError;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,14 @@ describe('createGrpcTransportError', () => {

it('should return InvalidRequestDPPError', async () => {
// grpc-js expects Buffer
let driveErrorDataBin = cbor.encode({
serializedError: new ProtocolVersionParsingError('test').serialize(),
...errorData,
});
let serializedError = new ProtocolVersionParsingError('test').serialize();

// and grpc-web expects string
// TODO: remove when we switch to single grpc implementation for both Node and Web
if (typeof window !== 'undefined') {
driveErrorDataBin = driveErrorDataBin.toString('base64');
serializedError = serializedError.toString('base64');
}
metadata.set('drive-error-data-bin', driveErrorDataBin);
metadata.set('dash-serialized-consensus-error-bin', serializedError);

const grpcError = new GrpcError(
10001,
Expand Down
34 changes: 16 additions & 18 deletions packages/rs-dapi-client/src/dapi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ use tracing::Instrument;

use crate::address_list::AddressListError;
use crate::connection_pool::ConnectionPool;
use crate::transport::TransportError;
use crate::{
transport::{TransportClient, TransportRequest},
Address, AddressList, CanRetry, DapiRequestExecutor, ExecutionError, ExecutionResponse,
ExecutionResult, RequestSettings,
AddressList, CanRetry, DapiRequestExecutor, ExecutionError, ExecutionResponse, ExecutionResult,
RequestSettings,
};

/// General DAPI request error type.
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
pub enum DapiClientError<TE: Mockable> {
pub enum DapiClientError {
/// The error happened on transport layer
#[error("transport error: {0}")]
Transport(#[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))] TE),
Transport(
#[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))]
TransportError,
),
/// There are no valid DAPI addresses to use.
#[error("no available addresses to use")]
NoAvailableAddresses,
Expand All @@ -37,7 +41,7 @@ pub enum DapiClientError<TE: Mockable> {
Mock(#[from] crate::mock::MockError),
}

impl<TE: CanRetry + Mockable> CanRetry for DapiClientError<TE> {
impl CanRetry for DapiClientError {
fn can_retry(&self) -> bool {
use DapiClientError::*;
match self {
Expand All @@ -50,17 +54,10 @@ impl<TE: CanRetry + Mockable> CanRetry for DapiClientError<TE> {
}
}

#[cfg(feature = "mocks")]
#[derive(serde::Serialize, serde::Deserialize)]
struct TransportErrorData {
transport_error: Vec<u8>,
address: Address,
}

/// Serialization of [DapiClientError].
///
/// We need to do manual serialization because of the generic type parameter which doesn't support serde derive.
impl<TE: Mockable> Mockable for DapiClientError<TE> {
impl Mockable for DapiClientError {
#[cfg(feature = "mocks")]
fn mock_serialize(&self) -> Option<Vec<u8>> {
Some(serde_json::to_vec(self).expect("serialize DAPI client error"))
Expand Down Expand Up @@ -110,11 +107,11 @@ impl DapiRequestExecutor for DapiClient {
&self,
request: R,
settings: RequestSettings,
) -> ExecutionResult<R::Response, DapiClientError<<R::Client as TransportClient>::Error>>
) -> ExecutionResult<R::Response, DapiClientError>
where
R: TransportRequest + Mockable,
R::Response: Mockable,
<R::Client as TransportClient>::Error: Mockable,
TransportError: Mockable,
{
// Join settings of different sources to get final version of the settings for this execution:
let applied_settings = self
Expand Down Expand Up @@ -148,9 +145,10 @@ impl DapiRequestExecutor for DapiClient {
.read()
.expect("can't get address list for read");

let address_result = address_list.get_live_address().cloned().ok_or(
DapiClientError::<<R::Client as TransportClient>::Error>::NoAvailableAddresses,
);
let address_result = address_list
.get_live_address()
.cloned()
.ok_or(DapiClientError::NoAvailableAddresses);
Comment on lines +148 to +151
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle NoAvailableAddresses error correctly

The change at lines 148-151 introduces DapiClientError::NoAvailableAddresses when there are no live addresses. Verify that this error is handled appropriately in calling code, and that user-facing error messages are clear when this condition occurs.

Consider adding user-friendly messages or logging to help diagnose issues when no addresses are available, enhancing the developer experience during debugging.


drop(address_list);

Expand Down
7 changes: 3 additions & 4 deletions packages/rs-dapi-client/src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::transport::{TransportClient, TransportRequest};
use crate::transport::TransportRequest;
use crate::{Address, CanRetry, DapiClientError, RequestSettings};
use dapi_grpc::mock::Mockable;
use dapi_grpc::tonic::async_trait;
Expand All @@ -12,11 +12,10 @@ pub trait DapiRequestExecutor {
&self,
request: R,
settings: RequestSettings,
) -> ExecutionResult<R::Response, DapiClientError<<R::Client as TransportClient>::Error>>
) -> ExecutionResult<R::Response, DapiClientError>
where
R: TransportRequest + Mockable,
R::Response: Mockable,
<R::Client as TransportClient>::Error: Mockable;
R::Response: Mockable;
}

/// Unwrap wrapped types
Expand Down
11 changes: 3 additions & 8 deletions packages/rs-dapi-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub use address_list::AddressList;
pub use address_list::AddressListError;
pub use connection_pool::ConnectionPool;
pub use dapi_client::{DapiClient, DapiClientError};
use dapi_grpc::mock::Mockable;
#[cfg(feature = "dump")]
pub use dump::DumpData;
pub use executor::{
Expand All @@ -38,21 +37,19 @@ pub use request_settings::RequestSettings;
/// let mut client = MockDapiClient::new();
/// let request: proto::GetIdentityRequest = proto::get_identity_request::GetIdentityRequestV0 { id: b"0".to_vec(), prove: true }.into();
/// let response = request.execute(&mut client, RequestSettings::default()).await?;
/// # Ok::<(), ExecutionError<DapiClientError<_>>>(())
/// # Ok::<(), ExecutionError<DapiClientError>>(())
/// # };
/// ```
pub trait DapiRequest {
/// Response from DAPI for this specific request.
type Response;
/// An error type for the transport this request uses.
type TransportError: Mockable;

/// Executes the request.
fn execute<'c, D: DapiRequestExecutor>(
self,
dapi_client: &'c D,
settings: RequestSettings,
) -> BoxFuture<'c, ExecutionResult<Self::Response, DapiClientError<Self::TransportError>>>
) -> BoxFuture<'c, ExecutionResult<Self::Response, DapiClientError>>
where
Self: 'c;
}
Expand All @@ -61,13 +58,11 @@ pub trait DapiRequest {
impl<T: transport::TransportRequest + Send> DapiRequest for T {
type Response = T::Response;

type TransportError = <T::Client as transport::TransportClient>::Error;

fn execute<'c, D: DapiRequestExecutor>(
self,
dapi_client: &'c D,
settings: RequestSettings,
) -> BoxFuture<'c, ExecutionResult<Self::Response, DapiClientError<Self::TransportError>>>
) -> BoxFuture<'c, ExecutionResult<Self::Response, DapiClientError>>
where
Self: 'c,
{
Expand Down
10 changes: 3 additions & 7 deletions packages/rs-dapi-client/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
//! See `tests/mock_dapi_client.rs` for an example.

use crate::{
transport::{TransportClient, TransportRequest},
DapiClientError, DapiRequestExecutor, ExecutionError, ExecutionResponse, ExecutionResult,
RequestSettings,
transport::TransportRequest, DapiClientError, DapiRequestExecutor, ExecutionError,
ExecutionResponse, ExecutionResult, RequestSettings,
};
use dapi_grpc::mock::Mockable;
use dapi_grpc::tonic::async_trait;
Expand All @@ -36,10 +35,7 @@ pub struct MockDapiClient {
expectations: Expectations,
}
/// Result of executing a mock request
pub type MockResult<R> = ExecutionResult<
<R as TransportRequest>::Response,
DapiClientError<<<R as TransportRequest>::Client as TransportClient>::Error>,
>;
pub type MockResult<T> = ExecutionResult<<T as TransportRequest>::Response, DapiClientError>;

impl MockDapiClient {
/// Create a new mock client
Expand Down
Loading
Loading