Skip to content

feat(sdk): return state transition execution error #2454

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

Merged
merged 7 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -14,7 +14,6 @@ const {
},
} = require('@dashevo/dapi-grpc');

const cbor = require('cbor');
const UnavailableGrpcError = require('@dashevo/grpc-common/lib/server/error/UnavailableGrpcError');
const TransactionWaitPeriodExceededError = require('../../../errors/TransactionWaitPeriodExceededError');
const TransactionErrorResult = require('../../../externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult');
Expand Down Expand Up @@ -49,9 +48,13 @@ function waitForStateTransitionResultHandlerFactory(

const error = new StateTransitionBroadcastError();

const metadata = grpcError.getRawMetadata();
if (metadata['dash-serialized-consensus-error-bin']) {
error.setData(metadata['dash-serialized-consensus-error-bin']);
}

error.setCode(txDeliverResult.code);
error.setMessage(grpcError.getMessage());
error.setData(cbor.encode(grpcError.getRawMetadata()));

return error;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('waitForStateTransitionResultHandlerFactory', () => {
errorInfo = {
message: 'Identity not found',
metadata: {
error: 'some data',
'dash-serialized-consensus-error-bin': Buffer.from('0122a249dac309c9a8b775316c905688da04bf0ee05b3861db05814540c32fba4179', 'hex'),
},
};

Expand Down Expand Up @@ -305,26 +305,28 @@ describe('waitForStateTransitionResultHandlerFactory', () => {

it('should wait for state transition and return result with error', (done) => {
waitForStateTransitionResultHandler(call).then((result) => {
expect(result).to.be.an.instanceOf(WaitForStateTransitionResultResponse);
expect(result.getV0().getProof()).to.be.undefined();

const error = result.getV0().getError();
expect(error).to.be.an.instanceOf(StateTransitionBroadcastError);

const errorData = error.getData();
const errorCode = error.getCode();
const errorMessage = error.getMessage();

expect(createGrpcErrorFromDriveResponseMock).to.be.calledOnceWithExactly(
wsMessagesFixture.error.data.value.result.code,
wsMessagesFixture.error.data.value.result.info,
);

expect(errorCode).to.equal(wsMessagesFixture.error.data.value.result.code);
expect(errorData).to.deep.equal(cbor.encode(errorInfo.metadata));
expect(errorMessage).to.equal(errorInfo.message);

done();
try {
expect(result).to.be.an.instanceOf(WaitForStateTransitionResultResponse);
expect(result.getV0().getProof()).to.be.undefined();

const error = result.getV0().getError();
expect(error).to.be.an.instanceOf(StateTransitionBroadcastError);

const errorData = error.getData();
const errorCode = error.getCode();
const errorMessage = error.getMessage();

expect(createGrpcErrorFromDriveResponseMock).to.be.calledOnceWithExactly(
wsMessagesFixture.error.data.value.result.code,
wsMessagesFixture.error.data.value.result.info,
);

expect(errorCode).to.equal(wsMessagesFixture.error.data.value.result.code);
expect(errorData).to.deep.equal(cbor.encode(errorInfo.metadata));
expect(errorMessage).to.equal(errorInfo.message);
} finally {
done();
}
});

process.nextTick(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class ErrorResult {
/**
* @param {number} code
* @param {string} message
* @param {*} data
* @param {Buffer|undefined} data
*/
constructor(code, message, data) {
this.code = code;
Expand All @@ -25,7 +25,7 @@ class ErrorResult {
}

/**
* @returns {*}
* @returns {Buffer|undefined}
*/
getData() {
return this.data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const cbor = require('cbor');

const AbstractResponse = require('../response/AbstractResponse');
const Metadata = require('../response/Metadata');
const Proof = require('../response/Proof');
Expand Down Expand Up @@ -38,9 +36,9 @@ class WaitForStateTransitionResultResponse extends AbstractResponse {

if (proto.getV0().getError()) {
let data;
const rawData = proto.getV0().getError().getData();
if (rawData) {
data = cbor.decode(Buffer.from(rawData));

if (proto.getV0().getError().getData()) {
data = Buffer.from(proto.getV0().getError().getData());
}

error = new ErrorResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ describe('waitForStateTransitionResultFactory', () => {
});

it('should return response with error', async () => {
const data = cbor.encode({ data: 'error data' });

const error = new StateTransitionBroadcastError();
error.setCode(2);
error.setMessage('Some error');
error.setData(cbor.encode({ data: 'error data' }));
error.setData(data);

response.getV0().setError(error);

Expand All @@ -135,7 +137,7 @@ describe('waitForStateTransitionResultFactory', () => {
expect(result.getError()).to.be.deep.equal({
code: 2,
message: 'Some error',
data: { data: 'error data' },
data: Buffer.from(data),
});

const { WaitForStateTransitionResultRequestV0 } = WaitForStateTransitionResultRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export interface IStateTransitionResult {
error?: {
code: number,
message: string,
data: any,
data?: Buffer,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ export default async function broadcastStateTransition(
// instead of passing it as GrpcError constructor argument
// Otherwise it will be converted to grpc-js metadata
// Which is not compatible with web
grpcError.metadata = error.data;
if (error.data) {
grpcError.metadata = {
'dash-serialized-consensus-error-bin': error.data.toString('base64'),
};
}

let cause = await createGrpcTransportError(grpcError);

Expand Down
42 changes: 42 additions & 0 deletions packages/rs-sdk/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Definitions of errors
use dapi_grpc::platform::v0::StateTransitionBroadcastError as StateTransitionBroadcastErrorProto;
use dapi_grpc::tonic::Code;
use dpp::consensus::ConsensusError;
use dpp::serialization::PlatformDeserializable;
Expand Down Expand Up @@ -77,6 +78,47 @@ pub enum Error {
/// Remote node is stale; try another server
#[error(transparent)]
StaleNode(#[from] StaleNodeError),

/// Error returned when trying to broadcast a state transition
#[error(transparent)]
StateTransitionBroadcastError(#[from] StateTransitionBroadcastError),
}

/// State transition broadcast error
#[derive(Debug, thiserror::Error)]
#[error("state transition broadcast error: {message}")]
pub struct StateTransitionBroadcastError {
/// Error code
pub code: u32,
/// Error message
pub message: String,
/// Consensus error caused the state transition broadcast error
pub cause: Option<ConsensusError>,
}

impl TryFrom<StateTransitionBroadcastErrorProto> for StateTransitionBroadcastError {
type Error = Error;

fn try_from(value: StateTransitionBroadcastErrorProto) -> Result<Self, Self::Error> {
let cause = if value.data.len() > 0 {
let consensus_error =
ConsensusError::deserialize_from_bytes(&value.data).map_err(|e| {
tracing::debug!("Failed to deserialize consensus error: {}", e);

Error::Protocol(e)
})?;

Some(consensus_error)
} else {
None
};

Ok(Self {
code: value.code,
message: value.message,
cause,
})
}
}

// TODO: Decompose DapiClientError to more specific errors like connection, node error instead of DAPI client error
Expand Down
30 changes: 29 additions & 1 deletion packages/rs-sdk/src/platform/transition/broadcast.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use super::broadcast_request::BroadcastRequestForStateTransition;
use super::put_settings::PutSettings;
use crate::error::StateTransitionBroadcastError;
use crate::platform::block_info_from_metadata::block_info_from_metadata;
use crate::sync::retry;
use crate::{Error, Sdk};
use dapi_grpc::platform::v0::{Proof, WaitForStateTransitionResultResponse};
use dapi_grpc::platform::v0::wait_for_state_transition_result_response::wait_for_state_transition_result_response_v0;
use dapi_grpc::platform::v0::{
wait_for_state_transition_result_response, Proof, WaitForStateTransitionResultResponse,
};
use dapi_grpc::platform::VersionedGrpcResponse;
use dpp::state_transition::proof_result::StateTransitionProofResult;
use dpp::state_transition::StateTransition;
Expand Down Expand Up @@ -80,6 +84,30 @@ impl BroadcastStateTransition for StateTransition {
let response = request.execute(sdk, request_settings).await.inner_into()?;

let grpc_response: &WaitForStateTransitionResultResponse = &response.inner;

// We use match here to have a compilation error if a new version of the response is introduced
let state_transition_broadcast_error = match &grpc_response.version {
Some(wait_for_state_transition_result_response::Version::V0(result)) => {
match &result.result {
Some(wait_for_state_transition_result_response_v0::Result::Error(e)) => {
Some(e)
}
_ => None,
}
}
None => None,
};

if let Some(e) = state_transition_broadcast_error {
let state_transition_broadcast_error: StateTransitionBroadcastError =
StateTransitionBroadcastError::try_from(e.clone())
.wrap_to_execution_result(&response)?
.inner;

return Err(Error::from(state_transition_broadcast_error))
.wrap_to_execution_result(&response);
}

let metadata = grpc_response
.metadata()
.wrap_to_execution_result(&response)?
Expand Down
Loading