Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/gem_algorand/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = { workspace = true }
[features]
default = []
rpc = ["dep:chrono", "dep:chain_traits", "dep:gem_client"]
signer = ["dep:gem_hash", "dep:hex", "dep:num-traits", "dep:signer"]
reqwest = ["gem_client/reqwest"]
chain_integration_tests = ["rpc", "reqwest", "settings/testkit"]

Expand All @@ -15,14 +16,20 @@ serde = { workspace = true }
serde_json = { workspace = true }
gem_encoding = { path = "../gem_encoding" }
num-bigint = { workspace = true }
num-traits = { workspace = true, optional = true }

chrono = { workspace = true, optional = true }

primitives = { path = "../primitives" }
chain_traits = { path = "../chain_traits", optional = true }
gem_client = { path = "../gem_client", optional = true }
gem_hash = { path = "../gem_hash", optional = true }
hex = { workspace = true, optional = true }
signer = { path = "../signer", optional = true }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
reqwest = { workspace = true }
settings = { path = "../settings", features = ["testkit"] }
primitives = { path = "../primitives", features = ["testkit"] }
hex = { workspace = true }
67 changes: 67 additions & 0 deletions crates/gem_algorand/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use gem_encoding::{decode_base32, encode_base32};
use gem_hash::sha2::sha512_256;
use primitives::Address;
use signer::Base32Address;
use std::fmt;

const ADDRESS_DATA_LENGTH: usize = 32;
const ADDRESS_CHECKSUM_LENGTH: usize = 4;

#[derive(Clone)]
pub struct AlgorandAddress {
pub(crate) base32: Base32Address,
}

impl Address for AlgorandAddress {
fn try_parse(address: &str) -> Option<Self> {
let decoded = decode_base32(address.as_bytes()).ok()?;
if decoded.len() != ADDRESS_DATA_LENGTH + ADDRESS_CHECKSUM_LENGTH {
return None;
}
let base32 = Base32Address::from_slice(&decoded[..ADDRESS_DATA_LENGTH]).ok()?;
(decoded[ADDRESS_DATA_LENGTH..] == Self::checksum(base32.payload())).then_some(Self { base32 })
}

fn as_bytes(&self) -> &[u8] {
self.base32.payload()
}

fn encode(&self) -> String {
let mut raw = Vec::with_capacity(ADDRESS_DATA_LENGTH + ADDRESS_CHECKSUM_LENGTH);
raw.extend_from_slice(self.base32.payload());
raw.extend_from_slice(&Self::checksum(self.base32.payload()));
encode_base32(&raw)
}
}

impl AlgorandAddress {
fn checksum(bytes: &[u8; 32]) -> [u8; ADDRESS_CHECKSUM_LENGTH] {
let digest = sha512_256(bytes);
let mut checksum = [0u8; ADDRESS_CHECKSUM_LENGTH];
checksum.copy_from_slice(&digest[digest.len() - ADDRESS_CHECKSUM_LENGTH..]);
checksum
}
}

impl fmt::Display for AlgorandAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.encode())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_algorand_address() {
assert!(AlgorandAddress::is_valid("QKDS2YGDHDFZFAAGA4HAF3AJIKW5ZN46P66QDR3ELCXKKJUJTPJSXVHNQU"));
assert!(!AlgorandAddress::is_valid(""));
assert!(!AlgorandAddress::is_valid("invalid"));
assert!(!AlgorandAddress::is_valid("QKDS2YGDHDFZFAAGA4HAF3AJIKW5ZN46P66QDR3ELCXKKJUJTPJSXVHNQX"));

let addr = AlgorandAddress::from_str("QKDS2YGDHDFZFAAGA4HAF3AJIKW5ZN46P66QDR3ELCXKKJUJTPJSXVHNQU").unwrap();
assert_eq!(addr.to_string(), "QKDS2YGDHDFZFAAGA4HAF3AJIKW5ZN46P66QDR3ELCXKKJUJTPJSXVHNQU");
assert_eq!(addr.as_bytes().len(), 32);
}
}
8 changes: 8 additions & 0 deletions crates/gem_algorand/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ pub mod rpc;
#[cfg(feature = "rpc")]
pub mod provider;

