Skip to content

Commit 477a73c

Browse files
authored
feat: app manager; asset manager; creator and sender abstractions (#229)
feat: app manager; asset manager; creator and sender abstractions This commit introduces comprehensive high-level abstractions for Algorand application and asset management, along with ergonomic transaction creation interfaces that significantly improve developer experience while maintaining full protocol capability. ## Core Features Added ### Application Manager (`app_manager.rs`) - Complete application lifecycle management (create, update, delete, call) - ABI method integration with automatic encoding/decoding - Box storage operations with binary key support and ABI type decoding - TEAL compilation with SHA256-based caching for performance - Application state retrieval with binary key handling for cross-language consistency ### Asset Manager (`asset_manager.rs`) - Asset creation, configuration, and lifecycle management - Bulk opt-in/opt-out operations for efficient batch processing - Asset information retrieval with flattened ergonomic interface - Account asset information access with proper type handling ### Transaction Abstractions - **Creator** (`creator.rs`): Fluent transaction building without automatic sending - **Sender** (`sender.rs`): Direct transaction creation and execution - **Sender Results** (`sender_results.rs`): Rich result types with transaction metadata - Unified error handling and validation across all transaction types ## API Client Enhancements ### Generated Client Improvements - Vendor extension system for field customization (application-index → app_id, asset-index → asset_id) - Proper binary data handling with base64 deserialization for TealValue.bytes - Consistent field naming across algod and indexer clients - Generator-level fixes to avoid manual modification of auto-generated code ### AlgorandClient Integration - Memory-efficient Arc<AlgodClient> sharing to eliminate unnecessary cloning - Integrated manager instances for streamlined access patterns - Comprehensive test utilities with ergonomic Arc-based fixtures ## Developer Experience Improvements ### Type Safety & Ergonomics - AssetInformation with flattened field access (asset.total vs asset.params.total) - Optional close_remainder_to in asset opt-out with automatic creator resolution - Consistent app_id/asset_id naming throughout the API surface - Rich transaction result types with compilation metadata ### Cross-Language Consistency - Binary key handling in application state for TypeScript/Python alignment - Vec<u8> box identifiers for proper non-UTF-8 support - Standardized ABI return handling across all operations - Consistent validation patterns between creator and sender abstractions ## Technical Architecture ### Performance Optimizations - TEAL compilation caching with SHA256 keys for large programs - Arc-based client sharing to minimize memory overhead - Bulk operations using TransactionComposer for efficient execution - Early validation with comprehensive error propagation ### Code Organization - Clean separation of concerns between managers, creators, and senders - Leveraged existing algokit_transact validation instead of duplication - Modular design enabling incremental adoption - Comprehensive test coverage with deterministic validation ## Breaking Changes - AssetManager.get_by_id() returns AssetInformation instead of raw algod types - AssetOptOutParams.creator renamed to close_remainder_to for clarity - Some method names updated for consistency (_method_call suffix for ABI methods) ## Migration Benefits This foundation enables the upcoming AlgoKit App Client while providing immediate value for direct usage. The abstractions eliminate boilerplate while preserving full protocol access, supporting both simple operations and complex multi-transaction workflows. Addresses 45+ PR review comments with architectural improvements, performance optimizations, and API refinements based on extensive feedback integration.
1 parent 45577c0 commit 477a73c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+5224
-137
lines changed

api/oas_generator/rust_oas_generator/parser/oas_parser.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,17 @@ class Property:
380380
is_signed_transaction: bool = field(init=False)
381381

382382
def __post_init__(self) -> None:
383-
self.rust_name = rust_snake_case(self.name)
383+
# Check for field name override from vendor extension
384+
field_name_override = self._get_field_name_override()
385+
field_name = field_name_override if field_name_override else self.name
386+
387+
self.rust_name = rust_snake_case(field_name)
384388
self.rust_field_name = escape_rust_keyword(self.rust_name)
389+
390+
# Check for bytes base64 override from vendor extension
391+
if self._has_bytes_base64_extension():
392+
self.is_base64_encoded = True
393+
385394
if self.is_base64_encoded:
386395
self.rust_type_with_msgpack = "Vec<u8>"
387396
elif self.items and self.items.is_base64_encoded and self.rust_type.startswith("Vec<"):
@@ -399,6 +408,20 @@ def __post_init__(self) -> None:
399408
"x-algokit-signed-txn" in ext_name and ext_value for ext_name, ext_value in self.items.vendor_extensions
400409
)
401410

