Skip to content

Commit

Permalink
feat(rust): impl Serialize and Deserialize for transactions (through …
Browse files Browse the repository at this point in the history
…AnyTransaction)
mehcode committed May 22, 2022
1 parent 3dca253 commit 4108b2f
Showing 9 changed files with 212 additions and 33 deletions.
3 changes: 0 additions & 3 deletions sdk/c/include/hedera.h
Original file line number Diff line number Diff line change
@@ -113,9 +113,6 @@ void hedera_client_set_payer_account_id(struct HederaClient *client, struct Hede
*/
void hedera_client_add_default_signer(struct HederaClient *client, struct HederaSigner *signer);

void hedera_client_add_default_signer_private_key(struct HederaClient *client,
struct HederaPrivateKey *key);

/**
* Execute this request against the provided client of the Hedera network.
*/
2 changes: 2 additions & 0 deletions sdk/rust/src/error.rs
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ pub enum Error {
#[error("failed to sign request: {0}")]
Signature(BoxStdError),

#[cfg(feature = "ffi")]
#[error("failed to parse a request from JSON: {0}")]
RequestParse(BoxStdError),
}
@@ -69,6 +70,7 @@ impl Error {
Self::BasicParse(error.into())
}

#[cfg(feature = "ffi")]
pub(crate) fn request_parse<E: Into<BoxStdError>>(error: E) -> Self {
Self::RequestParse(error.into())
}
8 changes: 5 additions & 3 deletions sdk/rust/src/query/any.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use crate::query::payment_transaction::PaymentTransaction;
use crate::query::QueryExecute;
use crate::{AccountBalance, AccountInfo, FromProtobuf, Query};

/// Any possible query that may be executed on the Hedera network.
pub type AnyQuery = Query<AnyQueryData>;

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
@@ -81,11 +82,12 @@ impl FromProtobuf for AnyQueryResponse {
// we create a proxy type that has the same layout but is only for AnyQueryData and does
// derive(Deserialize).

#[derive(serde::Deserialize, Debug)]
#[derive(serde::Deserialize)]
struct AnyQueryProxy {
#[serde(flatten)]
data: AnyQueryData,
// TODO: payment: Option<PaymentTransaction>
#[serde(default)]
payment: PaymentTransaction,
}

impl<'de> Deserialize<'de> for AnyQuery {
@@ -94,6 +96,6 @@ impl<'de> Deserialize<'de> for AnyQuery {
D: Deserializer<'de>,
{
<AnyQueryProxy as Deserialize>::deserialize(deserializer)
.map(|query| Self { data: query.data, payment: PaymentTransaction::default() })
.map(|query| Self { data: query.data, payment: query.payment })
}
}
45 changes: 42 additions & 3 deletions sdk/rust/src/query/payment_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
use async_trait::async_trait;
use hedera_proto::services::crypto_service_client::CryptoServiceClient;
use hedera_proto::services::{self};
use serde::{Deserialize, Deserializer};
use serde_with::skip_serializing_none;
use time::Duration;
use tonic::transport::Channel;

use crate::transaction::{ToTransactionDataProtobuf, TransactionExecute};
use crate::transaction::{ToTransactionDataProtobuf, TransactionBody, TransactionExecute};
use crate::{AccountId, ToProtobuf, Transaction, TransactionId};

pub(super) type PaymentTransaction = Transaction<PaymentTransactionData>;

#[skip_serializing_none]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct PaymentTransactionData {
// TODO: Use Hbar
pub(super) amount: Option<u64>,
pub(super) max_amount: Option<u64>,
}

#[async_trait]
impl TransactionExecute for PaymentTransaction {
impl TransactionExecute for PaymentTransactionData {
async fn execute(
&self,
channel: Channel,
request: services::Transaction,
) -> Result<tonic::Response<services::TransactionResponse>, tonic::Status> {
@@ -55,3 +59,38 @@ impl ToTransactionDataProtobuf for PaymentTransactionData {
})
}
}

// TODO: this is identical to AnyTransaction

#[derive(serde::Deserialize, Debug)]
struct PaymentTransactionBodyProxy {
data: PaymentTransactionData,
node_account_ids: Option<Vec<AccountId>>,
#[serde(with = "crate::serde::duration_opt")]
transaction_valid_duration: Option<Duration>,
max_transaction_fee: Option<u64>,
#[serde(skip_serializing_if = "crate::serde::skip_if_string_empty")]
transaction_memo: String,
payer_account_id: Option<AccountId>,
transaction_id: Option<TransactionId>,
}

impl<'de> Deserialize<'de> for PaymentTransaction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
<PaymentTransactionBodyProxy as Deserialize>::deserialize(deserializer).map(|body| Self {
body: TransactionBody {
data: body.data,
node_account_ids: body.node_account_ids,
transaction_valid_duration: body.transaction_valid_duration,
max_transaction_fee: body.max_transaction_fee,
transaction_memo: body.transaction_memo,
payer_account_id: body.payer_account_id,
transaction_id: body.transaction_id,
},
signers: Vec::new(),
})
}
}
89 changes: 89 additions & 0 deletions sdk/rust/src/transaction/any.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use async_trait::async_trait;
use hedera_proto::services;
use serde::{Deserialize, Deserializer};
use time::Duration;
use tonic::transport::Channel;
use tonic::{Response, Status};