#[cfg(feature = "signer")]
pub mod address;
pub mod constants;
pub mod models;
#[cfg(feature = "signer")]
pub mod signer;

#[cfg(feature = "signer")]
pub use address::AlgorandAddress;
#[cfg(feature = "rpc")]
pub use rpc::client::AlgorandClient;
#[cfg(feature = "signer")]
pub use signer::*;
2 changes: 2 additions & 0 deletions crates/gem_algorand/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ pub mod account;
pub mod asset;
pub mod block;
pub mod indexer;
#[cfg(feature = "signer")]
pub mod signing;
pub mod transaction;

pub use account::*;
Expand Down
5 changes: 5 additions & 0 deletions crates/gem_algorand/src/models/signing/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod operation;
mod transaction;

pub use operation::Operation;
pub use transaction::AlgorandTransaction;
41 changes: 41 additions & 0 deletions crates/gem_algorand/src/models/signing/operation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::address::AlgorandAddress;

const TX_TYPE_PAYMENT: &str = "pay";
const TX_TYPE_ASSET_TRANSFER: &str = "axfer";

// Canonical msgpack field counts per operation type.
const PAYMENT_FIELDS: u8 = 9;
const PAYMENT_ZERO_AMOUNT_FIELDS: u8 = 8;
const ASSET_TRANSFER_FIELDS: u8 = 10;
const ASSET_OPT_IN_FIELDS: u8 = 9;

pub enum Operation {
Payment { destination: AlgorandAddress, amount: u64 },
AssetTransfer { destination: AlgorandAddress, amount: u64, asset_id: u64 },
AssetOptIn { asset_id: u64 },
}

impl Operation {
pub fn tx_type(&self) -> &'static str {
match self {
Self::Payment { .. } => TX_TYPE_PAYMENT,
Self::AssetTransfer { .. } | Self::AssetOptIn { .. } => TX_TYPE_ASSET_TRANSFER,
}
}

pub fn size(&self) -> u8 {
match self {
Self::Payment { amount: 0, .. } => PAYMENT_ZERO_AMOUNT_FIELDS,
Self::Payment { .. } => PAYMENT_FIELDS,
Self::AssetTransfer { .. } => ASSET_TRANSFER_FIELDS,
Self::AssetOptIn { .. } => ASSET_OPT_IN_FIELDS,
}
}

pub fn payment_amount(&self) -> Option<u64> {
match self {
Self::Payment { amount, .. } if *amount > 0 => Some(*amount),
_ => None,
}
}
}
66 changes: 66 additions & 0 deletions crates/gem_algorand/src/models/signing/transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use super::Operation;
use crate::address::AlgorandAddress;
use gem_encoding::decode_base64;
use num_traits::ToPrimitive;
use primitives::{Address, SignerError, SignerInput};
use signer::InvalidInput;

const TRANSACTION_VALIDITY_ROUNDS: u64 = 1000;

pub struct AlgorandTransaction {
pub sender: AlgorandAddress,
pub fee: u64,
pub first_round: u64,
pub last_round: u64,
pub genesis_id: String,
pub genesis_hash: Vec<u8>,
pub note: Vec<u8>,
pub operation: Operation,
}

