Skip to main content

Rust client

note

We are currently updating this documentation. While it offers a general overview, some details may be outdated. Please check back soon for the latest version.

The Rust client is used for interacting with the Chromia blockchain deployed to a Postchain single node (manual mode) or multi-nodes managed by the Directory Chain (managed mode).

This library provides functionality for executing queries, creating and signing transactions, and managing blockchain operations.

Installation

Add this to your Cargo.toml:

This is for using only the postchain_client::utils::operation::Params enum to construct data for queries and transactions.

[dependencies]
postchain-client = "0.0.3"
tokio = { version = "1.42.0", features = ["rt"] }

For both, use the postchain_client::utils::operation::Params enum and the Rust's struct to serialize and deserialize with serde:

[dependencies]
postchain-client = "0.0.3"
tokio = { version = "1.42.0", features = ["rt"] }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }

1. Setting up the client

use postchain_client::transport::client::RestClient;

let client = RestClient {
node_url: vec!["http://localhost:7740", "http://localhost:7741"],
request_time_out: 30,
poll_attemps: 5,
poll_attemp_interval_time: 5
};

2. Executing queries

Queries allow us to fetch data from the blockchain:

use postchain_client::utils::operation::Params;

async fn execute_query_with_params(client: &RestClient<'_>) -> Result<(), Box<dyn std::error::Error>> {
let query_type = "<query_name>";
let mut query_arguments = vec![
("arg1", Params::Text("value1".to_string())),
("arg2", Params::Text("value2".to_string())),
];

let result = client.query(
"<BLOCKCHAIN_RID>",
None,
query_type,
None,
Some(&mut query_arguments)
).await?;

Ok(())
}

async fn execute_query_with_struct(client: &RestClient<'_>) -> Result<(), Box<dyn std::error::Error>> {
let query_type = "<query_name>";

#[derive(Debug, Default, serde::Serialize)]
struct QueryArguments {
arg1: String,
arg2: String
}

let query_arguments = Params::from_struct_to_vec(&QueryArguments {
arg1: "value1".to_string(), arg2: "value2".to_string()
});

let result = client.query(
"<BLOCKCHAIN_RID>",
None,
query_type,
None,
Some(&mut query_arguments_ref)
).await?;

if let RestResponse::Bytes(val1) = result {
println!("{:?}", gtv::decode(&val1));
}

Ok(())
}

3. Creating and sending transactions

3.1 Creating operations

use postchain_client::utils::operation::{Operation, Params};

// Create operation with named parameters (dictionary)
let operation = Operation::from_dict(
"operation_name",
vec![
("param1", Params::Text("value1".to_string())),
("param2", Params::Integer(42)),
]
);

// Or create an operation with unnamed parameters (list)
let operation = Operation::from_list(
"operation_name",
vec![
Params::Text("value1".to_string()),
Params::Integer(42),
]
);

3.2 Creating and signing transactions

use postchain_client::utils::transaction::Transaction;

// Create a new transaction
let mut tx = Transaction::new(
brid_hex_decoded.to_vec(), // blockchain RID in hex decode to vec
Some(vec![operation]), // operations
None, // signers (optional)
None // signatures (optional)
);

// Sign the transaction
let private_key1 = "C70D5A77CC10552019179B7390545C46647C9FCA1B6485850F2B913F87270300"; // Replace with actual private key
tx.sign(&hex::decode(private_key1).unwrap().try_into().expect("Invalid private key 1")).expect("Failed to sign transaction");

// Multi sign the transaction
let private_key2 = "17106092B72489B785615BD2ACB2DDE8D0EA05A2029DCA4054987494781F988C"; // Replace with actual private key
tx.sign(&[
&hex::decode(private_key1).unwrap().try_into().expect("Invalid private key 1"),
&hex::decode(private_key2).unwrap().try_into().expect("Invalid private key 2")
]).expect("Failed to multi sign transaction");

// Sign the transaction from raw private key
tx.sign_from_raw_priv_key(private_key1);

// Multi sign the transaction from raw private keys
tx.multi_sign_from_raw_priv_keys(&[private_key1, private_key2]);

3.3 Sending transactions

async fn send_transaction(client: &RestClient<'_>, tx: &Transaction<'_>) -> Result<(), Box<dyn std::error::Error>> {
// Send transaction
let response = client.send_transaction(tx).await?;

// Get transaction RID (for status checking)
let tx_rid = tx.tx_rid_hex();

// Check transaction status
let status = client.get_transaction_status("<blockchain RID>", &tx_rid).await?;

Ok(())
}