use crate::transaction::{ToTransactionDataProtobuf, TransactionBody, TransactionExecute};
use crate::transfer_transaction::TransferTransactionData;
use crate::{AccountId, Transaction, TransactionId};

/// Any possible transaction that may be executed on the Hedera network.
pub type AnyTransaction = Transaction<AnyTransactionData>;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AnyTransactionData {
Transfer(TransferTransactionData),
}

impl ToTransactionDataProtobuf for AnyTransactionData {
fn to_transaction_data_protobuf(
&self,
node_account_id: AccountId,
transaction_id: &TransactionId,
) -> services::transaction_body::Data {
match self {
Self::Transfer(transaction) => {
transaction.to_transaction_data_protobuf(node_account_id, transaction_id)
}
}
}
}

#[async_trait]
impl TransactionExecute for AnyTransactionData {
fn default_max_transaction_fee(&self) -> u64 {
match self {
Self::Transfer(transaction) => transaction.default_max_transaction_fee(),
}
}

async fn execute(
&self,
channel: Channel,
request: services::Transaction,
) -> Result<Response<services::TransactionResponse>, Status> {
match self {
Self::Transfer(transaction) => transaction.execute(channel, request).await,
}
}
}

// NOTE: as we cannot derive Deserialize on Query<T> directly as `T` is not Deserialize,
// we create a proxy type that has the same layout but is only for AnyQueryData and does
// derive(Deserialize).

#[derive(serde::Deserialize, Debug)]
struct AnyTransactionBodyProxy {
data: AnyTransactionData,
node_account_ids: Option<Vec<AccountId>>,
#[serde(with = "crate::serde::duration_opt")]
transaction_valid_duration: Option<Duration>,
max_transaction_fee: Option<u64>,
#[serde(skip_serializing_if = "crate::serde::skip_if_string_empty")]
transaction_memo: String,
payer_account_id: Option<AccountId>,
transaction_id: Option<TransactionId>,
}

impl<'de> Deserialize<'de> for AnyTransaction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
<AnyTransactionBodyProxy as Deserialize>::deserialize(deserializer).map(|body| Self {
body: TransactionBody {
data: body.data,
node_account_ids: body.node_account_ids,
transaction_valid_duration: body.transaction_valid_duration,
max_transaction_fee: body.max_transaction_fee,
transaction_memo: body.transaction_memo,
payer_account_id: body.payer_account_id,
transaction_id: body.transaction_id,
},
signers: Vec::new(),
})
}
}
15 changes: 7 additions & 8 deletions sdk/rust/src/transaction/execute.rs
Original file line number Diff line number Diff line change
@@ -14,12 +14,13 @@ use crate::{
};