impl AlgorandTransaction {
pub fn transfer(input: &SignerInput) -> Result<Self, SignerError> {
Self::from_input(
input,
Operation::Payment {
destination: AlgorandAddress::from_str(&input.destination_address).invalid_input("invalid Algorand address")?,
amount: input.value.parse::<u64>().invalid_input("invalid Algorand amount")?,
},
)
}

pub fn token_transfer(input: &SignerInput) -> Result<Self, SignerError> {
Self::from_input(
input,
Operation::AssetTransfer {
destination: AlgorandAddress::from_str(&input.destination_address).invalid_input("invalid Algorand address")?,
amount: input.value.parse::<u64>().invalid_input("invalid Algorand amount")?,
asset_id: get_asset_id(input)?,
},
)
}

pub fn account_action(input: &SignerInput) -> Result<Self, SignerError> {
Self::from_input(input, Operation::AssetOptIn { asset_id: get_asset_id(input)? })
}

fn from_input(input: &SignerInput, operation: Operation) -> Result<Self, SignerError> {
let fee = input.fee.fee.to_u64().invalid_input("invalid transaction fee")?;
let first_round = input.metadata.get_sequence().map_err(SignerError::from_display)?;

Ok(Self {
sender: AlgorandAddress::from_str(&input.sender_address).invalid_input("invalid Algorand address")?,
fee,
first_round,
last_round: first_round + TRANSACTION_VALIDITY_ROUNDS,
genesis_id: input.metadata.get_chain_id().map_err(SignerError::from_display)?,
genesis_hash: decode_base64(&input.metadata.get_block_hash().map_err(SignerError::from_display)?).invalid_input("invalid Algorand genesis hash")?,
note: input.memo.clone().unwrap_or_default().into_bytes(),
operation,
})
}
}

fn get_asset_id(input: &SignerInput) -> Result<u64, SignerError> {
input.input_type.get_asset().id.get_token_id()?.parse::<u64>().invalid_input("invalid Algorand asset id")
}
19 changes: 19 additions & 0 deletions crates/gem_algorand/src/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::str;

use gem_encoding::decode_base64;
use primitives::TransactionState;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -66,4 +67,22 @@ pub struct TransactionBroadcast {
pub struct TransactionStatus {
#[serde(rename = "confirmed-round")]
pub confirmed_round: Option<i64>,
#[serde(rename = "pool-error")]
pub pool_error: Option<String>,
}

impl TransactionStatus {
pub fn state(&self) -> TransactionState {
if self.confirmed_round.unwrap_or(0) > 0 {
TransactionState::Confirmed
} else if self.has_pool_error() {
TransactionState::Failed
} else {
TransactionState::Pending
}
}

fn has_pool_error(&self) -> bool {
self.pool_error.as_ref().is_some_and(|error| !error.trim().is_empty())
}
}
25 changes: 18 additions & 7 deletions crates/gem_algorand/src/provider/transaction_state_mapper.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use crate::models::TransactionStatus;
use primitives::{TransactionChange, TransactionState, TransactionUpdate};
use primitives::{TransactionChange, TransactionUpdate};

pub fn map_transaction_status(transaction: &TransactionStatus) -> TransactionUpdate {
let confirmed_round = transaction.confirmed_round.unwrap_or(0);
let state: TransactionState = if confirmed_round > 0 { TransactionState::Confirmed } else { TransactionState::Failed };

let state = transaction.state();
let mut changes = Vec::new();
if confirmed_round > 0 {
changes.push(TransactionChange::BlockNumber(confirmed_round.to_string()));
if let Some(round) = transaction.confirmed_round.filter(|r| *r > 0) {
changes.push(TransactionChange::BlockNumber(round.to_string()));
}

TransactionUpdate { state, changes }
Expand All @@ -21,7 +19,10 @@ mod tests {

#[test]
fn test_map_transaction_status_confirmed() {
let result = map_transaction_status(&TransactionStatus { confirmed_round: Some(52961610) });
let result = map_transaction_status(&TransactionStatus {
confirmed_round: Some(52961610),
pool_error: None,
});
assert_eq!(result.state, TransactionState::Confirmed);
assert_eq!(result.changes, vec![TransactionChange::BlockNumber("52961610".to_string())]);
}
Expand All @@ -38,6 +39,16 @@ mod tests {
fn test_map_transaction_status_pending_data() {
let status: TransactionStatus = serde_json::from_str(include_str!("../../testdata/transaction_transfer_pending.json")).unwrap();
let result = map_transaction_status(&status);
assert_eq!(result.state, TransactionState::Pending);
assert_eq!(result.changes.len(), 0);
}

#[test]
fn test_map_transaction_status_failed_with_pool_error() {
let result = map_transaction_status(&TransactionStatus {
confirmed_round: None,
pool_error: Some("overspend".to_string()),
});
assert_eq!(result.state, TransactionState::Failed);
assert_eq!(result.changes.len(), 0);
}
Expand Down
Loading
Loading