From c7bf123e02be717731a474d8ddffff53a158d930 Mon Sep 17 00:00:00 2001 From: Kevin Wellenzohn Date: Sun, 7 Jan 2024 08:55:01 +0100 Subject: [PATCH 1/2] Add box storage support for transaction signing --- algonaut_model/src/transaction.rs | 12 ++ algonaut_transaction/src/api_model.rs | 200 ++++++++++++++++++++++-- algonaut_transaction/src/builder.rs | 60 ++++++- algonaut_transaction/src/transaction.rs | 12 ++ src/atomic_transaction_composer/mod.rs | 6 +- src/util/dryrun_printer.rs | 7 +- tests/step_defs/integration/abi.rs | 8 +- 7 files changed, 286 insertions(+), 19 deletions(-) diff --git a/algonaut_model/src/transaction.rs b/algonaut_model/src/transaction.rs index 2bdda516..1a9965f2 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 061831a5..b425de9d 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,16 @@ 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 = api_box_references_from_box_references( + call.boxes.as_ref(), + call.foreign_apps.as_ref(), + call.app_id, + )? + .and_then(vec_as_api_option); } TransactionType::StateProofTransaction(_) => todo!(), } - api_t + Ok(api_t) } } @@ -198,6 +208,14 @@ impl TryFrom for Transaction { .app_arguments .map(|args| args.into_iter().map(|a| a.0).collect()), clear_state_program: api_t.clear_state_program.map(CompiledTeal), + + boxes: box_references_from_api_box_references( + api_t.boxes.as_ref(), + api_t.foreign_apps.as_ref(), + api_t.app_id, + )? + .and_then(vec_as_api_option), + foreign_apps: api_t.foreign_apps, foreign_assets: api_t.foreign_assets, @@ -386,21 +404,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 +458,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 +483,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,6 +593,99 @@ fn int_to_application_call_on_complete( } } +fn api_box_references_from_box_references( + boxes: Option<&Vec>, + foreign_apps: Option<&Vec>, + current_app_id: Option, +) -> Result>, TransactionError> { + boxes + .map(|boxes_| { + boxes_ + .iter() + .map(|box_| { + api_box_reference_from_box_references(box_, foreign_apps, current_app_id) + }) + .collect::, TransactionError>>() + }) + .map_or(Ok(None), |v| v.map(Some)) +} + +fn api_box_reference_from_box_references( + box_: &BoxReference, + foreign_apps: Option<&Vec>, + current_app_id: Option, +) -> Result { + let mut index = None; + if let Some(app_id) = box_.app_id { + if Some(app_id) == 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 = 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: box_.name.clone(), + }) +} + +fn box_references_from_api_box_references( + api_boxes: Option<&Vec>, + foreign_apps: Option<&Vec>, + current_app_id: Option, +) -> Result>, TransactionError> { + api_boxes + .map(|boxes| { + boxes + .iter() + .map(|api_box| { + box_reference_from_api_box_references(api_box, foreign_apps, current_app_id) + }) + .collect::, TransactionError>>() + }) + .map_or(Ok(None), |v| v.map(Some)) +} + +fn box_reference_from_api_box_references( + api_box: &ApiBoxReference, + foreign_apps: Option<&Vec>, + current_app_id: Option, +) -> Result { + let app_id = match api_box.index { + Some(index) => { + if index == 0 { + current_app_id + } else { + foreign_apps + .and_then(|fa| fa.get((index - 1) as usize)) + .copied() + } + } + None => None, + }; + Ok(BoxReference { + app_id, + name: api_box.name.clone(), + }) +} + #[cfg(test)] mod tests { use super::*; @@ -614,4 +733,57 @@ 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 current_app_id = 6355; + let foreign_apps = 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 = api_box_references_from_box_references( + Some(&boxes), + Some(&foreign_apps), + Some(current_app_id), + ) + .expect("api box references can 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 current_app_id = 6355; + let foreign_apps = vec![8577, 7466]; + let boxes = vec![BoxReference { + app_id: Some(1234), + name: box_name.clone(), + }]; + + assert!(api_box_references_from_box_references( + Some(&boxes), + Some(&foreign_apps), + Some(current_app_id), + ) + .is_err()); + } } diff --git a/algonaut_transaction/src/builder.rs b/algonaut_transaction/src/builder.rs index 545ed22d..488bdc3d 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 0868e841..70a6b2b1 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 6564ef18..b08810dc 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 9ef7cced..ec218f0e 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 ef2196bb..79b0d377 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()); From b44f3d7be3e471a851b5313287388188754ca310 Mon Sep 17 00:00:00 2001 From: Kevin Wellenzohn Date: Thu, 25 Jan 2024 17:21:34 +0100 Subject: [PATCH 2/2] Rusty conversion between BoxReference and ApiBoxReference This applies suggestions by @manuelmauro. --- algonaut_transaction/src/api_model.rs | 313 ++++++++++++++++---------- 1 file changed, 194 insertions(+), 119 deletions(-) diff --git a/algonaut_transaction/src/api_model.rs b/algonaut_transaction/src/api_model.rs index b425de9d..a9210f69 100644 --- a/algonaut_transaction/src/api_model.rs +++ b/algonaut_transaction/src/api_model.rs @@ -141,11 +141,10 @@ impl TryFrom 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 = api_box_references_from_box_references( - call.boxes.as_ref(), - call.foreign_apps.as_ref(), - call.app_id, - )? + api_t.boxes = TryInto::>>::try_into(BoxesInfo { + boxes: call.boxes.as_ref(), + call_metadata: call, + })? .and_then(vec_as_api_option); } TransactionType::StateProofTransaction(_) => todo!(), @@ -199,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(), @@ -208,17 +212,8 @@ impl TryFrom for Transaction { .app_arguments .map(|args| args.into_iter().map(|a| a.0).collect()), clear_state_program: api_t.clear_state_program.map(CompiledTeal), - - boxes: box_references_from_api_box_references( - api_t.boxes.as_ref(), - api_t.foreign_apps.as_ref(), - api_t.app_id, - )? - .and_then(vec_as_api_option), - 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, @@ -229,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), }) } @@ -593,103 +587,178 @@ fn int_to_application_call_on_complete( } } -fn api_box_references_from_box_references( - boxes: Option<&Vec>, - foreign_apps: Option<&Vec>, - current_app_id: Option, -) -> Result>, TransactionError> { - boxes - .map(|boxes_| { - boxes_ - .iter() - .map(|box_| { - api_box_reference_from_box_references(box_, foreign_apps, current_app_id) - }) - .collect::, TransactionError>>() - }) - .map_or(Ok(None), |v| v.map(Some)) -} - -fn api_box_reference_from_box_references( - box_: &BoxReference, - foreign_apps: Option<&Vec>, - current_app_id: Option, -) -> Result { - let mut index = None; - if let Some(app_id) = box_.app_id { - if Some(app_id) == 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 = 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); - } +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 } - Ok(ApiBoxReference { - index, - name: box_.name.clone(), - }) -} - -fn box_references_from_api_box_references( - api_boxes: Option<&Vec>, - foreign_apps: Option<&Vec>, - current_app_id: Option, -) -> Result>, TransactionError> { - api_boxes - .map(|boxes| { - boxes - .iter() - .map(|api_box| { - box_reference_from_api_box_references(api_box, foreign_apps, current_app_id) - }) - .collect::, TransactionError>>() - }) - .map_or(Ok(None), |v| v.map(Some)) -} - -fn box_reference_from_api_box_references( - api_box: &ApiBoxReference, - foreign_apps: Option<&Vec>, - current_app_id: Option, -) -> Result { - let app_id = match api_box.index { - Some(index) => { - if index == 0 { - current_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 { - foreign_apps - .and_then(|fa| fa.get((index - 1) as usize)) - .copied() + 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); } } - None => None, - }; - Ok(BoxReference { - app_id, - name: api_box.name.clone(), - }) + 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![ @@ -737,8 +806,11 @@ mod tests { #[test] fn test_api_box_references_from_box_references() { let box_name = vec![1, 2, 3, 4]; - let current_app_id = 6355; - let foreign_apps = vec![8577, 7466]; + let dummy_app_call = DummyAppCall { + app_id: Some(6355), + foreign_apps: Some(vec![8577, 7466]), + }; + let boxes = vec![ BoxReference { app_id: Some(6355), @@ -754,12 +826,11 @@ mod tests { }, ]; - let api_boxes = api_box_references_from_box_references( - Some(&boxes), - Some(&foreign_apps), - Some(current_app_id), - ) - .expect("api box references can be created"); + 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(); @@ -772,18 +843,22 @@ mod tests { #[test] fn test_api_box_references_from_box_references_invalid_reference() { let box_name = vec![1, 2, 3, 4]; - let current_app_id = 6355; - let foreign_apps = vec![8577, 7466]; + + 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!(api_box_references_from_box_references( - Some(&boxes), - Some(&foreign_apps), - Some(current_app_id), - ) - .is_err()); + assert!( + TryInto::>>::try_into(BoxesInfo { + boxes: Some(&boxes), + call_metadata: &dummy_app_call, + }) + .is_err() + ); } -} +} \ No newline at end of file