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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions packages/dash-platform-queries/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,23 @@ rust-version.workspace = true
license = "MIT"

[features]
default = []
default = ["proofs"]
# The proof-verification half of the crate: query building and
# request-bound response verification through drive-proof-verifier and
# the context provider. With this off the crate is a pure codec
# (wire-proto decoders, document builders, string validation) — the
# subset rs-drive-abci consumes — so the server build never links the
# client proof stack (drive-proof-verifier's tenderdash verification,
# the context provider). dapi-grpc's `client` feature stays in the base
# dependencies: it is what exposes the generated message types and is
# transport-free (tonic's transport stack is behind dapi-grpc's
# separate `transport` feature), and drive-abci's `server` feature is a
# superset of it. Likewise drive's `verify` feature stays in the base
# so the codec's `drive::query` types are available in a standalone
# build; drive-abci's `server` feature also provides them.
proofs = ["dep:dash-context-provider", "dep:drive-proof-verifier"]
mocks = [
"proofs",
"dep:serde",
"dep:serde_json",
"dapi-grpc/mocks",
Expand All @@ -17,11 +32,12 @@ mocks = [
]

[dependencies]
ciborium = { version = "0.2.2" }
dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [
"platform",
"client",
] }
dash-context-provider = { path = "../rs-context-provider", default-features = false }
dash-context-provider = { path = "../rs-context-provider", default-features = false, optional = true }
dash-platform-macros = { path = "../rs-dash-platform-macros" }
dpp = { path = "../rs-dpp", default-features = false, features = [
"platform-value-cbor",
Expand All @@ -31,7 +47,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [
drive = { path = "../rs-drive", default-features = false, features = [
"verify",
] }
drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false }
drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false, optional = true }
hex = { version = "0.4.3" }
serde = { version = "1.0.219", default-features = false, features = [
"rc",
Expand Down
21 changes: 11 additions & 10 deletions packages/dash-platform-queries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ If you want networking, retries, and a managed connection pool, use

## What's here

- `DocumentQuery` — rich document query builder with wire
encoding for both request versions.
- `DocumentQuery` — rich document query builder, wire encoding for both
request versions, and decoding **from** the wire request
(`DocumentQuery::try_from_request`) using the same proto conversions the
server (`drive-abci`) uses, so client and server cannot drift.
- `verify_documents_response` — request-driven proof verification for document
queries, delegating to `drive-proof-verifier`'s `FromProof`.
- Aggregate proof helpers (count/sum/average/ranked) shared with `dash-sdk`.
- DPNS username helpers — label normalization/validation and the
convertibility/contested checks shared with `dash-sdk`.
- `transition::validation` — structural validation for state transitions
ahead of signing.

Wire-request decoding (`DocumentQuery::try_from_request`), request-driven
proof verification, and pure DPNS/DashPay document builders arrive in the
next slice of this series.
- Pure DPNS builders — `build_dpns_preorder_and_domain_documents`, label
normalization/validation — and pure DashPay contact-request document
assembly (`dashpay::build_contact_request_document`); crypto material is
supplied by the caller, keys never enter this crate.
- `transition::validation` helpers.

## Feature flags

Expand Down
143 changes: 143 additions & 0 deletions packages/dash-platform-queries/src/dashpay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! Transport-free DashPay contact request document assembly.
//!
//! The Sdk-bound DashPay surface (recipient fetching, ECDH, encryption,
//! broadcasting) lives in `dash-sdk`; this module is the pure DIP-15
//! `contactRequest` document assembly it shares with embedders. All crypto
//! material arrives here as bytes — key derivation and encryption stay with
//! the caller.

use crate::Error;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
use dpp::data_contract::DataContract;
use dpp::document::Document;
use dpp::platform_value::Value;
use dpp::prelude::Identifier;
use std::collections::BTreeMap;

/// Already-derived crypto material and metadata for a DIP-15
/// `contactRequest` document.
///
/// Everything here is plain data: the ECDH/encryption that produced
/// `encrypted_public_key` and `encrypted_account_label`, and the randomness
/// that produced `entropy`, happen in the caller (`dash-sdk` or an
/// embedder).
#[derive(Debug, Clone)]
pub struct ContactRequestDocumentParams {
/// The sender's identity id (the document owner)
pub sender_id: Identifier,
/// The recipient's identity id (`toUserId`)
pub recipient_id: Identifier,
/// The sender's encryption key index used for ECDH
pub sender_key_index: u32,
/// The recipient's key index used for ECDH
pub recipient_key_index: u32,
/// Reference to the DashPay receiving account
pub account_reference: u32,
/// ECDH-encrypted extended public key: exactly 96 bytes
/// (16-byte IV + 80 bytes of encrypted DIP-15 compact xpub)
pub encrypted_public_key: Vec<u8>,
/// Optional encrypted account label: 48-80 bytes
/// (16-byte IV + 32-64 bytes of encrypted data)
pub encrypted_account_label: Option<Vec<u8>>,
/// Optional auto-accept proof (38-102 bytes) - not encrypted
pub auto_accept_proof: Option<Vec<u8>>,
/// The entropy that derives the document id; the same entropy must be
/// attached to the create transition, or platform consensus rejects it
/// with `InvalidDocumentTransitionIdError`.
pub entropy: [u8; 32],
}

/// Validate the size of a DIP-15 `autoAcceptProof` (38-102 bytes).
pub fn validate_auto_accept_proof(proof: &[u8]) -> Result<(), Error> {
if proof.len() < 38 || proof.len() > 102 {
return Err(Error::InvalidInput(format!(
"autoAcceptProof must be 38-102 bytes, got {}",
proof.len()
)));
}
Ok(())
}

/// Build the id and property map of a DIP-15 `contactRequest` document from
/// already-derived crypto material.
///
/// This is the pure document-assembly half of `dash-sdk`'s
/// `create_contact_request`: the document id derives from
/// `params.entropy`, and the property map carries exactly the fields the
/// DashPay contract defines (`toUserId`, `encryptedPublicKey`,
/// `senderKeyIndex`, `recipientKeyIndex`, `accountReference`, plus the
/// optional `encryptedAccountLabel` and `autoAcceptProof`).
///
/// Returns `(document_id, properties)`.
pub fn build_contact_request_document(
contract: &DataContract,
params: ContactRequestDocumentParams,
) -> Result<(Identifier, BTreeMap<String, Value>), Error> {
if let Some(ref proof) = params.auto_accept_proof {
validate_auto_accept_proof(proof)?;
}

// Validate encrypted public key size (must be exactly 96 bytes: 16-byte IV + 80-byte encrypted data)
if params.encrypted_public_key.len() != 96 {
return Err(Error::InvalidInput(format!(
"Encrypted public key size mismatch: expected 96 bytes, got {}",
params.encrypted_public_key.len()
)));
}

// Validate encrypted label size (48-80 bytes: 16-byte IV + 32-64 byte encrypted data)
if let Some(ref label) = params.encrypted_account_label {
if label.len() < 48 || label.len() > 80 {
return Err(Error::InvalidInput(format!(
"Encrypted account label size out of range: expected 48-80 bytes, got {}",
label.len()
)));
}
}

let contact_request_document_type =
contract
.document_type_for_name("contactRequest")
.map_err(|_| {
Error::InvalidInput("DashPay contactRequest document type not found".to_string())
})?;

let document_id = Document::generate_document_id_v0(
&contract.id(),
&params.sender_id,
contact_request_document_type.name(),
params.entropy.as_slice(),
);

let mut properties = BTreeMap::new();
properties.insert(
"toUserId".to_string(),
Value::Identifier(params.recipient_id.to_buffer()),
);
properties.insert(
"encryptedPublicKey".to_string(),
Value::Bytes(params.encrypted_public_key),
);
properties.insert(
"senderKeyIndex".to_string(),
Value::U32(params.sender_key_index),
);
properties.insert(
"recipientKeyIndex".to_string(),
Value::U32(params.recipient_key_index),
);
properties.insert(
"accountReference".to_string(),
Value::U32(params.account_reference),
);

if let Some(label) = params.encrypted_account_label {
properties.insert("encryptedAccountLabel".to_string(), Value::Bytes(label));
}
if let Some(proof) = params.auto_accept_proof {
properties.insert("autoAcceptProof".to_string(), Value::Bytes(proof));
}

Ok((document_id, properties))
}
Loading
Loading