mod multisig;
mod transactions;
use algokit_transact::constants::*;
use algokit_transact::{
AlgorandMsgpack, EstimateTransactionSize, TransactionId, Transactions, Validate,
};
use ffi_macros::{ffi_enum, ffi_func, ffi_record};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
pub use multisig::{MultisigSignature, MultisigSubsignature};
pub use transactions::AppCallTransactionFields;
pub use transactions::AssetConfigTransactionFields;
pub use transactions::AssetFreezeTransactionFields;
pub use transactions::AssetTransferTransactionFields;
pub use transactions::KeyRegistrationTransactionFields;
pub use transactions::PaymentTransactionFields;
use snafu::Snafu;
#[derive(Debug, Snafu)]
#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Error))]
pub enum AlgoKitTransactError {
#[snafu(display("EncodingError: {message}"))]
EncodingError { message: String },
#[snafu(display("DecodingError: {message}"))]
DecodingError { message: String },
#[snafu(display("{message}"))]
InputError { message: String },
#[snafu(display("MsgPackError: {message}"))]
MsgPackError { message: String },
}
#[cfg(feature = "ffi_wasm")]
impl From<AlgoKitTransactError> for JsValue {
fn from(e: AlgoKitTransactError) -> Self {
JsValue::from(e.to_string())
}
}
impl From<algokit_transact::AlgoKitTransactError> for AlgoKitTransactError {
fn from(e: algokit_transact::AlgoKitTransactError) -> Self {
match e {
algokit_transact::AlgoKitTransactError::DecodingError { .. } => {
AlgoKitTransactError::DecodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::EncodingError { .. } => {
AlgoKitTransactError::EncodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::MsgpackDecodingError { .. } => {
AlgoKitTransactError::DecodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::MsgpackEncodingError { .. } => {
AlgoKitTransactError::EncodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::UnknownTransactionType { .. } => {
AlgoKitTransactError::DecodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::InputError { message } => {
AlgoKitTransactError::InputError { message }
}
algokit_transact::AlgoKitTransactError::InvalidAddress { .. } => {
AlgoKitTransactError::DecodingError {
message: e.to_string(),
}
}
algokit_transact::AlgoKitTransactError::InvalidMultisigSignature { .. } => {
AlgoKitTransactError::DecodingError {
message: e.to_string(),
}
}
}
}
}
#[cfg(feature = "ffi_uniffi")]
use uniffi::{self};
#[cfg(feature = "ffi_uniffi")]
uniffi::setup_scaffolding!();
#[cfg(feature = "ffi_wasm")]
use js_sys::Uint8Array;
#[cfg(feature = "ffi_wasm")]
use tsify_next::Tsify;
#[cfg(feature = "ffi_wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "ffi_uniffi")]
impl UniffiCustomTypeConverter for ByteBuf {
type Builtin = Vec<u8>;
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Ok(ByteBuf::from(val))
}
fn from_custom(obj: Self) -> Self::Builtin {
obj.to_vec()
}
}
#[cfg(feature = "ffi_uniffi")]
uniffi::custom_type!(ByteBuf, Vec<u8>);
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "ffi_wasm", derive(Tsify))]
#[cfg_attr(feature = "ffi_wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Enum))]
pub enum TransactionType {
Payment,
AssetTransfer,
AssetFreeze,
AssetConfig,
KeyRegistration,
AppCall,
}
#[ffi_record]
pub struct KeyPairAccount {
pub_key: ByteBuf,
}
impl From<algokit_transact::KeyPairAccount> for KeyPairAccount {
fn from(value: algokit_transact::KeyPairAccount) -> Self {
Self {
pub_key: value.pub_key.to_vec().into(),
}
}
}
impl TryFrom<KeyPairAccount> for algokit_transact::KeyPairAccount {
type Error = AlgoKitTransactError;
fn try_from(value: KeyPairAccount) -> Result<Self, Self::Error> {
let pub_key: [u8; ALGORAND_PUBLIC_KEY_BYTE_LENGTH] = bytebuf_to_bytes(&value.pub_key)
.map_err(|e| AlgoKitTransactError::DecodingError {
message: format!("Error while decoding a public key: {}", e),
})?;
Ok(algokit_transact::KeyPairAccount::from_pubkey(&pub_key))
}
}
impl From<algokit_transact::Address> for KeyPairAccount {
fn from(value: algokit_transact::Address) -> Self {
Self {
pub_key: value.as_bytes().to_vec().into(),
}
}
}
impl TryFrom<KeyPairAccount> for algokit_transact::Address {
type Error = AlgoKitTransactError;
fn try_from(value: KeyPairAccount) -> Result<Self, Self::Error> {
let impl_keypair_account: algokit_transact::KeyPairAccount = value.try_into()?;
Ok(impl_keypair_account.address())
}
}
#[ffi_record]
pub struct FeeParams {
fee_per_byte: u64,
min_fee: u64,
extra_fee: Option<u64>,
max_fee: Option<u64>,
}
#[ffi_record]
pub struct Transaction {
transaction_type: TransactionType,
sender: String,
fee: Option<u64>,
first_valid: u64,
last_valid: u64,
genesis_hash: Option<ByteBuf>,
genesis_id: Option<String>,
note: Option<ByteBuf>,
rekey_to: Option<String>,
lease: Option<ByteBuf>,
group: Option<ByteBuf>,
payment: Option<PaymentTransactionFields>,
asset_transfer: Option<AssetTransferTransactionFields>,
asset_config: Option<AssetConfigTransactionFields>,
app_call: Option<AppCallTransactionFields>,
key_registration: Option<KeyRegistrationTransactionFields>,
asset_freeze: Option<AssetFreezeTransactionFields>,
}
impl TryFrom<Transaction> for algokit_transact::Transaction {
type Error = AlgoKitTransactError;
fn try_from(tx: Transaction) -> Result<Self, AlgoKitTransactError> {
if [
tx.payment.is_some(),
tx.asset_transfer.is_some(),
tx.asset_config.is_some(),
tx.key_registration.is_some(),
tx.app_call.is_some(),
tx.asset_freeze.is_some(),
]
.into_iter()
.filter(|&x| x)
.count()
> 1
{
return Err(Self::Error::DecodingError {
message: "Multiple transaction type specific fields set".to_string(),
});
}
match tx.transaction_type {
TransactionType::Payment => Ok(algokit_transact::Transaction::Payment(tx.try_into()?)),
TransactionType::AssetTransfer => {
Ok(algokit_transact::Transaction::AssetTransfer(tx.try_into()?))
}
TransactionType::KeyRegistration => Ok(algokit_transact::Transaction::KeyRegistration(
tx.try_into()?,
)),
TransactionType::AssetConfig => {
Ok(algokit_transact::Transaction::AssetConfig(tx.try_into()?))
}
TransactionType::AppCall => Ok(algokit_transact::Transaction::AppCall(tx.try_into()?)),
TransactionType::AssetFreeze => {
Ok(algokit_transact::Transaction::AssetFreeze(tx.try_into()?))
}
}
}
}
impl TryFrom<Transaction> for algokit_transact::TransactionHeader {
type Error = AlgoKitTransactError;
fn try_from(tx: Transaction) -> Result<Self, AlgoKitTransactError> {
Ok(Self {
sender: tx.sender.parse()?,
fee: tx.fee,
first_valid: tx.first_valid,
last_valid: tx.last_valid,
genesis_id: tx.genesis_id,
genesis_hash: tx
.genesis_hash
.map(|buf| bytebuf_to_bytes::<32>(&buf))
.transpose()?,
note: tx.note.map(ByteBuf::into_vec),
rekey_to: tx.rekey_to.map(|addr| addr.parse()).transpose()?,
lease: tx
.lease
.map(|buf| bytebuf_to_bytes::<32>(&buf))
.transpose()?,
group: tx
.group
.map(|buf| bytebuf_to_bytes::<32>(&buf))
.transpose()?,
})
}
}
impl TryFrom<algokit_transact::Transaction> for Transaction {
type Error = AlgoKitTransactError;
fn try_from(tx: algokit_transact::Transaction) -> Result<Self, AlgoKitTransactError> {
match tx {
algokit_transact::Transaction::Payment(payment) => {
let payment_fields = payment.clone().into();
build_transaction(
payment.header,
TransactionType::Payment,
Some(payment_fields),
None,
None,
None,
None,
None,
)
}
algokit_transact::Transaction::AssetTransfer(asset_transfer) => {
let asset_transfer_fields = asset_transfer.clone().into();
build_transaction(
asset_transfer.header,
TransactionType::AssetTransfer,
None,
Some(asset_transfer_fields),
None,
None,
None,
None,
)
}
algokit_transact::Transaction::AssetConfig(asset_config) => {
let asset_config_fields = asset_config.clone().into();
build_transaction(
asset_config.header.clone(),
TransactionType::AssetConfig,
None,
None,
Some(asset_config_fields),
None,
None,
None,
)
}
algokit_transact::Transaction::AppCall(app_call) => {
let app_call_fields = app_call.clone().into();
build_transaction(
app_call.header,
TransactionType::AppCall,
None,
None,
None,
Some(app_call_fields),
None,
None,
)
}
algokit_transact::Transaction::KeyRegistration(key_registration) => {
let key_registration_fields = key_registration.clone().into();
build_transaction(
key_registration.header,
TransactionType::KeyRegistration,
None,
None,
None,
None,
Some(key_registration_fields),
None,
)
}
algokit_transact::Transaction::AssetFreeze(asset_freeze) => {
let asset_freeze_fields = asset_freeze.clone().into();
build_transaction(
asset_freeze.header,
TransactionType::AssetFreeze,
None,
None,
None,
None,
None,
Some(asset_freeze_fields),
)
}
}
}
}
#[ffi_record]
pub struct SignedTransaction {
pub transaction: Transaction,
pub signature: Option<ByteBuf>,
pub auth_address: Option<String>,
pub multisignature: Option<MultisigSignature>,
}
impl From<algokit_transact::SignedTransaction> for SignedTransaction {
fn from(signed_tx: algokit_transact::SignedTransaction) -> Self {
Self {
transaction: signed_tx.transaction.try_into().unwrap(),
signature: signed_tx.signature.map(|sig| sig.to_vec().into()),
auth_address: signed_tx.auth_address.map(|addr| addr.as_str()),
multisignature: signed_tx.multisignature.map(Into::into),
}
}
}
impl TryFrom<SignedTransaction> for algokit_transact::SignedTransaction {
type Error = AlgoKitTransactError;
fn try_from(signed_tx: SignedTransaction) -> Result<Self, Self::Error> {
Ok(Self {
transaction: signed_tx.transaction.try_into()?,
signature: signed_tx
.signature
.map(|sig| bytebuf_to_bytes(&sig))
.transpose()
.map_err(|e| AlgoKitTransactError::DecodingError {
message: format!(
"Error while decoding the signature in a signed transaction: {}",
e
),
})?,
auth_address: signed_tx
.auth_address
.map(|addr| addr.parse())
.transpose()?,
multisignature: signed_tx
.multisignature
.map(TryInto::try_into)
.transpose()?,
})
}
}
fn bytebuf_to_bytes<const N: usize>(buf: &ByteBuf) -> Result<[u8; N], AlgoKitTransactError> {
buf.to_vec()
.try_into()
.map_err(|_| AlgoKitTransactError::DecodingError {
message: format!("Expected {} bytes but got a different length", N),
})
}
fn byte32_to_bytebuf(b32: Byte32) -> ByteBuf {
ByteBuf::from(b32.to_vec())
}
#[allow(clippy::too_many_arguments)]
fn build_transaction(
header: algokit_transact::TransactionHeader,
transaction_type: TransactionType,
payment: Option<PaymentTransactionFields>,
asset_transfer: Option<AssetTransferTransactionFields>,
asset_config: Option<AssetConfigTransactionFields>,
app_call: Option<AppCallTransactionFields>,
key_registration: Option<KeyRegistrationTransactionFields>,
asset_freeze: Option<AssetFreezeTransactionFields>,
) -> Result<Transaction, AlgoKitTransactError> {
Ok(Transaction {
transaction_type,
sender: header.sender.as_str(),
fee: header.fee,
first_valid: header.first_valid,
last_valid: header.last_valid,
genesis_id: header.genesis_id,
genesis_hash: header.genesis_hash.map(byte32_to_bytebuf),
note: header.note.map(Into::into),
rekey_to: header.rekey_to.map(|addr| addr.as_str()),
lease: header.lease.map(byte32_to_bytebuf),
group: header.group.map(byte32_to_bytebuf),
payment,
asset_transfer,
asset_config,
app_call,
key_registration,
asset_freeze,
})
}
#[ffi_func]
pub fn get_encoded_transaction_type(bytes: &[u8]) -> Result<TransactionType, AlgoKitTransactError> {
let decoded = algokit_transact::Transaction::decode(bytes)?;
match decoded {
algokit_transact::Transaction::Payment(_) => Ok(TransactionType::Payment),
algokit_transact::Transaction::AssetTransfer(_) => Ok(TransactionType::AssetTransfer),
algokit_transact::Transaction::AssetConfig(_) => Ok(TransactionType::AssetConfig),
algokit_transact::Transaction::AppCall(_) => Ok(TransactionType::AppCall),
algokit_transact::Transaction::KeyRegistration(_) => Ok(TransactionType::KeyRegistration),
algokit_transact::Transaction::AssetFreeze(_) => Ok(TransactionType::AssetFreeze),
}
}
#[ffi_func]
pub fn encode_transaction(tx: Transaction) -> Result<Vec<u8>, AlgoKitTransactError> {
let ctx: algokit_transact::Transaction = tx.try_into()?;
Ok(ctx.encode()?)
}
#[cfg(feature = "ffi_wasm")]
#[ffi_func]
pub fn encode_transactions(txs: Vec<Transaction>) -> Result<Vec<Uint8Array>, AlgoKitTransactError> {
txs.into_iter()
.map(|tx| encode_transaction(tx).map(|bytes| bytes.as_slice().into()))
.collect()
}
#[cfg(not(feature = "ffi_wasm"))]
#[ffi_func]
pub fn encode_transactions(txs: Vec<Transaction>) -> Result<Vec<Vec<u8>>, AlgoKitTransactError> {
txs.into_iter().map(encode_transaction).collect()
}
#[ffi_func]
pub fn encode_transaction_raw(tx: Transaction) -> Result<Vec<u8>, AlgoKitTransactError> {
let ctx: algokit_transact::Transaction = tx.try_into()?;
Ok(ctx.encode_raw()?)
}
#[ffi_func]
pub fn decode_transaction(encoded_tx: &[u8]) -> Result<Transaction, AlgoKitTransactError> {
let ctx: algokit_transact::Transaction = algokit_transact::Transaction::decode(encoded_tx)?;
ctx.try_into()
}
#[cfg(feature = "ffi_wasm")]
#[ffi_func]
pub fn decode_transactions(
encoded_txs: Vec<Uint8Array>,
) -> Result<Vec<Transaction>, AlgoKitTransactError> {
encoded_txs
.iter()
.map(|bytes| decode_transaction(bytes.to_vec().as_slice()))
.collect()
}
#[cfg(not(feature = "ffi_wasm"))]
#[ffi_func]
pub fn decode_transactions(
encoded_txs: Vec<Vec<u8>>,
) -> Result<Vec<Transaction>, AlgoKitTransactError> {
encoded_txs
.iter()
.map(|tx| decode_transaction(tx))
.collect()
}
#[ffi_func]
pub fn estimate_transaction_size(transaction: Transaction) -> Result<u64, AlgoKitTransactError> {
let core_tx: algokit_transact::Transaction = transaction.try_into()?;
core_tx
.estimate_size()?
.try_into()
.map_err(|_| AlgoKitTransactError::EncodingError {
message: "Failed to convert size to u64".to_string(),
})
}
#[ffi_func]
pub fn keypair_account_from_pub_key(
pub_key: &[u8],
) -> Result<KeyPairAccount, AlgoKitTransactError> {
Ok(
algokit_transact::KeyPairAccount::from_pubkey(pub_key.try_into().map_err(|_| {
AlgoKitTransactError::EncodingError {
message: format!(
"public key should be {} bytes",
ALGORAND_PUBLIC_KEY_BYTE_LENGTH
),
}
})?)
.into(),
)
}
#[ffi_func]
pub fn keypair_account_from_address(address: &str) -> Result<KeyPairAccount, AlgoKitTransactError> {
Ok(address
.parse::<algokit_transact::KeyPairAccount>()
.map(Into::into)?)
}
#[ffi_func]
pub fn address_from_keypair_account(
account: KeyPairAccount,
) -> Result<String, AlgoKitTransactError> {
let impl_keypair_account: algokit_transact::KeyPairAccount = account.try_into()?;
Ok(impl_keypair_account.address().as_str())
}
#[ffi_func]
pub fn get_transaction_id_raw(tx: Transaction) -> Result<Vec<u8>, AlgoKitTransactError> {
let tx_internal: algokit_transact::Transaction = tx.try_into()?;
let id_raw = tx_internal.id_raw()?;
Ok(id_raw.to_vec())
}
#[ffi_func]
pub fn get_transaction_id(tx: Transaction) -> Result<String, AlgoKitTransactError> {
let tx_internal: algokit_transact::Transaction = tx.try_into()?;
Ok(tx_internal.id()?)
}
#[ffi_func]
pub fn group_transactions(txs: Vec<Transaction>) -> Result<Vec<Transaction>, AlgoKitTransactError> {
let txs_internal: Vec<algokit_transact::Transaction> = txs
.into_iter()
.map(|tx| tx.try_into())
.collect::<Result<Vec<_>, _>>()?;
let grouped_txs: Vec<Transaction> = txs_internal
.assign_group()?
.into_iter()
.map(|tx| tx.try_into())
.collect::<Result<Vec<_>, _>>()?;
Ok(grouped_txs)
}
#[ffi_enum]
pub enum AlgorandConstant {
HashLength,
ChecksumLength,
AddressLength,
PublicKeyLength,
SecretKeyLength,
SignatureLength,
SignatureEncodingIncrLength,
MaxTxGroupSize,
}
impl AlgorandConstant {
pub fn value(&self) -> u64 {
match self {
AlgorandConstant::HashLength => HASH_BYTES_LENGTH as u64,
AlgorandConstant::ChecksumLength => ALGORAND_CHECKSUM_BYTE_LENGTH as u64,
AlgorandConstant::AddressLength => ALGORAND_ADDRESS_LENGTH as u64,
AlgorandConstant::PublicKeyLength => ALGORAND_PUBLIC_KEY_BYTE_LENGTH as u64,
AlgorandConstant::SecretKeyLength => ALGORAND_SECRET_KEY_BYTE_LENGTH as u64,
AlgorandConstant::SignatureLength => ALGORAND_SIGNATURE_BYTE_LENGTH as u64,
AlgorandConstant::SignatureEncodingIncrLength => {
ALGORAND_SIGNATURE_ENCODING_INCR as u64
}
AlgorandConstant::MaxTxGroupSize => MAX_TX_GROUP_SIZE as u64,
}
}
}
#[ffi_func]
pub fn get_algorand_constant(constant: AlgorandConstant) -> u64 {
constant.value()
}
impl TryFrom<FeeParams> for algokit_transact::FeeParams {
type Error = AlgoKitTransactError;
fn try_from(value: FeeParams) -> Result<Self, Self::Error> {
Ok(Self {
fee_per_byte: value.fee_per_byte,
min_fee: value.min_fee,
extra_fee: value.extra_fee,
max_fee: value.max_fee,
})
}
}
#[ffi_func]
pub fn assign_fee(
txn: Transaction,
fee_params: FeeParams,
) -> Result<Transaction, AlgoKitTransactError> {
let txn_internal: algokit_transact::Transaction = txn.try_into()?;
let fee_params_internal: algokit_transact::FeeParams = fee_params.try_into()?;
let updated_txn = txn_internal.assign_fee(fee_params_internal)?;
updated_txn.try_into()
}
#[ffi_func]
pub fn decode_signed_transaction(bytes: &[u8]) -> Result<SignedTransaction, AlgoKitTransactError> {
let signed_tx = algokit_transact::SignedTransaction::decode(bytes)?;
Ok(signed_tx.into())
}
#[cfg(feature = "ffi_wasm")]
#[ffi_func]
pub fn decode_signed_transactions(
encoded_signed_txs: Vec<Uint8Array>,
) -> Result<Vec<SignedTransaction>, AlgoKitTransactError> {
encoded_signed_txs
.iter()
.map(|bytes| decode_signed_transaction(bytes.to_vec().as_slice()))
.collect()
}
#[cfg(not(feature = "ffi_wasm"))]
#[ffi_func]
pub fn decode_signed_transactions(
encoded_signed_txs: Vec<Vec<u8>>,
) -> Result<Vec<SignedTransaction>, AlgoKitTransactError> {
encoded_signed_txs
.iter()
.map(|tx| decode_signed_transaction(tx))
.collect()
}
#[ffi_func]
pub fn encode_signed_transaction(
signed_tx: SignedTransaction,
) -> Result<Vec<u8>, AlgoKitTransactError> {
let signed_tx_internal: algokit_transact::SignedTransaction = signed_tx.try_into()?;
Ok(signed_tx_internal.encode()?)
}
#[cfg(feature = "ffi_wasm")]
#[ffi_func]
pub fn encode_signed_transactions(
signed_txs: Vec<SignedTransaction>,
) -> Result<Vec<Uint8Array>, AlgoKitTransactError> {
signed_txs
.into_iter()
.map(|tx| encode_signed_transaction(tx).map(|bytes| bytes.as_slice().into()))
.collect()
}
#[cfg(not(feature = "ffi_wasm"))]
#[ffi_func]
pub fn encode_signed_transactions(
signed_txs: Vec<SignedTransaction>,
) -> Result<Vec<Vec<u8>>, AlgoKitTransactError> {
signed_txs
.into_iter()
.map(encode_signed_transaction)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use algokit_transact::test_utils::TestDataMother;
use pretty_assertions::assert_eq;
#[test]
fn test_group_transactions_ffi() {
let expected_group = [
157, 37, 101, 171, 205, 211, 38, 98, 250, 86, 254, 215, 115, 126, 212, 252, 24, 53,
199, 142, 152, 75, 250, 200, 173, 128, 52, 142, 13, 193, 184, 137,
];
let tx1 = TestDataMother::simple_payment()
.transaction
.try_into()
.unwrap();
let tx2 = TestDataMother::simple_asset_transfer()
.transaction
.try_into()
.unwrap();
let tx3 = TestDataMother::opt_in_asset_transfer()
.transaction
.try_into()
.unwrap();
let txs = vec![tx1, tx2, tx3];
let grouped_txs = group_transactions(txs.clone()).unwrap();
assert_eq!(grouped_txs.len(), txs.len());
for grouped_tx in grouped_txs.into_iter() {
assert_eq!(grouped_tx.group.unwrap(), &expected_group);
}
}
}