4. Error and response handling

The response from client.query and client.send_transaction is a postchain_client::transport::client::RestResponse enum if success or a postchain_client::transport::client::RestError enum if failed.

We can handle it as follows:

let result = client.query(/* ... */).await;

match result {
Ok(resp: RestResponse) => {
if let RestResponse::Bytes(val1) = resp {
let params = gtv::decode(&val1);
/// Do whatever we want with the decoded params
}
},
Error(error: RestError) => {
/// Do whatever we want with the error
}
}
let result = client.send_transaction(&tx).await;

match result {
Ok(resp: RestResponse) => {
println!("Transaction sent successfully: {:?}", resp);
},
Err(error: ) => {
eprintln!("Error sending transaction: {:?}", err);
}
}

The response from client.get_transaction_status is a postchain_client::utils::transaction::TransactionStatus enum if success or a postchain_client::transport::client::RestError enum if failed.

We can handle it as follows:

let result = client.get_transaction_status("<blockchain RID>", &tx_rid).await;

match result {
Ok(resp: TransactionStatus) => {
/// Do anything else here
},
Err(error: RestError) => {
/// Do whatever we want with the error
}
}

5. Use serde for serialize or deserialize a struct to Params::Dict or vice versa

Please look at all tests in the operation.rs source here: https://github.com/cuonglb/postchain-client-rust/blob/dev/src/utils/operation.rs

6. Logging

The postchain-client uses tracing crate for logging. You can use tracing-subscriber crate to enable all logs.

use tracing_subscriber;

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
...
}

7. Parameter types

The library supports various parameter types through the Params enum and Rust struct :

GTV(*)Rust typesParams enumsNote
nullOption<T> = NoneParams::Null
integerboolParams::Boolean(bool)
integeri64Params::Integer(i64)
bigIntegernum_bigint::BigIntParams::BigInteger(num_bigint::BigInt)(**)
decimalbigdecimal::BigDecimaParams::Decimal(bigdecimal::BigDecimal)(***)
stringStringParams::Text(String)
arrayVec<T>Params::Array(Vec<Params>)
dictBTreeMap<K, V>Params::Dict(BTreeMap<String, Params>)
byteArrayVec<u8>Params:: ByteArray(Vec<u8>)

(*) GTV gets converted to ASN.1 DER when it's sent. See more : https://docs.chromia.com/intro/about/architecture/generic-transaction-protocol#generic-transfer-value-gtv

(*) GTV gets converted to ASN.1 DER when it's sent. See more : https://docs.chromia.com/intro/about/architecture/generic-transaction-protocol#generic-transfer-value-gtv

(**) We can use serde custom derive macros in some of cases to:

Handle arbitrary-precision integers:

operation::deserialize_bigint for deserialization. operation::serialize_bigint for serialization.

    use postchain_client::utils::{operation::{deserialize_bigint, serialize_bigint}};
...
#[derive(serde::Serialize, serde::Deserialize)]
struct TestStructBigInt {
#[serde(serialize_with = "serialize_bigint", deserialize_with = "deserialize_bigint")]
bigint: num_bigint::BigInt
}
...

Handle arbitrary-precision decimal:

operation::deserialize_bigint for deserialization. operation::serialize_bigint for serialization.

    use postchain_client::utils::{operation::{serialize_bigdecimal, deserialize_bigdecimal}};
...
#[derive(serde::Serialize, serde::Deserialize)]
struct TestStructDecimal {
#[serde(serialize_with = "serialize_bigdecimal", deserialize_with = "deserialize_bigdecimal")]
bigdecimal: bigdecimal::BigDecimal
}
...

Examples

Book review application example

Here's a real-world example from a book review application that demonstrates querying, creating transactions, and handling structured data: https://github.com/cuonglb/postchain-client-rust/tree/dev/examples/book-review

This example demonstrates:

  • Defining and using structured data with Serde
  • Querying the blockchain and handling responses
  • Creating and sending transactions
  • Signing transactions with a private key
  • Transaction error handling and status checking

How to run

Install Rust (https://www.rust-lang.org/tools/install) and Docker with compose.

Start a Postchain single node with the book-review Rell dapp:

$ cd examples/book-review/rell-dapp/
$ sudo docker compose up -d

Start a simple Rust application to interact with the book-review blockchain:

$ cd examples/book-review/client
$ cargo run