mod ids;
pub mod virtual_ops;
pub use ids::{all_names, OperationId, FIRST_VIRTUAL_OP, LAST_OP};
pub use virtual_ops::VirtualOperation;
use crate::asset::Amount;
use crate::authority::Authority;
use crate::error::{Error, Result};
use crate::keys::PublicKey;
use crate::reader::{GrapheneDeserialize, Reader};
use crate::types::{
hived_transport_form, write_array, write_bool, write_i16, write_optional, write_string,
write_u16, write_u32, write_u64, write_varint32, GrapheneSerialize, PointInTime,
};
pub const MAX_CUSTOM_ID_LEN: usize = 32;
pub const MAX_AUTHORITY_MEMBERSHIP: usize = 40;
pub const MAX_BENEFICIARIES: usize = 127;
pub const MAX_PROPOSAL_SUBJECT_LEN: usize = 80;
pub const MAX_PROPOSAL_IDS: usize = 5;
pub const MAX_WITNESS_URL_LEN: usize = 2048;
pub const MAX_MEMO_LEN: usize = 2047;
pub const MAX_TITLE_LEN: usize = 255;
pub const MAX_PERMLINK_LEN: usize = 255;
pub const MAX_CUSTOM_OPS_PER_BLOCK: usize = 5;
pub const MAX_CUSTOM_DATA_LEN: usize = 8192;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Price {
pub base: Amount,
pub quote: Amount,
}
impl GrapheneSerialize for Price {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
self.base.append_to(out)?;
self.quote.append_to(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Beneficiary {
pub account: String,
pub weight: u16,
}
impl GrapheneSerialize for Beneficiary {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_string(out, &self.account)?;
write_u16(out, self.weight);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommentOptionsExtension {
Beneficiaries(Vec<Beneficiary>),
}
impl CommentOptionsExtension {
fn validate(&self) -> Result<()> {
match self {
CommentOptionsExtension::Beneficiaries(list) => {
if list.is_empty() {
return Err(Error::field("beneficiaries must name at least one account"));
}
if list.len() > MAX_BENEFICIARIES {
return Err(Error::field(format!(
"{} beneficiaries; hived allows at most {MAX_BENEFICIARIES}",
list.len()
)));
}
let total: u32 = list.iter().map(|b| u32::from(b.weight)).sum();
if total > 10_000 {
return Err(Error::field(format!(
"beneficiary weights total {total} basis points, which exceeds 10000"
)));
}
let mut sorted = list.clone();
sorted.sort_by(|a, b| {
hived_transport_form(&a.account).cmp(&hived_transport_form(&b.account))
});
if sorted.windows(2).any(|w| {
hived_transport_form(&w[0].account) == hived_transport_form(&w[1].account)
}) {
return Err(Error::field("the same beneficiary is listed twice"));
}
Ok(())
}
}
}
}
impl GrapheneSerialize for CommentOptionsExtension {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
match self {
CommentOptionsExtension::Beneficiaries(list) => {
let mut sorted = list.clone();
sorted.sort_by(|a, b| {
hived_transport_form(&a.account).cmp(&hived_transport_form(&b.account))
});
write_varint32(out, 0);
write_array(out, &sorted)
}
}
}
}
impl serde::Serialize for CommentOptionsExtension {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeTuple;
match self {
CommentOptionsExtension::Beneficiaries(list) => {
let mut sorted = list.clone();
sorted.sort_by(|a, b| {
hived_transport_form(&a.account).cmp(&hived_transport_form(&b.account))
});
let mut t = s.serialize_tuple(2)?;
t.serialize_element(&0u8)?;
t.serialize_element(&serde_json::json!({ "beneficiaries": sorted }))?;
t.end()
}
}
}
}
impl<'de> serde::Deserialize<'de> for CommentOptionsExtension {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
#[derive(serde::Deserialize)]
struct Beneficiaries {
beneficiaries: Vec<Beneficiary>,
}
let (tag, value) = <(u32, serde_json::Value)>::deserialize(d)?;
match tag {
0 => {
let b: Beneficiaries = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(CommentOptionsExtension::Beneficiaries(b.beneficiaries))
}
other => Err(D::Error::custom(format!(
"unknown comment_options extension variant {other}"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RecurrentTransferExtension {
PairId(u8),
}
impl GrapheneSerialize for RecurrentTransferExtension {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
match self {
RecurrentTransferExtension::PairId(id) => {
write_varint32(out, 1);
out.push(*id);
Ok(())
}
}
}
}
impl serde::Serialize for RecurrentTransferExtension {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
match self {
RecurrentTransferExtension::PairId(id) => {
let mut t = s.serialize_struct("extension", 2)?;
t.serialize_field("type", "recurrent_transfer_pair_id")?;
t.serialize_field("value", &serde_json::json!({ "pair_id": id }))?;
t.end()
}
}
}
}
impl<'de> serde::Deserialize<'de> for RecurrentTransferExtension {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
#[derive(serde::Deserialize)]
struct PairId {
pair_id: u8,
}
let raw = serde_json::Value::deserialize(d)?;
let value = match &raw {
serde_json::Value::Object(map) => {
let tag = map
.get("type")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| D::Error::custom("extension object has no `type`"))?;
if tag != "recurrent_transfer_pair_id" {
return Err(D::Error::custom(format!(
"unknown recurrent_transfer extension `{tag}`"
)));
}
map.get("value")
.ok_or_else(|| D::Error::custom("extension object has no `value`"))?
.clone()
}
serde_json::Value::Array(items) if items.len() == 2 => {
let tag = items[0]
.as_u64()
.ok_or_else(|| D::Error::custom("extension tag is not an integer"))?;
if tag != 1 {
return Err(D::Error::custom(format!(
"unknown recurrent_transfer extension variant {tag}"
)));
}
items[1].clone()
}
_ => {
return Err(D::Error::custom(
"recurrent_transfer extension must be an object or a [tag, value] pair",
))
}
};
let p: PairId = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(RecurrentTransferExtension::PairId(p.pair_id))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HexBytes(pub Vec<u8>);
impl HexBytes {
pub fn from_hex(s: &str) -> Result<Self> {
crate::hex::decode_vec(s.trim())
.map(HexBytes)
.map_err(|e| match e {
crate::hex::HexError::Length => {
Error::field("hex buffer has an odd number of characters")
}
crate::hex::HexError::NotHex => Error::field("buffer is not valid hex"),
})
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for HexBytes {
fn from(v: Vec<u8>) -> Self {
HexBytes(v)
}
}
impl serde::Serialize for HexBytes {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&self.to_hex())
}
}
impl<'de> serde::Deserialize<'de> for HexBytes {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let s = String::deserialize(d)?;
HexBytes::from_hex(&s).map_err(D::Error::custom)
}
}
impl GrapheneSerialize for HexBytes {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
crate::types::write_bytes(out, &self.0)
}
}
impl GrapheneDeserialize for HexBytes {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
Ok(HexBytes(r.bytes()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NoExtensions;
impl GrapheneSerialize for NoExtensions {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_varint32(out, 0);
Ok(())
}
}
impl<'de> serde::Deserialize<'de> for NoExtensions {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let v = Vec::<serde_json::Value>::deserialize(d)?;
if !v.is_empty() {
return Err(D::Error::custom(format!(
"operation carries {} extension(s), which this build does not model",
v.len()
)));
}
Ok(NoExtensions)
}
}
impl serde::Serialize for NoExtensions {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.collect_seq(std::iter::empty::<u8>())
}
}
macro_rules! op_struct {
(
$(#[$meta:meta])*
$name:ident {
$( $(#[$fmeta:meta])* $field:ident : $ty:ty ),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct $name {
$( $(#[$fmeta])* pub $field : $ty ),*
}
};
}
op_struct! {
Vote {
voter: String,
author: String,
permlink: String,
weight: i16,
}
}
op_struct! {
Comment {
parent_author: String,
parent_permlink: String,
author: String,
permlink: String,
title: String,
body: String,
json_metadata: String,
}
}
op_struct! {
Transfer {
from: String,
to: String,
amount: Amount,
memo: String,
}
}
op_struct! {
TransferToVesting {
from: String,
to: String,
amount: Amount,
}
}
op_struct! {
WithdrawVesting {
account: String,
vesting_shares: Amount,
}
}
op_struct! {
LimitOrderCreate {
owner: String,
orderid: u32,
amount_to_sell: Amount,
min_to_receive: Amount,
fill_or_kill: bool,
expiration: PointInTime,
}
}
op_struct! {
LimitOrderCancel {
owner: String,
orderid: u32,
}
}
op_struct! {
FeedPublish {
publisher: String,
exchange_rate: Price,
}
}
op_struct! {
Convert {
owner: String,
requestid: u32,
amount: Amount,
}
}
op_struct! {
AccountWitnessVote {
account: String,
witness: String,
approve: bool,
}
}
op_struct! {
AccountWitnessProxy {
account: String,
proxy: String,
}
}
op_struct! {
Custom {
required_auths: Vec<String>,
id: u16,
data: HexBytes,
}
}
op_struct! {
DeleteComment {
author: String,
permlink: String,
}
}
op_struct! {
CustomJson {
required_auths: Vec<String>,
required_posting_auths: Vec<String>,
id: String,
json: String,
}
}
op_struct! {
CommentOptions {
author: String,
permlink: String,
max_accepted_payout: Amount,
percent_hbd: u16,
allow_votes: bool,
allow_curation_rewards: bool,
extensions: Vec<CommentOptionsExtension>,
}
}
op_struct! {
SetWithdrawVestingRoute {
from_account: String,
to_account: String,
percent: u16,
auto_vest: bool,
}
}
op_struct! {
LimitOrderCreate2 {
owner: String,
orderid: u32,
amount_to_sell: Amount,
exchange_rate: Price,
fill_or_kill: bool,
expiration: PointInTime,
}
}
op_struct! {
ClaimAccount {
creator: String,
fee: Amount,
extensions: NoExtensions,
}
}
op_struct! {
CreateClaimedAccount {
creator: String,
new_account_name: String,
owner: Authority,
active: Authority,
posting: Authority,
memo_key: PublicKey,
json_metadata: String,
extensions: NoExtensions,
}
}
op_struct! {
ChangeRecoveryAccount {
account_to_recover: String,
new_recovery_account: String,
extensions: NoExtensions,
}
}
op_struct! {
TransferToSavings {
from: String,
to: String,
amount: Amount,
memo: String,
}
}
op_struct! {
TransferFromSavings {
from: String,
request_id: u32,
to: String,
amount: Amount,
memo: String,
}
}
op_struct! {
CancelTransferFromSavings {
from: String,
request_id: u32,
}
}
op_struct! {
DeclineVotingRights {
account: String,
decline: bool,
}
}
op_struct! {
ClaimRewardBalance {
account: String,
reward_hive: Amount,
reward_hbd: Amount,
reward_vests: Amount,
}
}
op_struct! {
DelegateVestingShares {
delegator: String,
delegatee: String,
vesting_shares: Amount,
}
}
op_struct! {
AccountUpdate2 {
account: String,
owner: Option<Authority>,
active: Option<Authority>,
posting: Option<Authority>,
memo_key: Option<PublicKey>,
json_metadata: String,
posting_json_metadata: String,
extensions: NoExtensions,
}
}
op_struct! {
CreateProposal {
creator: String,
receiver: String,
start_date: PointInTime,
end_date: PointInTime,
daily_pay: Amount,
subject: String,
permlink: String,
extensions: NoExtensions,
}
}
op_struct! {
UpdateProposalVotes {
voter: String,
proposal_ids: Vec<u64>,
approve: bool,
extensions: NoExtensions,
}
}
op_struct! {
RemoveProposal {
proposal_owner: String,
proposal_ids: Vec<u64>,
extensions: NoExtensions,
}
}
op_struct! {
UpdateProposal {
proposal_id: u64,
creator: String,
daily_pay: Amount,
subject: String,
permlink: String,
extensions: NoExtensions,
}
}
op_struct! {
CollateralizedConvert {
owner: String,
requestid: u32,
amount: Amount,
}
}
op_struct! {
RecurrentTransfer {
from: String,
to: String,
amount: Amount,
memo: String,
recurrence: u16,
executions: u16,
extensions: Vec<RecurrentTransferExtension>,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockId(pub [u8; 20]);
impl BlockId {
pub fn from_hex(s: &str) -> Result<Self> {
let s = s.trim();
if s.len() != 40 {
return Err(Error::field(format!(
"block id must be 40 hex characters, got {}",
s.len()
)));
}
let mut out = [0u8; 20];
crate::hex::decode_exact(s, &mut out)
.map_err(|_| Error::field("block id is not valid hex"))?;
Ok(BlockId(out))
}
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
}
impl GrapheneSerialize for BlockId {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
crate::types::write_raw(out, &self.0);
Ok(())
}
}
impl<'de> serde::Deserialize<'de> for BlockId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let s = String::deserialize(d)?;
BlockId::from_hex(&s).map_err(D::Error::custom)
}
}
impl serde::Serialize for BlockId {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&self.to_hex())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ChainProperties {
pub account_creation_fee: Amount,
pub maximum_block_size: u32,
pub hbd_interest_rate: u16,
}
impl GrapheneSerialize for ChainProperties {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
self.account_creation_fee.append_to(out)?;
write_u32(out, self.maximum_block_size);
write_u16(out, self.hbd_interest_rate);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WitnessProperty {
pub key: String,
pub value: Vec<u8>,
}
impl WitnessProperty {
pub fn public_key(key: &str, value: &PublicKey) -> Result<Self> {
Ok(WitnessProperty {
key: key.to_string(),
value: value.to_wire()?,
})
}
pub fn asset(key: &str, value: &Amount) -> Result<Self> {
Ok(WitnessProperty {
key: key.to_string(),
value: value.to_wire()?,
})
}
pub fn uint32(key: &str, value: u32) -> Self {
WitnessProperty {
key: key.to_string(),
value: value.to_le_bytes().to_vec(),
}
}
pub fn uint16(key: &str, value: u16) -> Self {
WitnessProperty {
key: key.to_string(),
value: value.to_le_bytes().to_vec(),
}
}
pub fn string(key: &str, value: &str) -> Result<Self> {
let mut buf = Vec::new();
write_string(&mut buf, value)?;
Ok(WitnessProperty {
key: key.to_string(),
value: buf,
})
}
}
impl GrapheneSerialize for WitnessProperty {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_string(out, &self.key)?;
crate::types::write_bytes(out, &self.value)?;
Ok(())
}
}
impl<'de> serde::Deserialize<'de> for WitnessProperty {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let (key, hex) = <(String, String)>::deserialize(d)?;
Ok(WitnessProperty {
key,
value: HexBytes::from_hex(&hex).map_err(D::Error::custom)?.0,
})
}
}
impl serde::Serialize for WitnessProperty {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeTuple;
let hex: String = self.value.iter().map(|b| format!("{b:02x}")).collect();
let mut t = s.serialize_tuple(2)?;
t.serialize_element(&self.key)?;
t.serialize_element(&hex)?;
t.end()
}
}
op_struct! {
AccountCreate {
fee: Amount,
creator: String,
new_account_name: String,
owner: Authority,
active: Authority,
posting: Authority,
memo_key: PublicKey,
json_metadata: String,
}
}
op_struct! {
AccountUpdate {
account: String,
owner: Option<Authority>,
active: Option<Authority>,
posting: Option<Authority>,
memo_key: PublicKey,
json_metadata: String,
}
}
op_struct! {
WitnessUpdate {
owner: String,
url: String,
block_signing_key: PublicKey,
props: ChainProperties,
fee: Amount,
}
}
op_struct! {
WitnessBlockApprove {
witness: String,
block_id: BlockId,
}
}
op_struct! {
RequestAccountRecovery {
recovery_account: String,
account_to_recover: String,
new_owner_authority: Authority,
extensions: NoExtensions,
}
}
op_struct! {
RecoverAccount {
account_to_recover: String,
new_owner_authority: Authority,
recent_owner_authority: Authority,
extensions: NoExtensions,
}
}
op_struct! {
EscrowTransfer {
from: String,
to: String,
hbd_amount: Amount,
hive_amount: Amount,
escrow_id: u32,
agent: String,
fee: Amount,
json_meta: String,
ratification_deadline: PointInTime,
escrow_expiration: PointInTime,
}
}
op_struct! {
EscrowDispute {
from: String,
to: String,
agent: String,
who: String,
escrow_id: u32,
}
}
op_struct! {
EscrowRelease {
from: String,
to: String,
agent: String,
who: String,
receiver: String,
escrow_id: u32,
hbd_amount: Amount,
hive_amount: Amount,
}
}
op_struct! {
EscrowApprove {
from: String,
to: String,
agent: String,
who: String,
escrow_id: u32,
approve: bool,
}
}
op_struct! {
CustomBinary {
required_owner_auths: Vec<String>,
required_active_auths: Vec<String>,
required_posting_auths: Vec<String>,
required_auths: Vec<Authority>,
id: String,
data: HexBytes,
}
}
op_struct! {
ResetAccount {
reset_account: String,
account_to_reset: String,
new_owner_authority: Authority,
}
}
op_struct! {
SetResetAccount {
account: String,
current_reset_account: String,
reset_account: String,
}
}
op_struct! {
AccountCreateWithDelegation {
fee: Amount,
delegation: Amount,
creator: String,
new_account_name: String,
owner: Authority,
active: Authority,
posting: Authority,
memo_key: PublicKey,
json_metadata: String,
extensions: NoExtensions,
}
}
op_struct! {
WitnessSetProperties {
owner: String,
props: Vec<WitnessProperty>,
extensions: NoExtensions,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Operation {
Vote(Vote),
Comment(Comment),
AccountCreate(AccountCreate),
AccountUpdate(AccountUpdate),
WitnessUpdate(WitnessUpdate),
WitnessBlockApprove(WitnessBlockApprove),
RequestAccountRecovery(RequestAccountRecovery),
RecoverAccount(RecoverAccount),
EscrowTransfer(EscrowTransfer),
EscrowDispute(EscrowDispute),
EscrowRelease(EscrowRelease),
EscrowApprove(EscrowApprove),
CustomBinary(CustomBinary),
ResetAccount(ResetAccount),
SetResetAccount(SetResetAccount),
AccountCreateWithDelegation(AccountCreateWithDelegation),
WitnessSetProperties(WitnessSetProperties),
Transfer(Transfer),
TransferToVesting(TransferToVesting),
WithdrawVesting(WithdrawVesting),
LimitOrderCreate(LimitOrderCreate),
LimitOrderCancel(LimitOrderCancel),
FeedPublish(FeedPublish),
Convert(Convert),
AccountWitnessVote(AccountWitnessVote),
AccountWitnessProxy(AccountWitnessProxy),
Custom(Custom),
DeleteComment(DeleteComment),
CustomJson(CustomJson),
CommentOptions(CommentOptions),
SetWithdrawVestingRoute(SetWithdrawVestingRoute),
LimitOrderCreate2(LimitOrderCreate2),
ClaimAccount(ClaimAccount),
CreateClaimedAccount(CreateClaimedAccount),
ChangeRecoveryAccount(ChangeRecoveryAccount),
TransferToSavings(TransferToSavings),
TransferFromSavings(TransferFromSavings),
CancelTransferFromSavings(CancelTransferFromSavings),
DeclineVotingRights(DeclineVotingRights),
ClaimRewardBalance(ClaimRewardBalance),
DelegateVestingShares(DelegateVestingShares),
AccountUpdate2(AccountUpdate2),
CreateProposal(CreateProposal),
UpdateProposalVotes(UpdateProposalVotes),
RemoveProposal(RemoveProposal),
UpdateProposal(UpdateProposal),
CollateralizedConvert(CollateralizedConvert),
RecurrentTransfer(RecurrentTransfer),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Checks {
Skip,
Enforce,
}
impl Checks {
fn on(self) -> bool {
self == Checks::Enforce
}
}
fn check_len(checks: Checks, value: &str, max: usize, what: &str) -> Result<()> {
if checks.on() && value.len() > max {
return Err(Error::field(format!(
"{what} is {} bytes; hived allows at most {max}",
value.len()
)));
}
Ok(())
}
impl Operation {
pub fn custom_op_accounts(&self) -> Vec<&str> {
self.custom_op_accounts_iter().collect()
}
pub(crate) fn custom_op_accounts_iter(&self) -> impl Iterator<Item = &str> {
const NONE: &[String] = &[];
let (a, b, c) = match self {
Operation::Custom(o) => (o.required_auths.as_slice(), NONE, NONE),
Operation::CustomJson(o) => (
o.required_auths.as_slice(),
o.required_posting_auths.as_slice(),
NONE,
),
Operation::CustomBinary(o) => (
o.required_owner_auths.as_slice(),
o.required_active_auths.as_slice(),
o.required_posting_auths.as_slice(),
),
_ => (NONE, NONE, NONE),
};
a.iter().chain(b).chain(c).map(String::as_str)
}
pub fn id(&self) -> OperationId {
match self {
Operation::Vote(_) => OperationId::Vote,
Operation::Comment(_) => OperationId::Comment,
Operation::AccountCreate(_) => OperationId::AccountCreate,
Operation::AccountUpdate(_) => OperationId::AccountUpdate,
Operation::WitnessUpdate(_) => OperationId::WitnessUpdate,
Operation::WitnessBlockApprove(_) => OperationId::WitnessBlockApprove,
Operation::RequestAccountRecovery(_) => OperationId::RequestAccountRecovery,
Operation::RecoverAccount(_) => OperationId::RecoverAccount,
Operation::EscrowTransfer(_) => OperationId::EscrowTransfer,
Operation::EscrowDispute(_) => OperationId::EscrowDispute,
Operation::EscrowRelease(_) => OperationId::EscrowRelease,
Operation::EscrowApprove(_) => OperationId::EscrowApprove,
Operation::CustomBinary(_) => OperationId::CustomBinary,
Operation::ResetAccount(_) => OperationId::ResetAccount,
Operation::SetResetAccount(_) => OperationId::SetResetAccount,
Operation::AccountCreateWithDelegation(_) => OperationId::AccountCreateWithDelegation,
Operation::WitnessSetProperties(_) => OperationId::WitnessSetProperties,
Operation::Transfer(_) => OperationId::Transfer,
Operation::TransferToVesting(_) => OperationId::TransferToVesting,
Operation::WithdrawVesting(_) => OperationId::WithdrawVesting,
Operation::LimitOrderCreate(_) => OperationId::LimitOrderCreate,
Operation::LimitOrderCancel(_) => OperationId::LimitOrderCancel,
Operation::FeedPublish(_) => OperationId::FeedPublish,
Operation::Convert(_) => OperationId::Convert,
Operation::AccountWitnessVote(_) => OperationId::AccountWitnessVote,
Operation::AccountWitnessProxy(_) => OperationId::AccountWitnessProxy,
Operation::Custom(_) => OperationId::Custom,
Operation::DeleteComment(_) => OperationId::DeleteComment,
Operation::CustomJson(_) => OperationId::CustomJson,
Operation::CommentOptions(_) => OperationId::CommentOptions,
Operation::SetWithdrawVestingRoute(_) => OperationId::SetWithdrawVestingRoute,
Operation::LimitOrderCreate2(_) => OperationId::LimitOrderCreate2,
Operation::ClaimAccount(_) => OperationId::ClaimAccount,
Operation::CreateClaimedAccount(_) => OperationId::CreateClaimedAccount,
Operation::ChangeRecoveryAccount(_) => OperationId::ChangeRecoveryAccount,
Operation::TransferToSavings(_) => OperationId::TransferToSavings,
Operation::TransferFromSavings(_) => OperationId::TransferFromSavings,
Operation::CancelTransferFromSavings(_) => OperationId::CancelTransferFromSavings,
Operation::DeclineVotingRights(_) => OperationId::DeclineVotingRights,
Operation::ClaimRewardBalance(_) => OperationId::ClaimRewardBalance,
Operation::DelegateVestingShares(_) => OperationId::DelegateVestingShares,
Operation::AccountUpdate2(_) => OperationId::AccountUpdate2,
Operation::CreateProposal(_) => OperationId::CreateProposal,
Operation::UpdateProposalVotes(_) => OperationId::UpdateProposalVotes,
Operation::RemoveProposal(_) => OperationId::RemoveProposal,
Operation::UpdateProposal(_) => OperationId::UpdateProposal,
Operation::CollateralizedConvert(_) => OperationId::CollateralizedConvert,
Operation::RecurrentTransfer(_) => OperationId::RecurrentTransfer,
}
}
pub fn validate(&self) -> Result<()> {
let mut discarded = Vec::new();
self.append_body(&mut discarded, Checks::Enforce)?;
if let Operation::CommentOptions(o) = self {
for extension in &o.extensions {
extension.validate()?;
}
}
Ok(())
}
fn append_body(&self, out: &mut Vec<u8>, checks: Checks) -> Result<()> {
match self {
Operation::Vote(o) => {
write_string(out, &o.voter)?;
write_string(out, &o.author)?;
write_string(out, &o.permlink)?;
write_i16(out, o.weight);
}
Operation::Comment(o) => {
check_len(checks, &o.title, MAX_TITLE_LEN, "comment title")?;
check_len(checks, &o.permlink, MAX_PERMLINK_LEN, "comment permlink")?;
check_len(
checks,
&o.parent_permlink,
MAX_PERMLINK_LEN,
"comment parent_permlink",
)?;
write_string(out, &o.parent_author)?;
write_string(out, &o.parent_permlink)?;
write_string(out, &o.author)?;
write_string(out, &o.permlink)?;
write_string(out, &o.title)?;
write_string(out, &o.body)?;
write_string(out, &o.json_metadata)?;
}
Operation::AccountCreate(o) => {
o.fee.append_to(out)?;
write_string(out, &o.creator)?;
write_string(out, &o.new_account_name)?;
o.owner.append_to(out)?;
o.active.append_to(out)?;
o.posting.append_to(out)?;
o.memo_key.append_to(out)?;
write_string(out, &o.json_metadata)?;
}
Operation::AccountUpdate(o) => {
write_string(out, &o.account)?;
write_optional(out, o.owner.as_ref())?;
write_optional(out, o.active.as_ref())?;
write_optional(out, o.posting.as_ref())?;
o.memo_key.append_to(out)?;
write_string(out, &o.json_metadata)?;
}
Operation::WitnessUpdate(o) => {
check_len(checks, &o.url, MAX_WITNESS_URL_LEN, "witness_update url")?;
write_string(out, &o.owner)?;
write_string(out, &o.url)?;
o.block_signing_key.append_to(out)?;
o.props.append_to(out)?;
o.fee.append_to(out)?;
}
Operation::WitnessBlockApprove(o) => {
write_string(out, &o.witness)?;
o.block_id.append_to(out)?;
}
Operation::RequestAccountRecovery(o) => {
write_string(out, &o.recovery_account)?;
write_string(out, &o.account_to_recover)?;
o.new_owner_authority.append_to(out)?;
o.extensions.append_to(out)?;
}
Operation::RecoverAccount(o) => {
write_string(out, &o.account_to_recover)?;
o.new_owner_authority.append_to(out)?;
o.recent_owner_authority.append_to(out)?;
o.extensions.append_to(out)?;
}
Operation::EscrowTransfer(o) => {
write_string(out, &o.from)?;
write_string(out, &o.to)?;
o.hbd_amount.append_to(out)?;
o.hive_amount.append_to(out)?;
write_u32(out, o.escrow_id);
write_string(out, &o.agent)?;
o.fee.append_to(out)?;
write_string(out, &o.json_meta)?;
o.ratification_deadline.append_to(out)?;
o.escrow_expiration.append_to(out)?;
}
Operation::EscrowDispute(o) => {
write_string(out, &o.from)?;
write_string(out, &o.to)?;
write_string(out, &o.agent)?;
write_string(out, &o.who)?;
write_u32(out, o.escrow_id);
}
Operation::EscrowRelease(o) => {
write_string(out, &o.from)?;
write_string(out, &o.to)?;
write_string(out, &o.agent)?;
write_string(out, &o.who)?;
write_string(out, &o.receiver)?;
write_u32(out, o.escrow_id);
o.hbd_amount.append_to(out)?;
o.hive_amount.append_to(out)?;
}
Operation::EscrowApprove(o) => {
write_string(out, &o.from)?;
write_string(out, &o.to)?;
write_string(out, &o.agent)?;
write_string(out, &o.who)?;
write_u32(out, o.escrow_id);
write_bool(out, o.approve);
}
Operation::CustomBinary(o) => {
if checks.on() && o.id.len() > MAX_CUSTOM_ID_LEN {
return Err(Error::field(format!(
"custom_binary id is {} bytes; hived allows at most {MAX_CUSTOM_ID_LEN}",
o.id.len()
)));
}
write_sorted_account_set(
out,
checks,
&o.required_owner_auths,
"required_owner_auths",
)?;
write_sorted_account_set(
out,
checks,
&o.required_active_auths,
"required_active_auths",
)?;
write_sorted_account_set(
out,
checks,
&o.required_posting_auths,
"required_posting_auths",
)?;
write_array(out, &o.required_auths)?;
write_string(out, &o.id)?;
o.data.append_to(out)?;
}
Operation::ResetAccount(o) => {
write_string(out, &o.reset_account)?;
write_string(out, &o.account_to_reset)?;
o.new_owner_authority.append_to(out)?;
}
Operation::SetResetAccount(o) => {
write_string(out, &o.account)?;
write_string(out, &o.current_reset_account)?;
write_string(out, &o.reset_account)?;
}
Operation::AccountCreateWithDelegation(o) => {
o.fee.append_to(out)?;
o.delegation.append_to(out)?;
write_string(out, &o.creator)?;
write_string(out, &o.new_account_name)?;
o.owner.append_to(out)?;
o.active.append_to(out)?;
o.posting.append_to(out)?;
o.memo_key.append_to(out)?;
write_string(out, &o.json_metadata)?;
o.extensions.append_to(out)?;
}
Operation::WitnessSetProperties(o) => {
write_string(out, &o.owner)?;
let mut props = o.props.clone();
props.sort_by(|a, b| {
hived_transport_form(&a.key).cmp(&hived_transport_form(&b.key))
});
if checks.on()
&& props
.windows(2)
.any(|w| hived_transport_form(&w[0].key) == hived_transport_form(&w[1].key))
{
return Err(Error::field(
"witness_set_properties lists the same key twice",
));
}
write_array(out, &props)?;
o.extensions.append_to(out)?;
}
Operation::Transfer(o) => {
check_len(checks, &o.memo, MAX_MEMO_LEN, "transfer memo")?;
write_string(out, &o.from)?;
write_string(out, &o.to)?;
o.amount.append_to(out)?;
write_string(out, &o.memo)?;
}
Operation::TransferToVesting(o) => {
write_string(out, &o.from)?;
write_string(out, &o.to)?;
o.amount.append_to(out)?;
}
Operation::WithdrawVesting(o) => {
write_string(out, &o.account)?;
o.vesting_shares.append_to(out)?;
}
Operation::LimitOrderCreate(o) => {
write_string(out, &o.owner)?;
write_u32(out, o.orderid);
o.amount_to_sell.append_to(out)?;
o.min_to_receive.append_to(out)?;
write_bool(out, o.fill_or_kill);
o.expiration.append_to(out)?;
}
Operation::LimitOrderCancel(o) => {
write_string(out, &o.owner)?;
write_u32(out, o.orderid);
}
Operation::FeedPublish(o) => {
write_string(out, &o.publisher)?;
o.exchange_rate.append_to(out)?;
}
Operation::Convert(o) => {
write_string(out, &o.owner)?;
write_u32(out, o.requestid);
o.amount.append_to(out)?;
}
Operation::AccountWitnessVote(o) => {
write_string(out, &o.account)?;
write_string(out, &o.witness)?;
write_bool(out, o.approve);
}
Operation::AccountWitnessProxy(o) => {
write_string(out, &o.account)?;
write_string(out, &o.proxy)?;
}
Operation::Custom(o) => {
if checks.on() && o.data.0.len() > MAX_CUSTOM_DATA_LEN {
return Err(Error::field(format!(
"custom data is {} bytes; hived allows at most {MAX_CUSTOM_DATA_LEN}",
o.data.0.len()
)));
}
write_sorted_account_set(out, checks, &o.required_auths, "required_auths")?;
write_u16(out, o.id);
o.data.append_to(out)?;
}
Operation::DeleteComment(o) => {
write_string(out, &o.author)?;
write_string(out, &o.permlink)?;
}
Operation::CustomJson(o) => {
if checks.on() && o.id.len() > MAX_CUSTOM_ID_LEN {
return Err(Error::field(format!(
"custom_json id is {} bytes; hived allows at most {MAX_CUSTOM_ID_LEN}",
o.id.len()
)));
}
if checks.on() && o.json.len() > MAX_CUSTOM_DATA_LEN {
return Err(Error::field(format!(
"custom_json json is {} bytes; hived allows at most \
{MAX_CUSTOM_DATA_LEN}. The whole transaction would be rejected, \
not just this operation, so batch the payload into several \
operations instead",
o.json.len()
)));
}
if checks.on()
&& o.required_auths.len() + o.required_posting_auths.len()
> MAX_AUTHORITY_MEMBERSHIP
{
return Err(Error::field(format!(
"custom_json names {} accounts across required_auths and \
required_posting_auths; hived allows at most \
{MAX_AUTHORITY_MEMBERSHIP} between them",
o.required_auths.len() + o.required_posting_auths.len()
)));
}
if checks.on() && o.required_auths.is_empty() && o.required_posting_auths.is_empty()
{
return Err(Error::field(
"custom_json needs at least one required_auths or required_posting_auths entry",
));
}
write_sorted_account_set(out, checks, &o.required_auths, "required_auths")?;
write_sorted_account_set(
out,
checks,
&o.required_posting_auths,
"required_posting_auths",
)?;
write_string(out, &o.id)?;
write_string(out, &o.json)?;
}
Operation::CommentOptions(o) => {
write_string(out, &o.author)?;
write_string(out, &o.permlink)?;
o.max_accepted_payout.append_to(out)?;
write_u16(out, o.percent_hbd);
write_bool(out, o.allow_votes);
write_bool(out, o.allow_curation_rewards);
write_array(out, &o.extensions)?;
}
Operation::SetWithdrawVestingRoute(o) => {
write_string(out, &o.from_account)?;
write_string(out, &o.to_account)?;
write_u16(out, o.percent);
write_bool(out, o.auto_vest);
}
Operation::LimitOrderCreate2(o) => {
write_string(out, &o.owner)?;
write_u32(out, o.orderid);
o.amount_to_sell.append_to(out)?;
o.exchange_rate.append_to(out)?;
write_bool(out, o.fill_or_kill);
o.expiration.append_to(out)?;
}
Operation::ClaimAccount(o) => {
write_string(out, &o.creator)?;
o.fee.append_to(out)?;
o.extensions.append_to(out)?;
}
Operation::CreateClaimedAccount(o) => {
write_string(out, &o.creator)?;
write_string(out, &o.new_account_name)?;
o.owner.append_to(out)?;
o.active.append_to(out)?;
o.posting.append_to(out)?;
o.memo_key.append_to(out)?;
write_string(out, &o.json_metadata)?;
o.extensions.append_to(out)?;
}
Operation::ChangeRecoveryAccount(o) => {
write_string(out, &o.account_to_recover)?;
write_string(out, &o.new_recovery_account)?;
o.extensions.append_to(out)?;
}
Operation::TransferToSavings(o) => {
check_len(checks, &o.memo, MAX_MEMO_LEN, "transfer_to_savings memo")?;
write_string(out, &o.from)?;
write_string(out, &o.to)?;
o.amount.append_to(out)?;
write_string(out, &o.memo)?;
}
Operation::TransferFromSavings(o) => {
check_len(checks, &o.memo, MAX_MEMO_LEN, "transfer_from_savings memo")?;
write_string(out, &o.from)?;
write_u32(out, o.request_id);
write_string(out, &o.to)?;
o.amount.append_to(out)?;
write_string(out, &o.memo)?;
}
Operation::CancelTransferFromSavings(o) => {
write_string(out, &o.from)?;
write_u32(out, o.request_id);
}
Operation::DeclineVotingRights(o) => {
write_string(out, &o.account)?;
write_bool(out, o.decline);
}
Operation::ClaimRewardBalance(o) => {
write_string(out, &o.account)?;
o.reward_hive.append_to(out)?;
o.reward_hbd.append_to(out)?;
o.reward_vests.append_to(out)?;
}
Operation::DelegateVestingShares(o) => {
write_string(out, &o.delegator)?;
write_string(out, &o.delegatee)?;
o.vesting_shares.append_to(out)?;
}
Operation::AccountUpdate2(o) => {
write_string(out, &o.account)?;
write_optional(out, o.owner.as_ref())?;
write_optional(out, o.active.as_ref())?;
write_optional(out, o.posting.as_ref())?;
write_optional(out, o.memo_key.as_ref())?;
write_string(out, &o.json_metadata)?;
write_string(out, &o.posting_json_metadata)?;
o.extensions.append_to(out)?;
}
Operation::CreateProposal(o) => {
check_len(
checks,
&o.subject,
MAX_PROPOSAL_SUBJECT_LEN,
"create_proposal subject",
)?;
check_len(
checks,
&o.permlink,
MAX_PERMLINK_LEN,
"create_proposal permlink",
)?;
write_string(out, &o.creator)?;
write_string(out, &o.receiver)?;
o.start_date.append_to(out)?;
o.end_date.append_to(out)?;
o.daily_pay.append_to(out)?;
write_string(out, &o.subject)?;
write_string(out, &o.permlink)?;
o.extensions.append_to(out)?;
}
Operation::UpdateProposalVotes(o) => {
write_string(out, &o.voter)?;
write_sorted_proposal_ids(out, checks, &o.proposal_ids)?;
write_bool(out, o.approve);
o.extensions.append_to(out)?;
}
Operation::RemoveProposal(o) => {
write_string(out, &o.proposal_owner)?;
write_sorted_proposal_ids(out, checks, &o.proposal_ids)?;
o.extensions.append_to(out)?;
}
Operation::UpdateProposal(o) => {
check_len(
checks,
&o.subject,
MAX_PROPOSAL_SUBJECT_LEN,
"update_proposal subject",
)?;
check_len(
checks,
&o.permlink,
MAX_PERMLINK_LEN,
"update_proposal permlink",
)?;
write_u64(out, o.proposal_id);
write_string(out, &o.creator)?;
o.daily_pay.append_to(out)?;
write_string(out, &o.subject)?;
write_string(out, &o.permlink)?;
o.extensions.append_to(out)?;
}
Operation::CollateralizedConvert(o) => {
write_string(out, &o.owner)?;
write_u32(out, o.requestid);
o.amount.append_to(out)?;
}
Operation::RecurrentTransfer(o) => {
check_len(checks, &o.memo, MAX_MEMO_LEN, "recurrent_transfer memo")?;
write_string(out, &o.from)?;
write_string(out, &o.to)?;
o.amount.append_to(out)?;
write_string(out, &o.memo)?;
write_u16(out, o.recurrence);
write_u16(out, o.executions);
write_array(out, &o.extensions)?;
}
}
Ok(())
}
}
fn write_sorted_account_set(
out: &mut Vec<u8>,
checks: Checks,
accounts: &[String],
what: &str,
) -> Result<()> {
let mut sorted: Vec<&String> = accounts.iter().collect();
sorted.sort_by(|a, b| hived_transport_form(a).cmp(&hived_transport_form(b)));
if checks.on()
&& sorted
.windows(2)
.any(|w| hived_transport_form(w[0]) == hived_transport_form(w[1]))
{
return Err(Error::field(format!("{what} lists the same account twice")));
}
write_array(out, &sorted)?;
Ok(())
}
fn write_sorted_proposal_ids(out: &mut Vec<u8>, checks: Checks, ids: &[u64]) -> Result<()> {
let mut sorted = ids.to_vec();
sorted.sort_unstable();
if checks.on() && sorted.windows(2).any(|w| w[0] == w[1]) {
return Err(Error::field("proposal_ids lists the same id twice"));
}
if checks.on() && sorted.is_empty() {
return Err(Error::field("proposal_ids is empty"));
}
if checks.on() && sorted.len() > MAX_PROPOSAL_IDS {
return Err(Error::field(format!(
"proposal_ids has {} entries; hived allows at most {MAX_PROPOSAL_IDS}",
sorted.len()
)));
}
write_array(out, &sorted)?;
Ok(())
}
impl GrapheneSerialize for Operation {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_varint32(out, self.id().as_u32());
self.append_body(out, Checks::Skip)
}
}
impl serde::Serialize for Operation {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
macro_rules! delegate {
($($variant:ident),* $(,)?) => {
match self {
$( Operation::$variant(o) => o.serialize(s), )*
}
};
}
delegate!(
Vote,
Comment,
AccountCreate,
AccountUpdate,
WitnessUpdate,
WitnessBlockApprove,
RequestAccountRecovery,
RecoverAccount,
EscrowTransfer,
EscrowDispute,
EscrowRelease,
EscrowApprove,
CustomBinary,
ResetAccount,
SetResetAccount,
AccountCreateWithDelegation,
WitnessSetProperties,
Transfer,
TransferToVesting,
WithdrawVesting,
LimitOrderCreate,
LimitOrderCancel,
FeedPublish,
Convert,
AccountWitnessVote,
AccountWitnessProxy,
Custom,
DeleteComment,
CustomJson,
CommentOptions,
SetWithdrawVestingRoute,
LimitOrderCreate2,
ClaimAccount,
CreateClaimedAccount,
ChangeRecoveryAccount,
TransferToSavings,
TransferFromSavings,
CancelTransferFromSavings,
DeclineVotingRights,
ClaimRewardBalance,
DelegateVestingShares,
AccountUpdate2,
CreateProposal,
UpdateProposalVotes,
RemoveProposal,
UpdateProposal,
CollateralizedConvert,
RecurrentTransfer,
)
}
}
fn read_no_extensions(r: &mut Reader<'_>) -> Result<NoExtensions> {
let count = r.varint32()?;
if count != 0 {
return Err(Error::ser(format!(
"operation carries {count} extension(s), which this build does not model"
)));
}
Ok(NoExtensions)
}
impl GrapheneDeserialize for ChainProperties {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
Ok(ChainProperties {
account_creation_fee: Amount::read_from(r)?,
maximum_block_size: r.u32()?,
hbd_interest_rate: r.u16()?,
})
}
}
impl GrapheneDeserialize for BlockId {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
let bytes = r.raw(20)?;
let mut out = [0u8; 20];
out.copy_from_slice(&bytes);
Ok(BlockId(out))
}
}
impl GrapheneDeserialize for Price {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
Ok(Price {
base: Amount::read_from(r)?,
quote: Amount::read_from(r)?,
})
}
}
impl GrapheneDeserialize for Beneficiary {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
Ok(Beneficiary {
account: r.string()?,
weight: r.u16()?,
})
}
}
impl GrapheneDeserialize for WitnessProperty {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
Ok(WitnessProperty {
key: r.string()?,
value: r.bytes()?,
})
}
}
impl GrapheneDeserialize for CommentOptionsExtension {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
match r.varint32()? {
0 => Ok(CommentOptionsExtension::Beneficiaries(r.array()?)),
tag => Err(Error::ser(format!(
"unknown comment_options extension variant {tag}"
))),
}
}
}
impl GrapheneDeserialize for RecurrentTransferExtension {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
match r.varint32()? {
1 => Ok(RecurrentTransferExtension::PairId(r.u8()?)),
tag => Err(Error::ser(format!(
"unknown recurrent_transfer extension variant {tag}"
))),
}
}
}
impl GrapheneDeserialize for Operation {
fn read_from(r: &mut Reader<'_>) -> Result<Self> {
let tag = r.varint32()?;
let id = OperationId::from_u32(tag)?;
if id.is_virtual() {
return Err(Error::ser(format!(
"{} is a virtual operation and cannot appear in a transaction",
id.name()
)));
}
Ok(match id {
OperationId::Vote => Operation::Vote(Vote {
voter: r.string()?,
author: r.string()?,
permlink: r.string()?,
weight: r.i16()?,
}),
OperationId::Comment => Operation::Comment(Comment {
parent_author: r.string()?,
parent_permlink: r.string()?,
author: r.string()?,
permlink: r.string()?,
title: r.string()?,
body: r.string()?,
json_metadata: r.string()?,
}),
OperationId::AccountCreate => Operation::AccountCreate(AccountCreate {
fee: Amount::read_from(r)?,
creator: r.string()?,
new_account_name: r.string()?,
owner: Authority::read_from(r)?,
active: Authority::read_from(r)?,
posting: Authority::read_from(r)?,
memo_key: PublicKey::read_from(r)?,
json_metadata: r.string()?,
}),
OperationId::AccountUpdate => Operation::AccountUpdate(AccountUpdate {
account: r.string()?,
owner: r.optional::<Authority>()?,
active: r.optional::<Authority>()?,
posting: r.optional::<Authority>()?,
memo_key: PublicKey::read_from(r)?,
json_metadata: r.string()?,
}),
OperationId::WitnessUpdate => Operation::WitnessUpdate(WitnessUpdate {
owner: r.string()?,
url: r.string()?,
block_signing_key: PublicKey::read_from(r)?,
props: ChainProperties::read_from(r)?,
fee: Amount::read_from(r)?,
}),
OperationId::WitnessBlockApprove => {
Operation::WitnessBlockApprove(WitnessBlockApprove {
witness: r.string()?,
block_id: BlockId::read_from(r)?,
})
}
OperationId::RequestAccountRecovery => {
Operation::RequestAccountRecovery(RequestAccountRecovery {
recovery_account: r.string()?,
account_to_recover: r.string()?,
new_owner_authority: Authority::read_from(r)?,
extensions: read_no_extensions(r)?,
})
}
OperationId::RecoverAccount => Operation::RecoverAccount(RecoverAccount {
account_to_recover: r.string()?,
new_owner_authority: Authority::read_from(r)?,
recent_owner_authority: Authority::read_from(r)?,
extensions: read_no_extensions(r)?,
}),
OperationId::EscrowTransfer => Operation::EscrowTransfer(EscrowTransfer {
from: r.string()?,
to: r.string()?,
hbd_amount: Amount::read_from(r)?,
hive_amount: Amount::read_from(r)?,
escrow_id: r.u32()?,
agent: r.string()?,
fee: Amount::read_from(r)?,
json_meta: r.string()?,
ratification_deadline: r.point_in_time()?,
escrow_expiration: r.point_in_time()?,
}),
OperationId::EscrowDispute => Operation::EscrowDispute(EscrowDispute {
from: r.string()?,
to: r.string()?,
agent: r.string()?,
who: r.string()?,
escrow_id: r.u32()?,
}),
OperationId::EscrowRelease => Operation::EscrowRelease(EscrowRelease {
from: r.string()?,
to: r.string()?,
agent: r.string()?,
who: r.string()?,
receiver: r.string()?,
escrow_id: r.u32()?,
hbd_amount: Amount::read_from(r)?,
hive_amount: Amount::read_from(r)?,
}),
OperationId::EscrowApprove => Operation::EscrowApprove(EscrowApprove {
from: r.string()?,
to: r.string()?,
agent: r.string()?,
who: r.string()?,
escrow_id: r.u32()?,
approve: r.bool()?,
}),
OperationId::CustomBinary => Operation::CustomBinary(CustomBinary {
required_owner_auths: r.array::<String>()?,
required_active_auths: r.array::<String>()?,
required_posting_auths: r.array::<String>()?,
required_auths: r.array::<Authority>()?,
id: r.string()?,
data: HexBytes::read_from(r)?,
}),
OperationId::ResetAccount => Operation::ResetAccount(ResetAccount {
reset_account: r.string()?,
account_to_reset: r.string()?,
new_owner_authority: Authority::read_from(r)?,
}),
OperationId::SetResetAccount => Operation::SetResetAccount(SetResetAccount {
account: r.string()?,
current_reset_account: r.string()?,
reset_account: r.string()?,
}),
OperationId::AccountCreateWithDelegation => {
Operation::AccountCreateWithDelegation(AccountCreateWithDelegation {
fee: Amount::read_from(r)?,
delegation: Amount::read_from(r)?,
creator: r.string()?,
new_account_name: r.string()?,
owner: Authority::read_from(r)?,
active: Authority::read_from(r)?,
posting: Authority::read_from(r)?,
memo_key: PublicKey::read_from(r)?,
json_metadata: r.string()?,
extensions: read_no_extensions(r)?,
})
}
OperationId::WitnessSetProperties => {
Operation::WitnessSetProperties(WitnessSetProperties {
owner: r.string()?,
props: r.array::<WitnessProperty>()?,
extensions: read_no_extensions(r)?,
})
}
OperationId::Transfer => Operation::Transfer(Transfer {
from: r.string()?,
to: r.string()?,
amount: Amount::read_from(r)?,
memo: r.string()?,
}),
OperationId::TransferToVesting => Operation::TransferToVesting(TransferToVesting {
from: r.string()?,
to: r.string()?,
amount: Amount::read_from(r)?,
}),
OperationId::WithdrawVesting => Operation::WithdrawVesting(WithdrawVesting {
account: r.string()?,
vesting_shares: Amount::read_from(r)?,
}),
OperationId::LimitOrderCreate => Operation::LimitOrderCreate(LimitOrderCreate {
owner: r.string()?,
orderid: r.u32()?,
amount_to_sell: Amount::read_from(r)?,
min_to_receive: Amount::read_from(r)?,
fill_or_kill: r.bool()?,
expiration: r.point_in_time()?,
}),
OperationId::LimitOrderCancel => Operation::LimitOrderCancel(LimitOrderCancel {
owner: r.string()?,
orderid: r.u32()?,
}),
OperationId::FeedPublish => Operation::FeedPublish(FeedPublish {
publisher: r.string()?,
exchange_rate: Price::read_from(r)?,
}),
OperationId::Convert => Operation::Convert(Convert {
owner: r.string()?,
requestid: r.u32()?,
amount: Amount::read_from(r)?,
}),
OperationId::AccountWitnessVote => Operation::AccountWitnessVote(AccountWitnessVote {
account: r.string()?,
witness: r.string()?,
approve: r.bool()?,
}),
OperationId::AccountWitnessProxy => {
Operation::AccountWitnessProxy(AccountWitnessProxy {
account: r.string()?,
proxy: r.string()?,
})
}
OperationId::Custom => Operation::Custom(Custom {
required_auths: r.array::<String>()?,
id: r.u16()?,
data: HexBytes::read_from(r)?,
}),
OperationId::DeleteComment => Operation::DeleteComment(DeleteComment {
author: r.string()?,
permlink: r.string()?,
}),
OperationId::CustomJson => Operation::CustomJson(CustomJson {
required_auths: r.array::<String>()?,
required_posting_auths: r.array::<String>()?,
id: r.string()?,
json: r.string()?,
}),
OperationId::CommentOptions => Operation::CommentOptions(CommentOptions {
author: r.string()?,
permlink: r.string()?,
max_accepted_payout: Amount::read_from(r)?,
percent_hbd: r.u16()?,
allow_votes: r.bool()?,
allow_curation_rewards: r.bool()?,
extensions: r.array::<CommentOptionsExtension>()?,
}),
OperationId::SetWithdrawVestingRoute => {
Operation::SetWithdrawVestingRoute(SetWithdrawVestingRoute {
from_account: r.string()?,
to_account: r.string()?,
percent: r.u16()?,
auto_vest: r.bool()?,
})
}
OperationId::LimitOrderCreate2 => Operation::LimitOrderCreate2(LimitOrderCreate2 {
owner: r.string()?,
orderid: r.u32()?,
amount_to_sell: Amount::read_from(r)?,
exchange_rate: Price::read_from(r)?,
fill_or_kill: r.bool()?,
expiration: r.point_in_time()?,
}),
OperationId::ClaimAccount => Operation::ClaimAccount(ClaimAccount {
creator: r.string()?,
fee: Amount::read_from(r)?,
extensions: read_no_extensions(r)?,
}),
OperationId::CreateClaimedAccount => {
Operation::CreateClaimedAccount(CreateClaimedAccount {
creator: r.string()?,
new_account_name: r.string()?,
owner: Authority::read_from(r)?,
active: Authority::read_from(r)?,
posting: Authority::read_from(r)?,
memo_key: PublicKey::read_from(r)?,
json_metadata: r.string()?,
extensions: read_no_extensions(r)?,
})
}
OperationId::ChangeRecoveryAccount => {
Operation::ChangeRecoveryAccount(ChangeRecoveryAccount {
account_to_recover: r.string()?,
new_recovery_account: r.string()?,
extensions: read_no_extensions(r)?,
})
}
OperationId::TransferToSavings => Operation::TransferToSavings(TransferToSavings {
from: r.string()?,
to: r.string()?,
amount: Amount::read_from(r)?,
memo: r.string()?,
}),
OperationId::TransferFromSavings => {
Operation::TransferFromSavings(TransferFromSavings {
from: r.string()?,
request_id: r.u32()?,
to: r.string()?,
amount: Amount::read_from(r)?,
memo: r.string()?,
})
}
OperationId::CancelTransferFromSavings => {
Operation::CancelTransferFromSavings(CancelTransferFromSavings {
from: r.string()?,
request_id: r.u32()?,
})
}
OperationId::DeclineVotingRights => {
Operation::DeclineVotingRights(DeclineVotingRights {
account: r.string()?,
decline: r.bool()?,
})
}
OperationId::ClaimRewardBalance => Operation::ClaimRewardBalance(ClaimRewardBalance {
account: r.string()?,
reward_hive: Amount::read_from(r)?,
reward_hbd: Amount::read_from(r)?,
reward_vests: Amount::read_from(r)?,
}),
OperationId::DelegateVestingShares => {
Operation::DelegateVestingShares(DelegateVestingShares {
delegator: r.string()?,
delegatee: r.string()?,
vesting_shares: Amount::read_from(r)?,
})
}
OperationId::AccountUpdate2 => Operation::AccountUpdate2(AccountUpdate2 {
account: r.string()?,
owner: r.optional::<Authority>()?,
active: r.optional::<Authority>()?,
posting: r.optional::<Authority>()?,
memo_key: r.optional::<PublicKey>()?,
json_metadata: r.string()?,
posting_json_metadata: r.string()?,
extensions: read_no_extensions(r)?,
}),
OperationId::CreateProposal => Operation::CreateProposal(CreateProposal {
creator: r.string()?,
receiver: r.string()?,
start_date: r.point_in_time()?,
end_date: r.point_in_time()?,
daily_pay: Amount::read_from(r)?,
subject: r.string()?,
permlink: r.string()?,
extensions: read_no_extensions(r)?,
}),
OperationId::UpdateProposalVotes => {
Operation::UpdateProposalVotes(UpdateProposalVotes {
voter: r.string()?,
proposal_ids: r.array::<u64>()?,
approve: r.bool()?,
extensions: read_no_extensions(r)?,
})
}
OperationId::RemoveProposal => Operation::RemoveProposal(RemoveProposal {
proposal_owner: r.string()?,
proposal_ids: r.array::<u64>()?,
extensions: read_no_extensions(r)?,
}),
OperationId::UpdateProposal => Operation::UpdateProposal(UpdateProposal {
proposal_id: r.u64()?,
creator: r.string()?,
daily_pay: Amount::read_from(r)?,
subject: r.string()?,
permlink: r.string()?,
extensions: read_no_extensions(r)?,
}),
OperationId::CollateralizedConvert => {
Operation::CollateralizedConvert(CollateralizedConvert {
owner: r.string()?,
requestid: r.u32()?,
amount: Amount::read_from(r)?,
})
}
OperationId::RecurrentTransfer => Operation::RecurrentTransfer(RecurrentTransfer {
from: r.string()?,
to: r.string()?,
amount: Amount::read_from(r)?,
memo: r.string()?,
recurrence: r.u16()?,
executions: r.u16()?,
extensions: r.array::<RecurrentTransferExtension>()?,
}),
OperationId::Pow | OperationId::Pow2 => {
return Err(Error::ser(format!(
"{} is an obsolete mining operation that this build does not decode",
id.name()
)))
}
other => {
return Err(Error::ser(format!(
"operation {} has no decoder",
other.name()
)))
}
})
}
}
impl Operation {
pub fn from_json(value: &serde_json::Value) -> Result<Self> {
let (name, payload) = virtual_ops::split_operation_json(value)?;
Self::from_named(name, payload)
}
pub fn from_json_owned(value: serde_json::Value) -> Result<Self> {
let (name, payload) = virtual_ops::split_operation_json_owned(value)?;
Self::from_named(name, payload)
}
fn from_named(name: String, payload: serde_json::Value) -> Result<Self> {
let id = OperationId::from_name(&name)?;
if id.is_virtual() {
return Err(Error::ser(format!(
"{} is a virtual operation and cannot be built as a signable operation",
id.name()
)));
}
Self::from_parts(id, payload)
}
fn from_parts(id: OperationId, value: serde_json::Value) -> Result<Self> {
let de_err =
|e: serde_json::Error| Error::ser(format!("could not decode {}: {e}", id.name()));
Ok(match id {
OperationId::Vote => Operation::Vote(serde_json::from_value(value).map_err(de_err)?),
OperationId::Comment => {
Operation::Comment(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountCreate => {
Operation::AccountCreate(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountUpdate => {
Operation::AccountUpdate(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::WitnessUpdate => {
Operation::WitnessUpdate(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::WitnessBlockApprove => {
Operation::WitnessBlockApprove(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::RequestAccountRecovery => {
Operation::RequestAccountRecovery(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::RecoverAccount => {
Operation::RecoverAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::EscrowTransfer => {
Operation::EscrowTransfer(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::EscrowDispute => {
Operation::EscrowDispute(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::EscrowRelease => {
Operation::EscrowRelease(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::EscrowApprove => {
Operation::EscrowApprove(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CustomBinary => {
Operation::CustomBinary(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::ResetAccount => {
Operation::ResetAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::SetResetAccount => {
Operation::SetResetAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountCreateWithDelegation => Operation::AccountCreateWithDelegation(
serde_json::from_value(value).map_err(de_err)?,
),
OperationId::WitnessSetProperties => {
Operation::WitnessSetProperties(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::Transfer => {
Operation::Transfer(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::TransferToVesting => {
Operation::TransferToVesting(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::WithdrawVesting => {
Operation::WithdrawVesting(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::LimitOrderCreate => {
Operation::LimitOrderCreate(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::LimitOrderCancel => {
Operation::LimitOrderCancel(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::FeedPublish => {
Operation::FeedPublish(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::Convert => {
Operation::Convert(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountWitnessVote => {
Operation::AccountWitnessVote(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountWitnessProxy => {
Operation::AccountWitnessProxy(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::Custom => {
Operation::Custom(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::DeleteComment => {
Operation::DeleteComment(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CustomJson => {
Operation::CustomJson(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CommentOptions => {
Operation::CommentOptions(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::SetWithdrawVestingRoute => {
Operation::SetWithdrawVestingRoute(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::LimitOrderCreate2 => {
Operation::LimitOrderCreate2(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::ClaimAccount => {
Operation::ClaimAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CreateClaimedAccount => {
Operation::CreateClaimedAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::ChangeRecoveryAccount => {
Operation::ChangeRecoveryAccount(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::TransferToSavings => {
Operation::TransferToSavings(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::TransferFromSavings => {
Operation::TransferFromSavings(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CancelTransferFromSavings => {
Operation::CancelTransferFromSavings(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::DeclineVotingRights => {
Operation::DeclineVotingRights(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::ClaimRewardBalance => {
Operation::ClaimRewardBalance(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::DelegateVestingShares => {
Operation::DelegateVestingShares(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::AccountUpdate2 => {
Operation::AccountUpdate2(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CreateProposal => {
Operation::CreateProposal(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::UpdateProposalVotes => {
Operation::UpdateProposalVotes(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::RemoveProposal => {
Operation::RemoveProposal(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::UpdateProposal => {
Operation::UpdateProposal(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::CollateralizedConvert => {
Operation::CollateralizedConvert(serde_json::from_value(value).map_err(de_err)?)
}
OperationId::RecurrentTransfer => {
Operation::RecurrentTransfer(serde_json::from_value(value).map_err(de_err)?)
}
other => {
return Err(Error::ser(format!(
"operation {} cannot be built from JSON",
other.name()
)))
}
})
}
pub fn to_json(&self) -> Result<serde_json::Value> {
let value = serde_json::to_value(self)
.map_err(|e| Error::ser(format!("could not render operation as JSON: {e}")))?;
Ok(serde_json::json!([self.id().name(), value]))
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(untagged)]
pub enum AnyOperation {
Signed(Operation),
Virtual(VirtualOperation),
}
impl AnyOperation {
pub fn from_json(value: &serde_json::Value) -> Result<Self> {
let (name, payload) = virtual_ops::split_operation_json(value)?;
let id = OperationId::from_name(&name)?;
if id.is_virtual() {
VirtualOperation::from_json(value).map(AnyOperation::Virtual)
} else {
Operation::from_parts(id, payload).map(AnyOperation::Signed)
}
}
pub fn id(&self) -> OperationId {
match self {
AnyOperation::Signed(o) => o.id(),
AnyOperation::Virtual(o) => o.id(),
}
}
pub fn name(&self) -> &'static str {
self.id().name()
}
pub fn is_virtual(&self) -> bool {
matches!(self, AnyOperation::Virtual(_))
}
pub fn as_signed(&self) -> Option<&Operation> {
match self {
AnyOperation::Signed(o) => Some(o),
AnyOperation::Virtual(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chains::Chain;
fn hive(s: &str) -> Amount {
Amount::parse(s, Chain::Hive).unwrap()
}
#[test]
fn vote_wire_layout() {
let op = Operation::Vote(Vote {
voter: "alice".into(),
author: "bob".into(),
permlink: "a-post".into(),
weight: 10_000,
});
let wire = op.to_wire().unwrap();
let mut expected = vec![0x00]; expected.extend_from_slice(&[5, b'a', b'l', b'i', b'c', b'e']);
expected.extend_from_slice(&[3, b'b', b'o', b'b']);
expected.extend_from_slice(&[6, b'a', b'-', b'p', b'o', b's', b't']);
expected.extend_from_slice(&10_000i16.to_le_bytes());
assert_eq!(wire, expected);
}
#[test]
fn downvotes_serialize_as_a_negative_int16() {
let op = Operation::Vote(Vote {
voter: "a".into(),
author: "b".into(),
permlink: "c".into(),
weight: -10_000,
});
let wire = op.to_wire().unwrap();
assert_eq!(&wire[wire.len() - 2..], &(-10_000i16).to_le_bytes());
}
#[test]
fn custom_json_wire_layout() {
let op = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["alice".into()],
id: "my_app_action".into(),
json: r#"{"trx_id":"abc"}"#.into(),
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 18, "custom_json is operation 18");
assert_eq!(wire[1], 0, "no active auths");
assert_eq!(wire[2], 1, "one posting auth");
}
#[test]
fn custom_json_sorts_and_dedups_auth_sets() {
let unsorted = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["zulu".into(), "alpha".into(), "mike".into()],
id: "x".into(),
json: "{}".into(),
});
let sorted = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["alpha".into(), "mike".into(), "zulu".into()],
id: "x".into(),
json: "{}".into(),
});
assert_eq!(unsorted.to_wire().unwrap(), sorted.to_wire().unwrap());
let dup = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["bob".into(), "bob".into()],
id: "x".into(),
json: "{}".into(),
});
assert!(dup.validate().is_err());
}
#[test]
fn custom_json_enforces_the_id_length_and_a_signer() {
let too_long = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["a".into()],
id: "x".repeat(33),
json: "{}".into(),
});
assert!(too_long.validate().is_err());
let no_auths = Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec![],
id: "x".into(),
json: "{}".into(),
});
assert!(no_auths.validate().is_err());
}
#[test]
fn transfer_includes_the_memo() {
let op = Operation::Transfer(Transfer {
from: "alice".into(),
to: "bob".into(),
amount: hive("1.000 HIVE"),
memo: "thanks".into(),
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 2);
assert!(wire.ends_with(&[6, b't', b'h', b'a', b'n', b'k', b's']));
}
#[test]
fn recurrent_transfer_is_constructible_and_correctly_typed() {
let op = Operation::RecurrentTransfer(RecurrentTransfer {
from: "alice".into(),
to: "bob".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
recurrence: 24,
executions: 12,
extensions: vec![],
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 49, "recurrent_transfer is operation 49");
assert_eq!(&wire[wire.len() - 5..], &[24, 0, 12, 0, 0]);
}
#[test]
fn recurrent_transfer_carries_the_hf28_pair_id() {
let op = Operation::RecurrentTransfer(RecurrentTransfer {
from: "alice".into(),
to: "bob".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
recurrence: 24,
executions: 12,
extensions: vec![RecurrentTransferExtension::PairId(7)],
});
let wire = op.to_wire().unwrap();
assert_eq!(&wire[wire.len() - 3..], &[1, 1, 7]);
}
#[test]
fn escrow_transfer_matches_hiveds_own_bytes() {
let op = Operation::EscrowTransfer(EscrowTransfer {
from: "aaa".into(),
to: "bbb".into(),
hbd_amount: hive("1.000 HBD"),
hive_amount: hive("2.000 HIVE"),
escrow_id: 0x1122_3344,
agent: "ccc".into(),
fee: hive("0.100 HIVE"),
json_meta: "JM".into(),
ratification_deadline: PointInTime::from_unix(1_893_456_000).unwrap(), escrow_expiration: PointInTime::from_unix(1_927_756_800).unwrap(), });
let expected = hex_to_bytes(
"1b0361616103626262e8030000000000000353424400000000d00700000000000003535445 454d00004433221103636363640000000000000003535445454d0000024a4d80d8db70003c e772",
);
assert_eq!(op.to_wire().unwrap(), expected);
}
#[test]
fn limit_order_create2_matches_hiveds_own_bytes() {
let op = Operation::LimitOrderCreate2(LimitOrderCreate2 {
owner: "aaa".into(),
orderid: 0x1122_3344,
amount_to_sell: hive("7.000 HIVE"),
exchange_rate: Price {
base: hive("3.000 HIVE"),
quote: hive("5.000 HBD"),
},
fill_or_kill: true,
expiration: PointInTime::from_unix(1_893_456_000).unwrap(), });
let expected = hex_to_bytes(
"150361616144332211581b00000000000003535445454d0000b80b0000000000000353544 5454d0000881300000000000003534244000000000180d8db70",
);
assert_eq!(op.to_wire().unwrap(), expected);
}
#[test]
fn recurrent_transfer_pair_id_uses_the_json_shape_hived_accepts() {
let ext = RecurrentTransferExtension::PairId(7);
let json = serde_json::to_string(&ext).unwrap();
assert_eq!(
json,
r#"{"type":"recurrent_transfer_pair_id","value":{"pair_id":7}}"#
);
let from_object: RecurrentTransferExtension = serde_json::from_str(&json).unwrap();
let from_array: RecurrentTransferExtension =
serde_json::from_str(r#"[1, {"pair_id": 7}]"#).unwrap();
assert_eq!(from_object, ext);
assert_eq!(from_array, ext);
}
fn hex_to_bytes(s: &str) -> Vec<u8> {
let clean: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
(0..clean.len())
.step_by(2)
.map(|i| u8::from_str_radix(&clean[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn deserialize_then_serialize_is_not_the_identity_for_control_bytes() {
use crate::operations::Operation;
use crate::{Chain, GrapheneDeserialize, GrapheneSerialize, Reader};
let wire = {
let mut out = vec![0u8]; out.extend_from_slice(&[4, 0, 0, 0, 0]); out.extend_from_slice(&[0]); out.extend_from_slice(&[0]); out.extend_from_slice(&[1, 0]); out
};
let mut reader = Reader::new(&wire, Chain::Hive);
let op = Operation::read_from(&mut reader).expect("parses");
let again = op.to_wire().expect("re-serializes");
assert_ne!(again, wire, "if these are equal the asymmetry is gone");
let mut reader2 = Reader::new(&again, Chain::Hive);
let op2 = Operation::read_from(&mut reader2).expect("parses");
assert_eq!(op2.to_wire().expect("re-serializes"), again);
}
#[test]
fn collateralized_convert_is_constructible() {
let op = Operation::CollateralizedConvert(CollateralizedConvert {
owner: "alice".into(),
requestid: 1,
amount: hive("1.000 HIVE"),
});
assert_eq!(op.to_wire().unwrap()[0], 48);
}
#[test]
fn account_update2_encodes_absent_optionals_as_a_single_zero() {
let op = Operation::AccountUpdate2(AccountUpdate2 {
account: "alice".into(),
owner: None,
active: None,
posting: None,
memo_key: None,
json_metadata: String::new(),
posting_json_metadata: "{}".into(),
extensions: NoExtensions,
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 43);
assert_eq!(&wire[7..11], &[0, 0, 0, 0]);
}
#[test]
fn comment_options_rejects_over_full_beneficiaries() {
let op = Operation::CommentOptions(CommentOptions {
author: "alice".into(),
permlink: "p".into(),
max_accepted_payout: Amount::parse("1000000.000 HBD", Chain::Hive).unwrap(),
percent_hbd: 10_000,
allow_votes: true,
allow_curation_rewards: true,
extensions: vec![CommentOptionsExtension::Beneficiaries(vec![
Beneficiary {
account: "a".into(),
weight: 6000,
},
Beneficiary {
account: "b".into(),
weight: 5000,
},
])],
});
assert!(op.validate().is_err(), "11000 basis points must be refused");
}
#[test]
fn proposal_ids_are_sorted_and_deduped() {
let a = Operation::RemoveProposal(RemoveProposal {
proposal_owner: "alice".into(),
proposal_ids: vec![9, 3, 5],
extensions: NoExtensions,
});
let b = Operation::RemoveProposal(RemoveProposal {
proposal_owner: "alice".into(),
proposal_ids: vec![3, 5, 9],
extensions: NoExtensions,
});
assert_eq!(a.to_wire().unwrap(), b.to_wire().unwrap());
let dup = Operation::RemoveProposal(RemoveProposal {
proposal_owner: "alice".into(),
proposal_ids: vec![3, 3],
extensions: NoExtensions,
});
assert!(dup.validate().is_err());
}
#[test]
fn every_variant_reports_a_non_virtual_id() {
let ops = sample_of_every_variant();
for op in &ops {
assert!(
!op.id().is_virtual(),
"{:?} is virtual and must not be in the Operation enum",
op.id()
);
}
}
#[test]
fn every_variant_serializes_with_its_own_tag() {
for op in sample_of_every_variant() {
let wire = op.to_wire().unwrap();
let (tag, _) = crate::types::read_varint32(&wire).unwrap();
assert_eq!(tag, op.id().as_u32(), "{:?} wrote the wrong tag", op.id());
}
}
#[test]
fn escrow_release_carries_agent_and_receiver() {
let op = Operation::EscrowRelease(EscrowRelease {
from: "alice".into(),
to: "bob".into(),
agent: "carol".into(),
who: "alice".into(),
receiver: "bob".into(),
escrow_id: 1,
hbd_amount: hive("1.000 HBD"),
hive_amount: hive("2.000 HIVE"),
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 29);
let names_len: usize = ["alice", "bob", "carol", "alice", "bob"]
.iter()
.map(|n| 1 + n.len())
.sum();
assert_eq!(wire.len(), 1 + names_len + 4 + 16 + 16);
assert!(wire.windows(5).any(|w| w == b"carol"));
}
#[test]
fn escrow_dispute_carries_agent() {
let op = Operation::EscrowDispute(EscrowDispute {
from: "alice".into(),
to: "bob".into(),
agent: "carol".into(),
who: "alice".into(),
escrow_id: 7,
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 28);
assert!(wire.windows(5).any(|w| w == b"carol"));
assert_eq!(&wire[wire.len() - 4..], &7u32.to_le_bytes());
}
#[test]
fn a_custom_json_payload_is_capped_at_the_boundary() {
let at_limit = |n: usize| {
Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["alice".into()],
id: "my_app".into(),
json: "x".repeat(n),
})
};
assert!(
at_limit(MAX_CUSTOM_DATA_LEN).validate().is_ok(),
"exactly {MAX_CUSTOM_DATA_LEN} bytes is within the limit, not past it"
);
let err = at_limit(MAX_CUSTOM_DATA_LEN + 1)
.validate()
.expect_err("one byte over must be refused");
let text = err.to_string();
assert!(
text.contains("8193"),
"the error should say how big it was: {text}"
);
assert!(
text.contains("whole transaction"),
"the error should say the whole transaction is rejected: {text}"
);
}
#[test]
fn a_custom_binary_payload_is_capped_at_the_boundary() {
let op = |n: usize| {
Operation::Custom(Custom {
required_auths: vec!["alice".into()],
id: 7,
data: HexBytes(vec![0x5a; n]),
})
};
assert!(op(MAX_CUSTOM_DATA_LEN).validate().is_ok());
assert!(op(MAX_CUSTOM_DATA_LEN + 1).validate().is_err());
}
#[test]
fn the_payload_cap_matches_hived() {
assert_eq!(MAX_CUSTOM_DATA_LEN, 8192);
assert_eq!(MAX_CUSTOM_ID_LEN, 32);
}
#[test]
fn the_length_bounds_sit_exactly_where_hived_puts_them() {
use crate::Chain;
let transfer = |memo: &str| {
Operation::Transfer(Transfer {
from: "alice".into(),
to: "bob".into(),
amount: Amount::parse("1.000 HIVE", Chain::Hive).unwrap(),
memo: memo.into(),
})
};
assert!(transfer(&"m".repeat(MAX_MEMO_LEN)).validate().is_ok());
assert!(transfer(&"m".repeat(MAX_MEMO_LEN + 1)).validate().is_err());
let comment = |title: &str, permlink: &str| {
Operation::Comment(Comment {
parent_author: String::new(),
parent_permlink: "hive-100".into(),
author: "alice".into(),
permlink: permlink.into(),
title: title.into(),
body: "b".into(),
json_metadata: "{}".into(),
})
};
let t = "t".repeat(MAX_TITLE_LEN);
let p = "p".repeat(MAX_PERMLINK_LEN);
assert!(
comment(&t, &p).validate().is_ok(),
"both exactly at the bound"
);
assert!(comment(&"t".repeat(MAX_TITLE_LEN + 1), &p)
.validate()
.is_err());
assert!(comment(&t, &"p".repeat(MAX_PERMLINK_LEN + 1))
.validate()
.is_err());
}
#[test]
fn the_length_bounds_match_hived() {
assert_eq!(MAX_MEMO_LEN, 2048 - 1);
assert_eq!(MAX_TITLE_LEN, 256 - 1);
assert_eq!(MAX_PERMLINK_LEN, 256 - 1);
}
#[test]
fn every_memo_carrying_operation_is_bounded() {
use crate::types::PointInTime;
use crate::Chain;
let big = "m".repeat(MAX_MEMO_LEN + 1);
let amount = Amount::parse("1.000 HIVE", Chain::Hive).unwrap();
let ops = vec![
Operation::Transfer(Transfer {
from: "alice".into(),
to: "bob".into(),
amount,
memo: big.clone(),
}),
Operation::TransferToSavings(TransferToSavings {
from: "alice".into(),
to: "bob".into(),
amount,
memo: big.clone(),
}),
Operation::TransferFromSavings(TransferFromSavings {
from: "alice".into(),
request_id: 1,
to: "bob".into(),
amount,
memo: big.clone(),
}),
Operation::RecurrentTransfer(RecurrentTransfer {
from: "alice".into(),
to: "bob".into(),
amount,
memo: big,
recurrence: 24,
executions: 2,
extensions: vec![],
}),
];
let _ = PointInTime::from_unix(0);
for op in ops {
let name = op.id().name();
assert!(op.validate().is_err(), "{name} did not bound its memo");
}
}
#[test]
fn the_remaining_bounds_sit_exactly_where_hived_puts_them() {
use crate::types::PointInTime;
use crate::Chain;
let proposal = |subject: &str, permlink: &str| {
Operation::CreateProposal(CreateProposal {
creator: "alice".into(),
receiver: "bob".into(),
start_date: PointInTime::from_unix(1_893_456_000).unwrap(),
end_date: PointInTime::from_unix(1_927_756_800).unwrap(),
daily_pay: Amount::parse("1.000 HBD", Chain::Hive).unwrap(),
subject: subject.into(),
permlink: permlink.into(),
extensions: NoExtensions,
})
};
let subj = "s".repeat(MAX_PROPOSAL_SUBJECT_LEN);
assert!(
proposal(&subj, "p").validate().is_ok(),
"exactly 80 is allowed"
);
assert!(proposal(&"s".repeat(MAX_PROPOSAL_SUBJECT_LEN + 1), "p")
.validate()
.is_err());
assert!(proposal(&subj, &"p".repeat(MAX_PERMLINK_LEN + 1))
.validate()
.is_err());
let witness = |url: &str| {
Operation::WitnessUpdate(WitnessUpdate {
owner: "alice".into(),
url: url.into(),
block_signing_key: PublicKey::from_prefixed_any(
"STM6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV",
)
.unwrap(),
props: ChainProperties {
account_creation_fee: Amount::parse("3.000 HIVE", Chain::Hive).unwrap(),
maximum_block_size: 65_536,
hbd_interest_rate: 0,
},
fee: Amount::parse("0.000 HIVE", Chain::Hive).unwrap(),
})
};
assert!(
witness(&"u".repeat(MAX_WITNESS_URL_LEN)).validate().is_ok(),
"2048 allowed"
);
assert!(witness(&"u".repeat(MAX_WITNESS_URL_LEN + 1))
.validate()
.is_err());
}
#[test]
fn proposal_id_lists_are_bounded() {
let votes = |n: u64| {
Operation::UpdateProposalVotes(UpdateProposalVotes {
voter: "alice".into(),
proposal_ids: (0..n).collect(),
approve: true,
extensions: NoExtensions,
})
};
assert!(votes(0).validate().is_err(), "empty is refused");
assert!(votes(MAX_PROPOSAL_IDS as u64).validate().is_ok());
assert!(votes(MAX_PROPOSAL_IDS as u64 + 1).validate().is_err());
}
#[test]
fn the_remaining_bounds_match_hived() {
assert_eq!(MAX_AUTHORITY_MEMBERSHIP, 40);
assert_eq!(MAX_BENEFICIARIES, 128 - 1);
assert_eq!(MAX_PROPOSAL_SUBJECT_LEN, 80);
assert_eq!(MAX_PROPOSAL_IDS, 5);
assert_eq!(MAX_WITNESS_URL_LEN, 2048);
}
#[test]
fn custom_binary_has_all_six_fields() {
let op = Operation::CustomBinary(CustomBinary {
required_owner_auths: vec![],
required_active_auths: vec!["alice".into()],
required_posting_auths: vec![],
required_auths: vec![],
id: "app".into(),
data: HexBytes(vec![0xde, 0xad]),
});
let wire = op.to_wire().unwrap();
assert_eq!(wire[0], 35);
assert_eq!(wire[1], 0, "no owner auths");
assert_eq!(wire[2], 1, "one active auth");
assert_eq!(&wire[3..9], b"\x05alice");
assert_eq!(wire[9], 0, "no posting auths");
assert_eq!(wire[10], 0, "no authority objects");
assert_eq!(
&wire[11..15],
b"\x03app",
"id is a length-prefixed string, not a u16"
);
}
#[test]
fn witness_set_properties_sorts_its_flat_map() {
let a = Operation::WitnessSetProperties(WitnessSetProperties {
owner: "w".into(),
props: vec![
WitnessProperty::uint32("maximum_block_size", 65536),
WitnessProperty::uint16("hbd_interest_rate", 1000),
],
extensions: NoExtensions,
});
let b = Operation::WitnessSetProperties(WitnessSetProperties {
owner: "w".into(),
props: vec![
WitnessProperty::uint16("hbd_interest_rate", 1000),
WitnessProperty::uint32("maximum_block_size", 65536),
],
extensions: NoExtensions,
});
assert_eq!(a.to_wire().unwrap(), b.to_wire().unwrap());
let dup = Operation::WitnessSetProperties(WitnessSetProperties {
owner: "w".into(),
props: vec![
WitnessProperty::uint32("maximum_block_size", 1),
WitnessProperty::uint32("maximum_block_size", 2),
],
extensions: NoExtensions,
});
assert!(dup.validate().is_err());
}
#[test]
fn account_update_memo_key_is_not_optional() {
let key = crate::keys::PrivateKey::generate().public_key();
let update = Operation::AccountUpdate(AccountUpdate {
account: "a".into(),
owner: None,
active: None,
posting: None,
memo_key: key,
json_metadata: String::new(),
});
let wire = update.to_wire().unwrap();
assert_eq!(&wire[3..6], &[0, 0, 0]);
assert_eq!(&wire[6..39], &key.to_bytes());
}
#[test]
fn every_non_virtual_operation_except_mining_is_constructible() {
use std::collections::HashSet;
let built: HashSet<u32> = sample_of_every_variant()
.iter()
.map(|op| op.id().as_u32())
.collect();
let obsolete_mining: HashSet<u32> = [14, 30].into_iter().collect();
let missing: Vec<u32> = (0..FIRST_VIRTUAL_OP)
.filter(|id| !built.contains(id) && !obsolete_mining.contains(id))
.collect();
assert!(
missing.is_empty(),
"operations not constructible: {missing:?}"
);
assert_eq!(
built.len(),
(FIRST_VIRTUAL_OP as usize) - obsolete_mining.len()
);
}
#[test]
fn every_operation_round_trips_through_the_wire_format() {
for op in sample_of_every_variant() {
let wire = op.to_wire().unwrap();
let mut r = Reader::new(&wire, Chain::Hive);
let back = Operation::read_from(&mut r)
.unwrap_or_else(|e| panic!("{:?} failed to decode: {e}", op.id()));
r.expect_end()
.unwrap_or_else(|e| panic!("{:?} left bytes unread: {e}", op.id()));
assert_eq!(back.id(), op.id());
assert_eq!(
back.to_wire().unwrap(),
wire,
"{:?} did not round trip",
op.id()
);
}
}
#[test]
fn decoding_refuses_virtual_operations() {
let mut wire = Vec::new();
crate::types::write_varint32(&mut wire, OperationId::ProducerReward.as_u32());
let mut r = Reader::new(&wire, Chain::Hive);
let err = Operation::read_from(&mut r).unwrap_err();
assert!(format!("{err}").contains("virtual"));
}
#[test]
fn decoding_refuses_unknown_operation_ids() {
let mut wire = Vec::new();
crate::types::write_varint32(&mut wire, 200);
let mut r = Reader::new(&wire, Chain::Hive);
assert!(Operation::read_from(&mut r).is_err());
}
#[test]
fn decoding_refuses_unmodelled_extensions() {
let mut wire = Vec::new();
crate::types::write_varint32(&mut wire, OperationId::ClaimAccount.as_u32());
crate::types::write_string(&mut wire, "alice").unwrap();
hive("0.000 HIVE").append_to(&mut wire).unwrap();
crate::types::write_varint32(&mut wire, 1); let mut r = Reader::new(&wire, Chain::Hive);
let err = Operation::read_from(&mut r).unwrap_err();
assert!(format!("{err}").contains("extension"));
}
#[test]
fn truncated_operations_error_rather_than_panic() {
let op = Operation::Transfer(Transfer {
from: "alice".into(),
to: "bob".into(),
amount: hive("1.000 HIVE"),
memo: "hello".into(),
});
let wire = op.to_wire().unwrap();
for cut in 1..wire.len() {
let mut r = Reader::new(&wire[..cut], Chain::Hive);
let decoded = Operation::read_from(&mut r).and_then(|o| {
r.expect_end()?;
Ok(o)
});
assert!(decoded.is_err(), "truncating to {cut} bytes should fail");
}
}
#[test]
fn json_round_trips_every_operation() {
for op in sample_of_every_variant() {
let json = op.to_json().unwrap();
let back = Operation::from_json(&json)
.unwrap_or_else(|e| panic!("{:?} failed to parse back: {e}", op.id()));
assert_eq!(back, op, "{:?} did not round trip through JSON", op.id());
}
}
#[test]
fn json_accepts_both_the_condenser_and_appbase_shapes() {
let condenser = serde_json::json!([
"transfer",
{"from": "alice", "to": "bob", "amount": "1.000 HIVE", "memo": "hi"}
]);
let appbase = serde_json::json!({
"type": "transfer_operation",
"value": {"from": "alice", "to": "bob", "amount": "1.000 HIVE", "memo": "hi"}
});
let a = Operation::from_json(&condenser).unwrap();
let b = Operation::from_json(&appbase).unwrap();
assert_eq!(a, b);
assert_eq!(a.id(), OperationId::Transfer);
}
#[test]
fn json_refuses_a_virtual_operation_as_signable() {
let vop = serde_json::json!([
"producer_reward",
{"producer": "alice", "vesting_shares": "1.000000 VESTS"}
]);
assert!(Operation::from_json(&vop).is_err());
let any = AnyOperation::from_json(&vop).unwrap();
assert!(any.is_virtual());
assert_eq!(any.name(), "producer_reward");
assert!(any.as_signed().is_none());
}
#[test]
fn json_refuses_malformed_and_unknown_shapes() {
assert!(Operation::from_json(&serde_json::json!("transfer")).is_err());
assert!(Operation::from_json(&serde_json::json!(["transfer"])).is_err());
assert!(Operation::from_json(&serde_json::json!(["nope", {}])).is_err());
assert!(Operation::from_json(&serde_json::json!({"type": 1, "value": {}})).is_err());
}
#[test]
fn json_refuses_unmodelled_extensions() {
let op = serde_json::json!([
"claim_account",
{"creator": "alice", "fee": "0.000 HIVE", "extensions": [[99, {}]]}
]);
assert!(Operation::from_json(&op).is_err());
}
#[test]
fn any_operation_reads_a_mixed_history_stream() {
let stream = serde_json::json!([
["transfer", {"from": "a", "to": "b", "amount": "1.000 HIVE", "memo": ""}],
["producer_reward", {"producer": "w", "vesting_shares": "1.000000 VESTS"}],
["fill_recurrent_transfer", {"from": "a", "to": "b", "amount": "1.000 HIVE",
"memo": "", "remaining_executions": 5}],
["curation_reward", {"curator": "c", "reward": "1.000000 VESTS",
"author": "a", "permlink": "p",
"payout_must_be_claimed": true}],
]);
let ops: Vec<AnyOperation> = stream
.as_array()
.unwrap()
.iter()
.map(|v| AnyOperation::from_json(v).unwrap())
.collect();
assert_eq!(ops.len(), 4);
assert!(!ops[0].is_virtual());
assert!(ops[1].is_virtual() && ops[2].is_virtual() && ops[3].is_virtual());
assert_eq!(
ops.iter().map(|o| o.id().as_u32()).collect::<Vec<_>>(),
vec![2, 64, 83, 52]
);
}
#[test]
fn hf25_and_later_virtual_operations_parse() {
let cases = [
(
"limit_order_cancelled",
serde_json::json!({"seller": "a", "orderid": 1, "amount_back": "1.000 HIVE"}),
),
(
"proposal_fee",
serde_json::json!({"creator": "a", "treasury": "t", "proposal_id": 1, "fee": "10.000 HBD"}),
),
("producer_missed", serde_json::json!({"producer": "w"})),
(
"proxy_cleared",
serde_json::json!({"account": "a", "proxy": "p"}),
),
(
"escrow_approved",
serde_json::json!({"from": "a", "to": "b", "agent": "c", "escrow_id": 1, "fee": "0.100 HIVE"}),
),
(
"declined_voting_rights",
serde_json::json!({"account": "a"}),
),
(
"expired_account_notification",
serde_json::json!({"account": "a"}),
),
(
"collateralized_convert_immediate_conversion",
serde_json::json!({"owner": "a", "requestid": 1, "hbd_out": "1.000 HBD"}),
),
];
for (name, value) in cases {
let json = serde_json::json!([name, value]);
let op = AnyOperation::from_json(&json)
.unwrap_or_else(|e| panic!("{name} failed to parse: {e}"));
assert!(op.is_virtual(), "{name} should be virtual");
assert_eq!(op.name(), name);
}
}
#[test]
fn a_recurrent_transfer_fill_carries_its_pair_id() {
let json = serde_json::json!([
"fill_recurrent_transfer",
{"from": "a", "to": "b", "amount": "1.000 HIVE", "memo": "",
"remaining_executions": 3, "extensions": [[1, {"pair_id": 7}]]}
]);
let op = AnyOperation::from_json(&json).unwrap();
match op {
AnyOperation::Virtual(VirtualOperation::FillRecurrentTransfer(v)) => {
assert_eq!(v.remaining_executions, 3);
assert_eq!(v.extensions, vec![RecurrentTransferExtension::PairId(7)]);
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn virtual_operation_ids_are_the_chains_not_beems() {
for (name, id) in [
("fill_convert_request", 50u32),
("producer_reward", 64),
("fill_recurrent_transfer", 83),
("declined_voting_rights", 92),
] {
assert_eq!(OperationId::from_name(name).unwrap().as_u32(), id);
}
}
#[test]
fn the_owned_decoder_agrees_with_the_borrowing_one() {
for op in sample_of_every_variant() {
let payload = serde_json::to_value(&op).expect("operation renders as JSON");
let name = op.id().name();
let condenser = serde_json::json!([name, payload]);
let appbase = serde_json::json!({ "type": name, "value": payload });
for (shape, value) in [("condenser", condenser), ("appbase", appbase)] {
let borrowed = Operation::from_json(&value)
.unwrap_or_else(|e| panic!("{name} as {shape} failed to decode: {e}"));
let owned = Operation::from_json_owned(value)
.unwrap_or_else(|e| panic!("{name} as {shape} failed to decode owned: {e}"));
assert_eq!(borrowed, owned, "{name} decoded differently as {shape}");
assert_eq!(borrowed, op, "{name} did not round-trip as {shape}");
}
}
}
#[test]
fn the_owned_decoder_rejects_what_the_borrowing_one_rejects() {
let bad = [
serde_json::json!([]),
serde_json::json!(["vote"]),
serde_json::json!(["vote", {}, "extra"]),
serde_json::json!([
"ignored",
"vote",
{ "voter": "alice", "author": "bob", "permlink": "p", "weight": 10000 }
]),
serde_json::json!([
{ "voter": "alice", "author": "bob", "permlink": "p", "weight": 10000 },
"vote"
]),
serde_json::json!([1, {}]),
serde_json::json!({ "type": 7, "value": {} }),
serde_json::json!({ "type": "vote" }),
serde_json::json!({ "value": {} }),
serde_json::json!({}),
serde_json::json!("vote"),
serde_json::json!(42),
serde_json::json!(null),
serde_json::json!(["producer_reward", { "producer": "alice", "vesting_shares": "1.000000 VESTS" }]),
];
for value in bad {
let borrowed = Operation::from_json(&value);
let owned = Operation::from_json_owned(value.clone());
assert_eq!(
borrowed.is_err(),
owned.is_err(),
"the two decoders disagree on whether to accept {value}"
);
}
}
fn sample_of_every_variant() -> Vec<Operation> {
let key = crate::keys::PrivateKey::generate().public_key();
let auth = Authority::from_key(key).unwrap();
let t = PointInTime::from_unix(1_700_000_000).unwrap();
let price = Price {
base: hive("1.000 HIVE"),
quote: hive("1.000 HBD"),
};
vec![
Operation::AccountCreate(AccountCreate {
fee: hive("3.000 HIVE"),
creator: "a".into(),
new_account_name: "b".into(),
owner: auth.clone(),
active: auth.clone(),
posting: auth.clone(),
memo_key: key,
json_metadata: "{}".into(),
}),
Operation::AccountUpdate(AccountUpdate {
account: "a".into(),
owner: None,
active: Some(auth.clone()),
posting: None,
memo_key: key,
json_metadata: "{}".into(),
}),
Operation::WitnessUpdate(WitnessUpdate {
owner: "a".into(),
url: "https://example.org".into(),
block_signing_key: key,
props: ChainProperties {
account_creation_fee: hive("3.000 HIVE"),
maximum_block_size: 65536,
hbd_interest_rate: 1000,
},
fee: hive("0.000 HIVE"),
}),
Operation::WitnessBlockApprove(WitnessBlockApprove {
witness: "a".into(),
block_id: BlockId([7u8; 20]),
}),
Operation::RequestAccountRecovery(RequestAccountRecovery {
recovery_account: "a".into(),
account_to_recover: "b".into(),
new_owner_authority: auth.clone(),
extensions: NoExtensions,
}),
Operation::RecoverAccount(RecoverAccount {
account_to_recover: "a".into(),
new_owner_authority: auth.clone(),
recent_owner_authority: auth.clone(),
extensions: NoExtensions,
}),
Operation::EscrowTransfer(EscrowTransfer {
from: "a".into(),
to: "b".into(),
agent: "c".into(),
escrow_id: 1,
hbd_amount: hive("1.000 HBD"),
hive_amount: hive("1.000 HIVE"),
fee: hive("0.100 HIVE"),
ratification_deadline: t,
escrow_expiration: t,
json_meta: "{}".into(),
}),
Operation::EscrowDispute(EscrowDispute {
from: "a".into(),
to: "b".into(),
agent: "c".into(),
who: "a".into(),
escrow_id: 1,
}),
Operation::EscrowRelease(EscrowRelease {
from: "a".into(),
to: "b".into(),
agent: "c".into(),
who: "a".into(),
receiver: "b".into(),
escrow_id: 1,
hbd_amount: hive("1.000 HBD"),
hive_amount: hive("1.000 HIVE"),
}),
Operation::EscrowApprove(EscrowApprove {
from: "a".into(),
to: "b".into(),
agent: "c".into(),
who: "c".into(),
escrow_id: 1,
approve: true,
}),
Operation::CustomBinary(CustomBinary {
required_owner_auths: vec![],
required_active_auths: vec!["a".into()],
required_posting_auths: vec![],
required_auths: vec![],
id: "x".into(),
data: HexBytes(vec![1, 2, 3]),
}),
Operation::ResetAccount(ResetAccount {
reset_account: "a".into(),
account_to_reset: "b".into(),
new_owner_authority: auth.clone(),
}),
Operation::SetResetAccount(SetResetAccount {
account: "a".into(),
current_reset_account: "b".into(),
reset_account: "c".into(),
}),
Operation::AccountCreateWithDelegation(AccountCreateWithDelegation {
fee: hive("3.000 HIVE"),
delegation: hive("0.000000 VESTS"),
creator: "a".into(),
new_account_name: "b".into(),
owner: auth.clone(),
active: auth.clone(),
posting: auth.clone(),
memo_key: key,
json_metadata: "{}".into(),
extensions: NoExtensions,
}),
Operation::WitnessSetProperties(WitnessSetProperties {
owner: "a".into(),
props: vec![
WitnessProperty::uint32("maximum_block_size", 65536),
WitnessProperty::asset("account_creation_fee", &hive("3.000 HIVE")).unwrap(),
],
extensions: NoExtensions,
}),
Operation::Vote(Vote {
voter: "a".into(),
author: "b".into(),
permlink: "c".into(),
weight: 1,
}),
Operation::Comment(Comment {
parent_author: String::new(),
parent_permlink: "t".into(),
author: "a".into(),
permlink: "p".into(),
title: "T".into(),
body: "B".into(),
json_metadata: "{}".into(),
}),
Operation::Transfer(Transfer {
from: "a".into(),
to: "b".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
}),
Operation::TransferToVesting(TransferToVesting {
from: "a".into(),
to: "b".into(),
amount: hive("1.000 HIVE"),
}),
Operation::WithdrawVesting(WithdrawVesting {
account: "a".into(),
vesting_shares: hive("1.000000 VESTS"),
}),
Operation::LimitOrderCreate(LimitOrderCreate {
owner: "a".into(),
orderid: 1,
amount_to_sell: hive("1.000 HIVE"),
min_to_receive: hive("1.000 HBD"),
fill_or_kill: false,
expiration: t,
}),
Operation::LimitOrderCancel(LimitOrderCancel {
owner: "a".into(),
orderid: 1,
}),
Operation::FeedPublish(FeedPublish {
publisher: "a".into(),
exchange_rate: price.clone(),
}),
Operation::Convert(Convert {
owner: "a".into(),
requestid: 1,
amount: hive("1.000 HBD"),
}),
Operation::AccountWitnessVote(AccountWitnessVote {
account: "a".into(),
witness: "w".into(),
approve: true,
}),
Operation::AccountWitnessProxy(AccountWitnessProxy {
account: "a".into(),
proxy: "p".into(),
}),
Operation::Custom(Custom {
required_auths: vec!["a".into()],
id: 1,
data: HexBytes(vec![1, 2, 3]),
}),
Operation::DeleteComment(DeleteComment {
author: "a".into(),
permlink: "p".into(),
}),
Operation::CustomJson(CustomJson {
required_auths: vec![],
required_posting_auths: vec!["a".into()],
id: "x".into(),
json: "{}".into(),
}),
Operation::CommentOptions(CommentOptions {
author: "a".into(),
permlink: "p".into(),
max_accepted_payout: hive("1.000 HBD"),
percent_hbd: 10_000,
allow_votes: true,
allow_curation_rewards: true,
extensions: vec![],
}),
Operation::SetWithdrawVestingRoute(SetWithdrawVestingRoute {
from_account: "a".into(),
to_account: "b".into(),
percent: 100,
auto_vest: false,
}),
Operation::LimitOrderCreate2(LimitOrderCreate2 {
owner: "a".into(),
orderid: 1,
amount_to_sell: hive("1.000 HIVE"),
fill_or_kill: false,
exchange_rate: price,
expiration: t,
}),
Operation::ClaimAccount(ClaimAccount {
creator: "a".into(),
fee: hive("0.000 HIVE"),
extensions: NoExtensions,
}),
Operation::CreateClaimedAccount(CreateClaimedAccount {
creator: "a".into(),
new_account_name: "b".into(),
owner: auth.clone(),
active: auth.clone(),
posting: auth.clone(),
memo_key: key,
json_metadata: "{}".into(),
extensions: NoExtensions,
}),
Operation::ChangeRecoveryAccount(ChangeRecoveryAccount {
account_to_recover: "a".into(),
new_recovery_account: "b".into(),
extensions: NoExtensions,
}),
Operation::TransferToSavings(TransferToSavings {
from: "a".into(),
to: "b".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
}),
Operation::TransferFromSavings(TransferFromSavings {
from: "a".into(),
request_id: 1,
to: "b".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
}),
Operation::CancelTransferFromSavings(CancelTransferFromSavings {
from: "a".into(),
request_id: 1,
}),
Operation::DeclineVotingRights(DeclineVotingRights {
account: "a".into(),
decline: true,
}),
Operation::ClaimRewardBalance(ClaimRewardBalance {
account: "a".into(),
reward_hive: hive("0.000 HIVE"),
reward_hbd: hive("0.000 HBD"),
reward_vests: hive("0.000000 VESTS"),
}),
Operation::DelegateVestingShares(DelegateVestingShares {
delegator: "a".into(),
delegatee: "b".into(),
vesting_shares: hive("1.000000 VESTS"),
}),
Operation::AccountUpdate2(AccountUpdate2 {
account: "a".into(),
owner: None,
active: None,
posting: None,
memo_key: None,
json_metadata: "{}".into(),
posting_json_metadata: "{}".into(),
extensions: NoExtensions,
}),
Operation::CreateProposal(CreateProposal {
creator: "a".into(),
receiver: "b".into(),
start_date: t,
end_date: t,
daily_pay: hive("1.000 HBD"),
subject: "s".into(),
permlink: "p".into(),
extensions: NoExtensions,
}),
Operation::UpdateProposalVotes(UpdateProposalVotes {
voter: "a".into(),
proposal_ids: vec![1],
approve: true,
extensions: NoExtensions,
}),
Operation::RemoveProposal(RemoveProposal {
proposal_owner: "a".into(),
proposal_ids: vec![1],
extensions: NoExtensions,
}),
Operation::UpdateProposal(UpdateProposal {
proposal_id: 1,
creator: "a".into(),
daily_pay: hive("1.000 HBD"),
subject: "s".into(),
permlink: "p".into(),
extensions: NoExtensions,
}),
Operation::CollateralizedConvert(CollateralizedConvert {
owner: "a".into(),
requestid: 1,
amount: hive("1.000 HIVE"),
}),
Operation::RecurrentTransfer(RecurrentTransfer {
from: "a".into(),
to: "b".into(),
amount: hive("1.000 HIVE"),
memo: String::new(),
recurrence: 24,
executions: 2,
extensions: vec![],
}),
]
}
}