Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk)!: return consensus errors from broadcast methods #2274

Merged
merged 10 commits into from
Oct 29, 2024
  •  
  •  
  •  
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;

shumkov marked this conversation as resolved.
Show resolved Hide resolved
// 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),
};
shumkov marked this conversation as resolved.
Show resolved Hide resolved

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');
shumkov marked this conversation as resolved.
Show resolved Hide resolved
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