411+
def _get_field_name_override(self) -> str | None:
412+
"""Get field name override from vendor extension."""
413+
for ext_name, ext_value in self.vendor_extensions:
414+
if ext_name == "x-algokit-field-rename" and isinstance(ext_value, str):
415+
return ext_value
416+
return None
417+
418+
def _has_bytes_base64_extension(self) -> bool:
419+
"""Check if this property has the bytes base64 vendor extension."""
420+
for ext_name, ext_value in self.vendor_extensions:
421+
if ext_name == "x-algokit-bytes-base64" and ext_value is True:
422+
return True
423+
return False
424+
402425

403426
@dataclass
404427
class Schema:

api/scripts/convert-openapi.ts

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,90 @@ function transformVendorExtensions(spec: OpenAPISpec, transforms: VendorExtensio
183183
return transformCounts;
184184
}
185185

186+
/**
187+
* Fix field naming - Add field rename extensions for better Rust ergonomics
188+
*/
189+
function fixFieldNaming(spec: OpenAPISpec): number {
190+
let fixedCount = 0;
191+
192+
// Properties that should be renamed for better developer experience
193+
const fieldRenames = [
194+
{ from: "application-index", to: "app_id" },
195+
{ from: "asset-index", to: "asset_id" },
196+
];
197+
198+
const processObject = (obj: any): void => {
199+
if (!obj || typeof obj !== "object") return;
200+
201+
if (Array.isArray(obj)) {
202+
obj.forEach((o) => processObject(o));
203+
return;
204+
}
205+
206+
// Look for properties object in schemas
207+
if (obj.properties && typeof obj.properties === "object") {
208+
for (const [propName, propDef] of Object.entries(obj.properties as Record<string, any>)) {
209+
if (propDef && typeof propDef === "object") {
210+
const rename = fieldRenames.find(r => r.from === propName);
211+
if (rename) {
212+
propDef["x-algokit-field-rename"] = rename.to;
213+
fixedCount++;
214+
}
215+
}
216+
}
217+
}
218+
219+
// Recursively process nested objects
220+
for (const value of Object.values(obj)) {
221+
if (value && typeof value === "object") {
222+
processObject(value);
223+
}
224+
}
225+
};
226+
227+
processObject(spec);
228+
return fixedCount;
229+
}
230+
231+
/**
232+
* Fix TealValue bytes - Add base64 extension for TealValue.bytes fields
233+
*/
234+
function fixTealValueBytes(spec: OpenAPISpec): number {
235+
let fixedCount = 0;
236+
237+
const processObject = (obj: any, schemaName?: string): void => {
238+
if (!obj || typeof obj !== "object") return;
239+
240+
if (Array.isArray(obj)) {
241+
obj.forEach((o) => processObject(o));
242+
return;
243+
}
244+
245+
// Check if this is a TealValue schema with bytes property
246+
if (schemaName === "TealValue" && obj.properties && obj.properties.bytes) {
247+
obj.properties.bytes["x-algokit-bytes-base64"] = true;
248+
fixedCount++;
249+
}
250+
251+
// Recursively process schemas
252+
if (obj.schemas && typeof obj.schemas === "object") {
253+
for (const [name, schemaDef] of Object.entries(obj.schemas)) {
254+
processObject(schemaDef, name);
255+
}
256+
} else {
257+
// Recursively process other nested objects
258+
for (const [key, value] of Object.entries(obj)) {
259+
if (value && typeof value === "object") {
260+
processObject(value, key);
261+
}
262+
}
263+
}
264+
};
265+
266+
processObject(spec);
267+
return fixedCount;
268+
}
269+
186270
/**
187271
* Fix bigint - Add x-algokit-bigint: true to properties that represent large integers
188272
*/
@@ -370,11 +454,19 @@ class OpenAPIProcessor {
370454
const pydanticCount = fixPydanticRecursionError(spec);
371455
console.log(`ℹ️ Fixed ${pydanticCount} pydantic recursion errors`);
372456

373-
// 3. Fix bigint properties
374-
const bigIntCount = fixBigInt(spec);
375-
console.log(`ℹ️ Added x-algokit-bigint to ${bigIntCount} properties`);
457+
// 3. Fix field naming
458+
const fieldNamingCount = fixFieldNaming(spec);
459+
console.log(`ℹ️ Added field rename extensions to ${fieldNamingCount} properties`);
460+
461+
// 4. Fix TealValue bytes fields
462+
const tealValueCount = fixTealValueBytes(spec);
463+
console.log(`ℹ️ Added bytes base64 extensions to ${tealValueCount} TealValue.bytes properties`);
464+
465+
// 5. Fix bigint properties
466+
const bigIntCount = fixBigInt(spec);
467+
console.log(`ℹ️ Added x-algokit-bigint to ${bigIntCount} properties`);
376468

377-
// 4. Transform vendor extensions if configured
469+
// 6. Transform vendor extensions if configured
378470
if (this.config.vendorExtensionTransforms && this.config.vendorExtensionTransforms.length > 0) {
379471
const transformCounts = transformVendorExtensions(spec, this.config.vendorExtensionTransforms);
380472

api/specs/algod.oas3.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5632,7 +5632,8 @@
56325632
},
56335633
"bytes": {
56345634
"type": "string",
5635-
"description": "\\[tb\\] bytes value."
5635+
"description": "\\[tb\\] bytes value.",
5636+
"x-algokit-bytes-base64": true
56365637
},
56375638
"uint": {
56385639
"type": "integer",
@@ -6272,12 +6273,14 @@
62726273
"type": "integer",
62736274
"description": "The asset index if the transaction was found and it created an asset.",
62746275
"x-go-type": "basics.AssetIndex",
6276+
"x-algokit-field-rename": "asset_id",
62756277
"x-algokit-bigint": true
62766278
},
62776279
"application-index": {
62786280
"type": "integer",
62796281
"description": "The application index if the transaction was found and it created an application.",
62806282
"x-go-type": "basics.AppIndex",
6283+
"x-algokit-field-rename": "app_id",
62816284
"x-algokit-bigint": true
62826285
},
62836286
"close-rewards": {
@@ -6831,6 +6834,7 @@
68316834
"type": "integer",
68326835
"description": "The application from which the logs were generated",
68336836
"x-go-type": "basics.AppIndex",
6837+
"x-algokit-field-rename": "app_id",
68346838
"x-algokit-bigint": true
68356839
},
68366840
"txId": {

api/specs/indexer.oas3.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3400,7 +3400,8 @@
34003400
},
34013401
"bytes": {
34023402
"type": "string",
3403-
"description": "bytes value."
3403+
"description": "bytes value.",
3404+
"x-algokit-bytes-base64": true
34043405
},
34053406
"uint": {
34063407
"type": "integer",

crates/algod_client/src/models/app_call_logs.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ pub struct AppCallLogs {
2222
pub logs: Vec<Vec<u8>>,
2323
/// The application from which the logs were generated
2424
#[serde(rename = "application-index")]
25-
pub application_index: u64,
25+
pub app_id: u64,
2626
/// The transaction ID of the outer app call that lead to these logs
2727
#[serde(rename = "txId")]
2828
pub tx_id: String,
2929
}
3030

3131
impl AppCallLogs {
3232
/// Constructor for AppCallLogs
33-
pub fn new(logs: Vec<Vec<u8>>, application_index: u64, tx_id: String) -> AppCallLogs {
33+
pub fn new(logs: Vec<Vec<u8>>, app_id: u64, tx_id: String) -> AppCallLogs {
3434
AppCallLogs {
3535
logs,
36-
application_index,
36+
app_id,
3737
tx_id,
3838
}
3939
}

crates/algod_client/src/models/pending_transaction_response.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ use crate::models::StateDelta;
2222
pub struct PendingTransactionResponse {
2323
/// The asset index if the transaction was found and it created an asset.
2424
#[serde(rename = "asset-index", skip_serializing_if = "Option::is_none")]
25-
pub asset_index: Option<u64>,
25+
pub asset_id: Option<u64>,
2626
/// The application index if the transaction was found and it created an application.
2727
#[serde(rename = "application-index", skip_serializing_if = "Option::is_none")]
28-
pub application_index: Option<u64>,
28+
pub app_id: Option<u64>,
2929
/// Rewards in microalgos applied to the close remainder to account.
3030
#[serde(rename = "close-rewards", skip_serializing_if = "Option::is_none")]
3131
pub close_rewards: Option<u64>,
@@ -70,8 +70,8 @@ pub struct PendingTransactionResponse {
7070
impl Default for PendingTransactionResponse {
7171
fn default() -> Self {
7272
Self {
73-
asset_index: None,
74-
application_index: None,
73+
asset_id: None,
74+
app_id: None,
7575
close_rewards: None,
7676
closing_amount: None,
7777
asset_closing_amount: None,
@@ -121,8 +121,8 @@ impl PendingTransactionResponse {
121121
PendingTransactionResponse {
122122
pool_error,
123123
txn,
124-
asset_index: None,
125-
application_index: None,
124+
asset_id: None,
125+
app_id: None,
126126
close_rewards: None,
127127
closing_amount: None,
128128
asset_closing_amount: None,

crates/algod_client/src/models/teal_value.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@
1111
use crate::models;
1212
use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction};
1313
use serde::{Deserialize, Serialize};
14+
use serde_with::{Bytes, serde_as};
1415

1516
/// Represents a TEAL value.
17+
#[serde_as]
1618
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
1719
pub struct TealValue {
1820
/// \[tt\] value type. Value `1` refers to **bytes**, value `2` refers to **uint**
1921
#[serde(rename = "type")]
2022
pub r#type: u64,
2123
/// \[tb\] bytes value.
24+
#[serde_as(as = "serde_with::base64::Base64")]
2225
#[serde(rename = "bytes")]
23-
pub bytes: String,
26+
pub bytes: Vec<u8>,
2427
/// \[ui\] uint value.
2528
#[serde(rename = "uint")]
2629
pub uint: u64,
@@ -32,7 +35,7 @@ impl AlgorandMsgpack for TealValue {
3235

3336
impl TealValue {
3437
/// Constructor for TealValue
35-
pub fn new(r#type: u64, bytes: String, uint: u64) -> TealValue {
38+
pub fn new(r#type: u64, bytes: Vec<u8>, uint: u64) -> TealValue {
3639
TealValue {
3740
r#type,
3841
bytes,

crates/algokit_abi/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ pub use arc56_contract::*;
1414
pub use error::ABIError;
1515

1616
pub use method::{
17-
ABIMethod, ABIMethodArg, ABIMethodArgType, ABIReferenceType, ABIReferenceValue,
17+
ABIMethod, ABIMethodArg, ABIMethodArgType, ABIReferenceType, ABIReferenceValue, ABIReturn,
1818
ABITransactionType,
1919
};

crates/algokit_abi/src/method.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::abi_type::ABIType;
2+
use crate::abi_value::ABIValue;
23
use crate::error::ABIError;
34
use sha2::{Digest, Sha512_256};
45
use std::fmt::Display;
@@ -357,6 +358,17 @@ impl ABIMethodArg {
357358
}
358359
}
359360

361+
/// Represents an ABI method return value with parsed data.
362+
#[derive(Debug, Clone)]
363+
pub struct ABIReturn {
364+
/// The method that was called.
365+
pub method: ABIMethod,
366+
/// The raw return value as bytes.
367+
pub raw_return_value: Vec<u8>,
368+
/// The parsed ABI return value.
369+
pub return_value: ABIValue,
370+
}
371+
360372
/// Find the matching closing parenthesis for an opening parenthesis.
361373
fn find_matching_closing_paren(s: &str, open_pos: usize) -> Result<usize, ABIError> {
362374
let chars: Vec<char> = s.chars().collect();

crates/algokit_test_artifacts/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//!
55
//! This crate provides static access to various Algorand contract artifacts
66
//! used for testing purposes. Artifacts are organized by contract name in
7-
//! individual folders, following the Python algokit-utils convention.
7+
//! individual folders.
88
//!
99
//! Each contract has its own folder named after the contract, containing
1010
//! standardized file names like `application.arc56.json` or `application.json`.

0 commit comments

Comments
 (0)