#[async_trait]
pub trait TransactionExecute {
fn default_max_transaction_fee() -> u64 {
pub trait TransactionExecute: ToTransactionDataProtobuf {
fn default_max_transaction_fee(&self) -> u64 {
2 * 100_000_000 // 2 hbar
}

async fn execute(
&self,
channel: Channel,
request: services::Transaction,
) -> Result<tonic::Response<services::TransactionResponse>, tonic::Status>;
@@ -28,8 +29,7 @@ pub trait TransactionExecute {
#[async_trait]
impl<D> Execute for Transaction<D>
where
D: ToTransactionDataProtobuf,
Self: TransactionExecute,
D: TransactionExecute,
{
type GrpcRequest = services::Transaction;

@@ -98,7 +98,7 @@ where
channel: Channel,
request: Self::GrpcRequest,
) -> Result<Response<Self::GrpcResponse>, Status> {
<Self as TransactionExecute>::execute(channel, request).await
self.body.data.execute(channel, request).await
}

fn make_response(
@@ -121,8 +121,7 @@ where

impl<D> Transaction<D>
where
D: ToTransactionDataProtobuf,
Self: TransactionExecute,
D: TransactionExecute,
{
#[allow(deprecated)]
fn to_transaction_body_protobuf(
@@ -141,7 +140,7 @@ where

// no max has been set on the client either
// fallback to the hard-coded default for this transaction type
_ => Self::default_max_transaction_fee(),
_ => self.body.data.default_max_transaction_fee(),
}
});

21 changes: 11 additions & 10 deletions sdk/rust/src/transaction/mod.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ use time::Duration;
use crate::execute::execute;
use crate::{AccountId, Client, Signer, TransactionId, TransactionResponse};

mod any;
mod execute;
mod protobuf;

@@ -12,28 +13,29 @@ pub(crate) use protobuf::ToTransactionDataProtobuf;

const DEFAULT_TRANSACTION_VALID_DURATION: Duration = Duration::seconds(120);

/// A transaction that can be executed on the Hedera network.
#[derive(serde::Serialize)]
pub struct Transaction<D> {
#[serde(flatten)]
pub(crate) body: TransactionBody<D>,

#[serde(skip)]
signers: Vec<Box<dyn Signer>>,
pub(crate) signers: Vec<Box<dyn Signer>>,
}

#[skip_serializing_none]
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionBody<D> {
pub(crate) struct TransactionBody<D> {
pub(crate) data: D,
node_account_ids: Option<Vec<AccountId>>,
pub(crate) node_account_ids: Option<Vec<AccountId>>,
#[serde(with = "crate::serde::duration_opt")]
transaction_valid_duration: Option<Duration>,
max_transaction_fee: Option<u64>,
pub(crate) transaction_valid_duration: Option<Duration>,
pub(crate) max_transaction_fee: Option<u64>,
#[serde(skip_serializing_if = "crate::serde::skip_if_string_empty")]
transaction_memo: String,
payer_account_id: Option<AccountId>,
transaction_id: Option<TransactionId>,
pub(crate) transaction_memo: String,
pub(crate) payer_account_id: Option<AccountId>,
pub(crate) transaction_id: Option<TransactionId>,
}

impl<D> Default for Transaction<D>
@@ -127,8 +129,7 @@ impl<D> Transaction<D> {

impl<D> Transaction<D>
where
D: ToTransactionDataProtobuf,
Self: TransactionExecute,
D: TransactionExecute,
{
/// Execute this transaction against the provided client of the Hedera network.
pub async fn execute(&mut self, client: &Client) -> crate::Result<TransactionResponse> {
54 changes: 51 additions & 3 deletions sdk/rust/src/transaction_id.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;

use hedera_proto::services;
use itertools::Itertools;
use rand::{thread_rng, Rng};
use serde_with::SerializeDisplay;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use time::{Duration, OffsetDateTime};

use crate::{AccountId, ToProtobuf};
use crate::{AccountId, Error, ToProtobuf};

/// The client-generated ID for a transaction.
///
/// This is used for retrieving receipts and records for a transaction, for appending to a file
/// right after creating it, for instantiating a smart contract with bytecode in a file just created,
/// and internally by the network for detecting when duplicate transactions are submitted.
///
#[derive(Copy, Clone, Eq, PartialEq, Hash, SerializeDisplay)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct TransactionId {
/// The account that pays for this transaction.
pub account_id: AccountId,
@@ -60,6 +62,52 @@ impl Display for TransactionId {
}
}

// TODO: add unit tests to prove parsing
// TODO: potentially improve parsing with `nom` or `combine`
impl FromStr for TransactionId {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
const EXPECTED: &str = "expecting <accountId>@<validStart>[?scheduled][/<nonce>]";

let mut parts = s.splitn(4, &['@', '?', '/']);

let account_id = if let Some(account_id_s) = parts.next() {
AccountId::from_str(account_id_s)?
} else {
return Err(Error::basic_parse(EXPECTED));
};

let valid_start = if let Some(valid_start_s) = parts.next() {
let (seconds_s, nanos_s) = valid_start_s
.splitn(2, '.')
.next_tuple()
.ok_or_else(|| Error::basic_parse(EXPECTED))?;

let seconds = i64::from_str(seconds_s).map_err(Error::basic_parse)?;
let nanos = i64::from_str(nanos_s).map_err(Error::basic_parse)?;

OffsetDateTime::from_unix_timestamp(seconds).map_err(Error::basic_parse)?
+ Duration::nanoseconds(nanos)
} else {
return Err(Error::basic_parse(EXPECTED));
};

let mut scheduled = false;
let mut nonce = None;

for part in parts.take(2) {
if part == "scheduled" {
scheduled = true;
} else {
nonce = Some(i32::from_str(part).map_err(Error::basic_parse)?);
}
}

Ok(Self { scheduled, nonce, valid_start, account_id })
}
}

impl ToProtobuf for TransactionId {
type Protobuf = services::TransactionId;

8 changes: 5 additions & 3 deletions sdk/rust/src/transfer_transaction.rs
Original file line number Diff line number Diff line change
@@ -17,14 +17,15 @@ use crate::{AccountIdOrAlias, ToProtobuf, Transaction};
///
pub type TransferTransaction = Transaction<TransferTransactionData>;

#[derive(Debug, Default)]
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferTransactionData {
hbar_transfers: Vec<HbarTransfer>,
// TODO: token_transfers
// TODO: nft_transfers
}

#[derive(Debug)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct HbarTransfer {
account: AccountIdOrAlias,
amount: i64,
@@ -56,8 +57,9 @@ impl TransferTransaction {
}

#[async_trait]
impl TransactionExecute for TransferTransaction {
impl TransactionExecute for TransferTransactionData {
async fn execute(
&self,
channel: Channel,
request: services::Transaction,
) -> Result<tonic::Response<services::TransactionResponse>, tonic::Status> {

0 comments on commit 4108b2f

Please sign in to comment.