diff --git a/algonaut_model/src/transaction.rs b/algonaut_model/src/transaction.rs index 2bdda51..1a9965f 100644 --- a/algonaut_model/src/transaction.rs +++ b/algonaut_model/src/transaction.rs @@ -54,6 +54,9 @@ pub struct ApiTransaction { #[serde(rename = "apat", skip_serializing_if = "Option::is_none")] pub accounts: Option>, + #[serde(rename = "apbx", skip_serializing_if = "Option::is_none")] + pub boxes: Option>, + #[serde(rename = "apep", skip_serializing_if = "Option::is_none")] pub extra_pages: Option, @@ -251,6 +254,15 @@ pub struct ApiStateSchema { pub number_ints: Option, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiBoxReference { + #[serde(rename = "i", skip_serializing_if = "Option::is_none")] + pub index: Option, + + #[serde(rename = "n", with = "serde_bytes")] + pub name: Vec, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct StateProof { #[serde(rename = "c")] diff --git a/algonaut_transaction/src/api_model.rs b/algonaut_transaction/src/api_model.rs index 061831a..a9210f6 100644 --- a/algonaut_transaction/src/api_model.rs +++ b/algonaut_transaction/src/api_model.rs @@ -5,22 +5,25 @@ use crate::{ transaction::{ to_tx_type_enum, ApplicationCallOnComplete, ApplicationCallTransaction, AssetAcceptTransaction, AssetClawbackTransaction, AssetConfigurationTransaction, - AssetFreezeTransaction, AssetParams, AssetTransferTransaction, KeyRegistration, Payment, - SignedLogic, StateProofTransaction, StateSchema, TransactionSignature, + AssetFreezeTransaction, AssetParams, AssetTransferTransaction, BoxReference, + KeyRegistration, Payment, SignedLogic, StateProofTransaction, StateSchema, + TransactionSignature, }, tx_group::TxGroup, SignedTransaction, Transaction, TransactionType, }; use algonaut_core::{CompiledTeal, LogicSignature, MicroAlgos, Round, ToMsgPack}; use algonaut_model::transaction::{ - ApiAssetParams, ApiSignedLogic, ApiSignedLogicArg, ApiSignedTransaction, ApiStateSchema, - ApiTransaction, AppArgument, + ApiAssetParams, ApiBoxReference, ApiSignedLogic, ApiSignedLogicArg, ApiSignedTransaction, + ApiStateSchema, ApiTransaction, AppArgument, }; use num_traits::Num; use serde::{Deserialize, Serialize}; -impl From for ApiTransaction { - fn from(t: Transaction) -> Self { +impl TryFrom for ApiTransaction { + type Error = TransactionError; + + fn try_from(t: Transaction) -> Result { let mut api_t = ApiTransaction { // Common fields fee: num_as_api_option(t.fee.0).map(MicroAlgos), @@ -64,6 +67,7 @@ impl From for ApiTransaction { vote_last: None, xfer: None, nonparticipating: None, + boxes: None, extra_pages: None, state_proof_type: None, state_proof: None, @@ -137,10 +141,15 @@ impl From for ApiTransaction { api_t.local_state_schema = call.to_owned().local_state_schema.and_then(|s| s.into()); api_t.extra_pages = num_as_api_option(call.extra_pages); + api_t.boxes = TryInto::>>::try_into(BoxesInfo { + boxes: call.boxes.as_ref(), + call_metadata: call, + })? + .and_then(vec_as_api_option); } TransactionType::StateProofTransaction(_) => todo!(), } - api_t + Ok(api_t) } } @@ -189,6 +198,11 @@ impl TryFrom for Transaction { let on_complete = int_to_application_call_on_complete(num_from_api_option(api_t.on_complete))?; TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { + boxes: TryInto::>>::try_into(ApiBoxesInfo { + boxes: api_t.boxes.as_ref(), + call_metadata: &api_t, + })? + .and_then(vec_as_api_option), sender: api_t.sender, app_id: api_t.app_id, on_complete: on_complete.clone(), @@ -200,7 +214,6 @@ impl TryFrom for Transaction { clear_state_program: api_t.clear_state_program.map(CompiledTeal), foreign_apps: api_t.foreign_apps, foreign_assets: api_t.foreign_assets, - global_state_schema: parse_state_schema( on_complete.clone(), api_t.app_id, @@ -211,7 +224,6 @@ impl TryFrom for Transaction { api_t.app_id, api_t.local_state_schema, ), - extra_pages: num_from_api_option(api_t.extra_pages), }) } @@ -386,21 +398,23 @@ impl From for AssetParams { } } -impl From for ApiSignedTransaction { - fn from(t: SignedTransaction) -> Self { +impl TryFrom for ApiSignedTransaction { + type Error = TransactionError; + + fn try_from(t: SignedTransaction) -> Result { let (sig, msig, lsig) = match t.sig { TransactionSignature::Single(sig) => (Some(sig), None, None), TransactionSignature::Multi(msig) => (None, Some(msig), None), TransactionSignature::Logic(lsig) => (None, None, Some(lsig)), }; - ApiSignedTransaction { + Ok(ApiSignedTransaction { sig, msig, lsig: lsig.map(|l| l.into()), - transaction: t.transaction.into(), + transaction: t.transaction.try_into()?, transaction_id: t.transaction_id, auth_address: t.auth_address, - } + }) } } @@ -438,7 +452,10 @@ impl Serialize for Transaction { where S: serde::Serializer, { - let api_transaction: ApiTransaction = self.to_owned().into(); + let api_transaction: ApiTransaction = self + .to_owned() + .try_into() + .map_err(|_| serde::ser::Error::custom("Serialization error for Transaction"))?; api_transaction.serialize(serializer) } } @@ -460,7 +477,10 @@ impl Serialize for SignedTransaction { where S: serde::Serializer, { - let api_transaction: ApiSignedTransaction = self.to_owned().into(); + let api_transaction: ApiSignedTransaction = self + .to_owned() + .try_into() + .map_err(|_| serde::ser::Error::custom("Serialization error for SignedTransaction"))?; api_transaction.serialize(serializer) } } @@ -567,10 +587,178 @@ fn int_to_application_call_on_complete( } } +trait AppCallMetadata { + fn current_app_id(&self) -> Option; + fn foreign_apps(&self) -> Option<&Vec>; +} + +impl AppCallMetadata for &ApplicationCallTransaction { + fn current_app_id(&self) -> Option { + self.app_id + } + + fn foreign_apps(&self) -> Option<&Vec> { + self.foreign_apps.as_ref() + } +} + +impl AppCallMetadata for &ApiTransaction { + fn current_app_id(&self) -> Option { + self.app_id + } + + fn foreign_apps(&self) -> Option<&Vec> { + self.foreign_apps.as_ref() + } +} + +struct BoxInfo { + box_: BoxReference, + call_metadata: T, +} + +impl<'a, T> TryFrom> for ApiBoxReference +where + &'a T: AppCallMetadata, +{ + type Error = TransactionError; + + fn try_from(info: BoxInfo<&'a T>) -> Result { + let mut index = None; + if let Some(app_id) = info.box_.app_id { + if Some(app_id) == info.call_metadata.current_app_id() { + // the current app ID is implicitly at index 0 in the + // foreign apps array + index = num_as_api_option(0); + } else { + let mut idx = 0; + if app_id > 0 { + let pos = info + .call_metadata + .foreign_apps() + .as_ref() + .and_then(|fa| fa.iter().position(|&id| id == app_id)) + .ok_or_else(|| { + TransactionError::Msg(format!( + "app_id {} not found in foreign apps array", + app_id + )) + })?; + // the foreign apps array starts at index 1 since index 0 is + // always occupied by the current app ID + idx = pos + 1; + } + index = num_as_api_option(idx as u64); + } + } + Ok(ApiBoxReference { + index, + name: info.box_.name.clone(), + }) + } +} + +struct BoxesInfo<'a, T: AppCallMetadata> { + boxes: Option<&'a Vec>, + call_metadata: T, +} + +impl<'a, 'b, T> TryFrom> for Option> +where + &'b T: AppCallMetadata, +{ + type Error = TransactionError; + + fn try_from(info: BoxesInfo<&'b T>) -> Result { + info.boxes + .map(|boxes| { + boxes + .iter() + .map(|box_| { + TryInto::::try_into(BoxInfo { + box_: box_.clone(), + call_metadata: info.call_metadata, + }) + }) + .collect::, TransactionError>>() + }) + .map_or(Ok(None), |v| v.map(Some)) + } +} + +struct ApiBoxInfo { + box_: ApiBoxReference, + call_metadata: T, +} + +impl TryFrom> for BoxReference { + type Error = TransactionError; + + fn try_from(info: ApiBoxInfo<&ApiTransaction>) -> Result { + let app_id = match info.box_.index { + Some(index) => { + if index == 0 { + info.call_metadata.current_app_id() + } else { + info.call_metadata + .foreign_apps() + .and_then(|fa| fa.get((index - 1) as usize)) + .copied() + } + } + None => None, + }; + Ok(BoxReference { + app_id, + name: info.box_.name.clone(), + }) + } +} + +struct ApiBoxesInfo<'a, T: AppCallMetadata> { + boxes: Option<&'a Vec>, + call_metadata: T, +} + +impl<'a> TryFrom> for Option> { + type Error = TransactionError; + + fn try_from(info: ApiBoxesInfo<&ApiTransaction>) -> Result { + info.boxes + .map(|boxes| { + boxes + .iter() + .map(|api_box| { + TryInto::::try_into(ApiBoxInfo { + box_: api_box.clone(), + call_metadata: info.call_metadata, + }) + }) + .collect::, TransactionError>>() + }) + .map_or(Ok(None), |v| v.map(Some)) + } +} + #[cfg(test)] mod tests { use super::*; + struct DummyAppCall { + app_id: Option, + foreign_apps: Option>, + } + + impl AppCallMetadata for &DummyAppCall { + fn current_app_id(&self) -> Option { + self.app_id + } + + fn foreign_apps(&self) -> Option<&Vec> { + self.foreign_apps.as_ref() + } + } + #[test] fn test_serialize_signed_logic_contract_account() { let program = CompiledTeal(vec![ @@ -614,4 +802,63 @@ mod tests { assert_eq!(lsig, lsig_deserialized); } -} + + #[test] + fn test_api_box_references_from_box_references() { + let box_name = vec![1, 2, 3, 4]; + let dummy_app_call = DummyAppCall { + app_id: Some(6355), + foreign_apps: Some(vec![8577, 7466]), + }; + + let boxes = vec![ + BoxReference { + app_id: Some(6355), + name: box_name.clone(), + }, + BoxReference { + app_id: Some(7466), + name: box_name.clone(), + }, + BoxReference { + app_id: Some(8577), + name: box_name.clone(), + }, + ]; + + let api_boxes = TryInto::>>::try_into(BoxesInfo { + boxes: Some(&boxes), + call_metadata: &dummy_app_call, + }) + .expect("api box references should be created"); + + assert!(api_boxes.is_some()); + let api_boxes = api_boxes.unwrap(); + + assert_eq!(None, api_boxes[0].index); + assert_eq!(Some(2), api_boxes[1].index); + assert_eq!(Some(1), api_boxes[2].index); + } + + #[test] + fn test_api_box_references_from_box_references_invalid_reference() { + let box_name = vec![1, 2, 3, 4]; + + let dummy_app_call = DummyAppCall { + app_id: Some(6355), + foreign_apps: Some(vec![8577, 7466]), + }; + let boxes = vec![BoxReference { + app_id: Some(1234), + name: box_name.clone(), + }]; + + assert!( + TryInto::>>::try_into(BoxesInfo { + boxes: Some(&boxes), + call_metadata: &dummy_app_call, + }) + .is_err() + ); + } +} \ No newline at end of file diff --git a/algonaut_transaction/src/builder.rs b/algonaut_transaction/src/builder.rs index 545ed22..488bdc3 100644 --- a/algonaut_transaction/src/builder.rs +++ b/algonaut_transaction/src/builder.rs @@ -3,8 +3,8 @@ use crate::{ transaction::{ ApplicationCallOnComplete, ApplicationCallTransaction, AssetAcceptTransaction, AssetClawbackTransaction, AssetConfigurationTransaction, AssetFreezeTransaction, - AssetParams, AssetTransferTransaction, KeyRegistration, Payment, StateSchema, Transaction, - TransactionType, + AssetParams, AssetTransferTransaction, BoxReference, KeyRegistration, Payment, StateSchema, + Transaction, TransactionType, }, }; use algonaut_core::{Address, CompiledTeal, MicroAlgos, Round, VotePk, VrfPk}; @@ -591,6 +591,7 @@ pub struct CreateApplication { global_state_schema: Option, local_state_schema: Option, extra_pages: u32, + boxes: Option>, } impl CreateApplication { @@ -612,6 +613,7 @@ impl CreateApplication { global_state_schema: Some(global_state_schema), local_state_schema: Some(local_state_schema), extra_pages: 0, + boxes: None, } } @@ -640,6 +642,11 @@ impl CreateApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -654,6 +661,7 @@ impl CreateApplication { global_state_schema: self.global_state_schema, local_state_schema: self.local_state_schema, extra_pages: self.extra_pages, + boxes: self.boxes, }) } } @@ -668,6 +676,7 @@ pub struct UpdateApplication { clear_state_program: Option, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl UpdateApplication { @@ -686,6 +695,7 @@ impl UpdateApplication { clear_state_program: Some(clear_state_program), foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -709,6 +719,11 @@ impl UpdateApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -723,6 +738,7 @@ impl UpdateApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } @@ -735,6 +751,7 @@ pub struct CallApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl CallApplication { @@ -746,6 +763,7 @@ impl CallApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -769,6 +787,11 @@ impl CallApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -783,6 +806,7 @@ impl CallApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } @@ -795,6 +819,7 @@ pub struct ClearApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl ClearApplication { @@ -806,6 +831,7 @@ impl ClearApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -829,6 +855,11 @@ impl ClearApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -843,6 +874,7 @@ impl ClearApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } @@ -855,6 +887,7 @@ pub struct CloseApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl CloseApplication { @@ -866,6 +899,7 @@ impl CloseApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -889,6 +923,11 @@ impl CloseApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -903,6 +942,7 @@ impl CloseApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } @@ -915,6 +955,7 @@ pub struct DeleteApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl DeleteApplication { @@ -926,6 +967,7 @@ impl DeleteApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -949,6 +991,11 @@ impl DeleteApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -963,6 +1010,7 @@ impl DeleteApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } @@ -975,6 +1023,7 @@ pub struct OptInApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + boxes: Option>, } impl OptInApplication { @@ -986,6 +1035,7 @@ impl OptInApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + boxes: None, } } @@ -1009,6 +1059,11 @@ impl OptInApplication { self } + pub fn boxes(mut self, boxes: Vec) -> Self { + self.boxes = Some(boxes); + self + } + pub fn build(self) -> TransactionType { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, @@ -1023,6 +1078,7 @@ impl OptInApplication { global_state_schema: None, local_state_schema: None, extra_pages: 0, + boxes: self.boxes, }) } } diff --git a/algonaut_transaction/src/transaction.rs b/algonaut_transaction/src/transaction.rs index 0868e84..70a6b2b 100644 --- a/algonaut_transaction/src/transaction.rs +++ b/algonaut_transaction/src/transaction.rs @@ -371,6 +371,18 @@ pub struct ApplicationCallTransaction { // Number of additional pages allocated to the application's approval and clear state programs. Each ExtraProgramPages is 2048 bytes. The sum of ApprovalProgram and ClearStateProgram may not exceed 2048*(1+ExtraProgramPages) bytes. pub extra_pages: u32, + + // Lists all boxes that the application may access + pub boxes: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BoxReference { + /// The ID of the application that this box belongs to + pub app_id: Option, + + /// The name of the box as bytes + pub name: Vec, } /// diff --git a/src/atomic_transaction_composer/mod.rs b/src/atomic_transaction_composer/mod.rs index 6564ef1..b08810d 100644 --- a/src/atomic_transaction_composer/mod.rs +++ b/src/atomic_transaction_composer/mod.rs @@ -14,7 +14,8 @@ use algonaut_crypto::HashDigest; use algonaut_transaction::{ error::TransactionError, transaction::{ - to_tx_type_enum, ApplicationCallOnComplete, ApplicationCallTransaction, StateSchema, + to_tx_type_enum, ApplicationCallOnComplete, ApplicationCallTransaction, BoxReference, + StateSchema, }, tx_group::TxGroup, SignedTransaction, Transaction, TransactionType, TxnBuilder, @@ -105,6 +106,8 @@ pub struct AddMethodCallParams { pub rekey_to: Option
, /// A transaction Signer that can authorize this application call from sender pub signer: TransactionSigner, + /// A list of boxes that the app call has access to + pub boxes: Option>, } #[derive(Debug, Clone)] @@ -325,6 +328,7 @@ impl AtomicTransactionComposer { global_state_schema: params.global_schema.clone(), local_state_schema: params.local_schema.clone(), extra_pages: params.extra_pages, + boxes: params.boxes.clone(), }); let mut tx_builder = TxnBuilder::with_fee(¶ms.suggested_params, params.fee, app_call); diff --git a/src/util/dryrun_printer.rs b/src/util/dryrun_printer.rs index 9ef7cce..ec218f0 100644 --- a/src/util/dryrun_printer.rs +++ b/src/util/dryrun_printer.rs @@ -83,6 +83,11 @@ pub async fn create_dryrun_with_settings( acct_infos.push(acc); } + let mut txns = Vec::new(); + for signed_txn in signed_txs { + txns.push(signed_txn.clone().try_into()?); + } + Ok(DryrunRequest { accounts: acct_infos, apps: app_infos, @@ -90,7 +95,7 @@ pub async fn create_dryrun_with_settings( protocol_version: protocol_version.to_owned(), round, sources: vec![], - txns: signed_txs.iter().map(|t| t.clone().into()).collect(), + txns, }) } diff --git a/tests/step_defs/integration/abi.rs b/tests/step_defs/integration/abi.rs index ef2196b..79b0d37 100644 --- a/tests/step_defs/integration/abi.rs +++ b/tests/step_defs/integration/abi.rs @@ -14,7 +14,7 @@ use algonaut_abi::{ use algonaut_algod::models::PendingTransactionResponse; use algonaut_core::{to_app_address, Address, MicroAlgos}; use algonaut_transaction::{ - transaction::{ApplicationCallOnComplete, StateSchema}, + transaction::{ApplicationCallOnComplete, BoxReference, StateSchema}, Pay, TxnBuilder, }; use cucumber::{codegen::Regex, given, then, when}; @@ -230,6 +230,7 @@ async fn i_add_a_method_call(w: &mut World, account_type: String, on_complete: S None, None, false, + None, ) .await; } @@ -256,6 +257,7 @@ async fn i_add_a_method_call_for_update( None, None, false, + None, ) .await; } @@ -287,6 +289,7 @@ async fn i_add_a_method_call_for_create( Some(local_ints), Some(extra_pages), false, + None, ) .await; } @@ -307,6 +310,7 @@ async fn i_add_method_call_with_nonce(w: &mut World, account_type: String, on_co None, None, true, + None, ) .await; } @@ -323,6 +327,7 @@ async fn add_method_call( local_ints: Option, extra_pages: Option, use_nonce: bool, + boxes: Option>, ) { let algod = w.algod.as_ref().unwrap(); let transient_account = w.transient_account.clone().unwrap(); @@ -414,6 +419,7 @@ async fn add_method_call( lease: None, rekey_to: None, signer: tx_signer, + boxes, }; tx_composer_methods.push(abi_method.to_owned());