Skip to content
Open
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
6 changes: 4 additions & 2 deletions packages/dapi-grpc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
// Derive features for versioned messages
//
// "GetConsensusParamsRequest" is excluded as this message does not support proofs
const VERSIONED_REQUESTS: [&str; 56] = [
const VERSIONED_REQUESTS: [&str; 57] = [
"GetDataContractHistoryRequest",
"GetDataContractRequest",
"GetDataContractsRequest",
"GetDocumentHistoryRequest",
"GetDocumentsRequest",
"GetIdentitiesByPublicKeyHashesRequest",
"GetIdentitiesRequest",
Expand Down Expand Up @@ -161,10 +162,11 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
// - "GetIdentityByNonUniquePublicKeyHashResponse"
//
// "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests
const VERSIONED_RESPONSES: [&str; 54] = [
const VERSIONED_RESPONSES: [&str; 55] = [
"GetDataContractHistoryResponse",
"GetDataContractResponse",
"GetDataContractsResponse",
"GetDocumentHistoryResponse",
"GetDocumentsResponse",
"GetIdentitiesByPublicKeyHashesResponse",
"GetIdentitiesResponse",
Expand Down
45 changes: 45 additions & 0 deletions packages/dapi-grpc/protos/platform/v0/platform.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ service Platform {
returns (GetDataContractHistoryResponse);
rpc getDataContracts(GetDataContractsRequest)
returns (GetDataContractsResponse);
rpc getDocumentHistory(GetDocumentHistoryRequest)
returns (GetDocumentHistoryResponse);
rpc getDocuments(GetDocumentsRequest) returns (GetDocumentsResponse);
rpc getIdentityByPublicKeyHash(GetIdentityByPublicKeyHashRequest)
returns (GetIdentityByPublicKeyHashResponse);
Expand Down Expand Up @@ -1300,6 +1302,49 @@ message GetDocumentsResponse {
}
}

message GetDocumentHistoryRequest {
message GetDocumentHistoryRequestV0 {
bytes data_contract_id = 1; // The ID of the data contract
string document_type_name = 2; // The document type name
bytes document_id = 3; // The document ID
google.protobuf.UInt32Value limit =
4; // The maximum number of history entries to return
google.protobuf.UInt32Value offset =
5; // The offset for pagination through the document history
uint64 start_at_ms = 6 [
jstype = JS_STRING
]; // Only return results after this time in milliseconds
bool prove = 7; // Flag to request a proof as the response
}
oneof version { GetDocumentHistoryRequestV0 v0 = 1; }
}

message GetDocumentHistoryResponse {
message GetDocumentHistoryResponseV0 {
// Represents a single entry in a document's history
message DocumentHistoryEntry {
uint64 date = 1 [ jstype = JS_STRING ]; // The date of the history entry
bytes value = 2; // The value of the document at this point in history
}

// Collection of document history entries
message DocumentHistory {
repeated DocumentHistoryEntry document_entries =
1; // List of history entries
}

oneof result {
DocumentHistory document_history =
1; // The actual history of the document
Proof proof =
2; // Cryptographic proof of the document history, if requested
}

ResponseMetadata metadata = 3; // Metadata about the blockchain state
}
oneof version { GetDocumentHistoryResponseV0 v0 = 1; }
}

message GetIdentityByPublicKeyHashRequest {
message GetIdentityByPublicKeyHashRequestV0 {
bytes public_key_hash =
Expand Down
12 changes: 12 additions & 0 deletions packages/js-evo-sdk/src/documents/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export class DocumentsFacade {
return w.getDocumentsWithProofInfo(query);
}

async history(query: wasm.DocumentHistoryQuery): Promise<Map<bigint, wasm.Document>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentHistory(query);
}

async historyWithProof(
query: wasm.DocumentHistoryQuery,
): Promise<wasm.ProofMetadataResponseTyped<Map<bigint, wasm.Document>>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getDocumentHistoryWithProofInfo(query);
}

async get(contractId: wasm.IdentifierLike, type: string, documentId: wasm.IdentifierLike):
Promise<wasm.Document | undefined> {
const w = await this.sdk.getWasmSdkConnected();
Expand Down
42 changes: 42 additions & 0 deletions packages/js-evo-sdk/tests/unit/facades/documents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ describe('DocumentsFacade', () => {
// Stub references for type-safe assertions
let getDocumentsStub: SinonStub;
let getDocumentsWithProofInfoStub: SinonStub;
let getDocumentHistoryStub: SinonStub;
let getDocumentHistoryWithProofInfoStub: SinonStub;
let getDocumentStub: SinonStub;
let getDocumentWithProofInfoStub: SinonStub;
let documentCreateStub: SinonStub;
Expand Down Expand Up @@ -46,6 +48,15 @@ describe('DocumentsFacade', () => {
proof: {},
metadata: {},
});
getDocumentHistoryStub = this.sinon.stub(wasmSdk, 'getDocumentHistory').resolves(new Map());
getDocumentHistoryWithProofInfoStub = this.sinon.stub(
wasmSdk,
'getDocumentHistoryWithProofInfo',
).resolves({
data: new Map(),
proof: {},
metadata: {},
});
getDocumentStub = this.sinon.stub(wasmSdk, 'getDocument').resolves(document);
getDocumentWithProofInfoStub = this.sinon.stub(wasmSdk, 'getDocumentWithProofInfo').resolves({
data: document,
Expand Down Expand Up @@ -91,6 +102,37 @@ describe('DocumentsFacade', () => {
});
});

describe('history()', () => {
it('should fetch document history', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'note',
documentId: '4mZmxva49PBb7BE7srw9o3gixvDfj1dAx1K6z4A7P9Ah',
startAtMs: 1000,
limit: 10,
offset: 1,
};

await client.documents.history(query);

expect(getDocumentHistoryStub).to.be.calledOnceWithExactly(query);
});
});

describe('historyWithProof()', () => {
it('should fetch document history with proof metadata', async () => {
const query = {
dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
documentTypeName: 'note',
documentId: '4mZmxva49PBb7BE7srw9o3gixvDfj1dAx1K6z4A7P9Ah',
};

await client.documents.historyWithProof(query);

expect(getDocumentHistoryWithProofInfoStub).to.be.calledOnceWithExactly(query);
});
});

describe('get()', () => {
it('should fetch a single document by ID', async () => {
const contractId = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec';
Expand Down
8 changes: 8 additions & 0 deletions packages/rs-dapi-client/src/transport/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ impl_transport_request_grpc!(
get_data_contract_history
);

impl_transport_request_grpc!(
platform_proto::GetDocumentHistoryRequest,
platform_proto::GetDocumentHistoryResponse,
PlatformGrpcClient,
RequestSettings::default(),
get_document_history
);

impl_transport_request_grpc!(
platform_proto::BroadcastStateTransitionRequest,
platform_proto::BroadcastStateTransitionResponse,
Expand Down
55 changes: 55 additions & 0 deletions packages/rs-drive-abci/src/query/document_history/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::error::query::QueryError;
use crate::error::Error;
use crate::platform_types::platform::Platform;
use crate::platform_types::platform_state::PlatformState;
use crate::query::QueryValidationResult;
use dapi_grpc::platform::v0::get_document_history_request::Version as RequestVersion;
use dapi_grpc::platform::v0::get_document_history_response::Version as ResponseVersion;
use dapi_grpc::platform::v0::{GetDocumentHistoryRequest, GetDocumentHistoryResponse};
use dpp::version::PlatformVersion;

mod v0;

impl<C> Platform<C> {
/// Querying of a document history.
pub fn query_document_history(
&self,
GetDocumentHistoryRequest { version }: GetDocumentHistoryRequest,
platform_state: &PlatformState,
platform_version: &PlatformVersion,
) -> Result<QueryValidationResult<GetDocumentHistoryResponse>, Error> {
let Some(version) = version else {
return Ok(QueryValidationResult::new_with_error(
QueryError::DecodingError("could not decode document history query".to_string()),
));
};

let feature_version_bounds = &platform_version.drive_abci.query.document_history;

let feature_version = match &version {
RequestVersion::V0(_) => 0,
};
if !feature_version_bounds.check_version(feature_version) {
return Ok(QueryValidationResult::new_with_error(
QueryError::UnsupportedQueryVersion(
"document_history".to_string(),
feature_version_bounds.min_version,
feature_version_bounds.max_version,
platform_version.protocol_version,
feature_version,
),
));
}

match version {
RequestVersion::V0(request_v0) => {
let result =
self.query_document_history_v0(request_v0, platform_state, platform_version)?;

Ok(result.map(|response_v0| GetDocumentHistoryResponse {
version: Some(ResponseVersion::V0(response_v0)),
}))
}
}
}
}
148 changes: 148 additions & 0 deletions packages/rs-drive-abci/src/query/document_history/v0/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use crate::error::query::QueryError;
use crate::error::Error;
use crate::platform_types::platform::Platform;
use crate::platform_types::platform_state::PlatformState;
use crate::query::response_metadata::CheckpointUsed;
use crate::query::QueryValidationResult;
use dapi_grpc::platform::v0::get_document_history_request::GetDocumentHistoryRequestV0;
use dapi_grpc::platform::v0::get_document_history_response::get_document_history_response_v0::DocumentHistoryEntry;
use dapi_grpc::platform::v0::get_document_history_response::{
get_document_history_response_v0, GetDocumentHistoryResponseV0,
};
use dpp::check_validation_result_with_data;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0;
use dpp::identifier::Identifier;
use dpp::validation::ValidationResult;
use dpp::version::PlatformVersion;
use drive::drive::document::MAX_DOCUMENT_HISTORY_FETCH_LIMIT;
use drive::util::grove_operations::GroveDBToUse;

impl<C> Platform<C> {
pub(super) fn query_document_history_v0(
&self,
GetDocumentHistoryRequestV0 {
data_contract_id,
document_type_name,
document_id,
limit,
offset,
start_at_ms,
prove,
}: GetDocumentHistoryRequestV0,
platform_state: &PlatformState,
platform_version: &PlatformVersion,
) -> Result<QueryValidationResult<GetDocumentHistoryResponseV0>, Error> {
let contract_id: Identifier =
check_validation_result_with_data!(data_contract_id.try_into().map_err(|_| {
QueryError::InvalidArgument(
"data_contract_id must be a valid identifier (32 bytes long)".to_string(),
)
}));
let document_id: Identifier =
check_validation_result_with_data!(document_id.try_into().map_err(|_| {
QueryError::InvalidArgument(
"document_id must be a valid identifier (32 bytes long)".to_string(),
)
}));

let limit = check_validation_result_with_data!(limit
.map(|limit| {
let limit = u16::try_from(limit)
.map_err(|_| QueryError::InvalidArgument("limit out of bounds".to_string()))?;

if !(1..=MAX_DOCUMENT_HISTORY_FETCH_LIMIT).contains(&limit) {
return Err(QueryError::InvalidArgument(format!(
"limit {} out of bounds of [1, {}]",
limit, MAX_DOCUMENT_HISTORY_FETCH_LIMIT,
)));
}

Ok(limit)
})
.transpose());

let offset = check_validation_result_with_data!(offset
.map(|offset| {
u16::try_from(offset)
.map_err(|_| QueryError::InvalidArgument("offset out of bounds".to_string()))
})
.transpose());

let maybe_contract_fetch_info = self
.drive
.fetch_contract(contract_id.to_buffer(), None, None, None, platform_version)
.unwrap()?;
let contract_fetch_info = check_validation_result_with_data!(maybe_contract_fetch_info
.ok_or_else(|| {
QueryError::NotFound(format!("data contract {} not found", contract_id))
}));
let contract = &contract_fetch_info.contract;
let document_type = check_validation_result_with_data!(contract
.document_type_for_name(&document_type_name)
.map_err(|_| QueryError::NotFound(format!(
"document type {} not found in data contract {}",
document_type_name, contract_id
))));

let response = if prove {
let proof = self.drive.prove_document_history(
contract_id.to_buffer(),
&document_type_name,
document_id.to_buffer(),
None,
start_at_ms,
limit,
offset,
platform_version,
)?;

GetDocumentHistoryResponseV0 {
result: Some(get_document_history_response_v0::Result::Proof(
self.response_proof_v0(platform_state, proof, GroveDBToUse::Current)
.map(|(_, proof)| proof)?,
)),
metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)),
}
} else {
let documents = self.drive.fetch_document_history(
contract_id.to_buffer(),
&document_type_name,
document_type,
document_id.to_buffer(),
None,
start_at_ms,
limit,
offset,
platform_version,
)?;

if documents.is_empty() {
return Ok(QueryValidationResult::new_with_error(QueryError::NotFound(
format!("document {} history not found", document_id),
)));
}

let document_entries = documents
.into_iter()
.map(|(date, document)| {
Ok(DocumentHistoryEntry {
date,
value: document
.serialize(document_type, contract, platform_version)
.map_err(Error::Protocol)?,
})
})
.collect::<Result<Vec<_>, Error>>()?;

GetDocumentHistoryResponseV0 {
result: Some(get_document_history_response_v0::Result::DocumentHistory(
get_document_history_response_v0::DocumentHistory { document_entries },
)),
metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)),
}
};

Ok(QueryValidationResult::new_with_data(response))
}
}
1 change: 1 addition & 0 deletions packages/rs-drive-abci/src/query/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod address_funds;
mod data_contract_based_queries;
mod document_history;
mod document_query;
mod group_queries;
mod identity_based_queries;
Expand Down
Loading
Loading