use std::collections::HashMap;
use std::sync::Arc;
use algonaut_abi::abi_interactions::{AbiMethod, TransactionArgType};
use algonaut_model::algod::{
SimulateRequest, SimulateRequestTransactionGroup, SimulateTransactionResponse,
};
use algonaut_model::transaction::ApiSignedTransaction;
use algonaut_transaction::group::assign_in_place;
use algonaut_transaction::{SignedTransaction, Signer, Transaction};
use crate::{
Error,
algod::v2::{Algod, PendingSubmission},
simulate::SimulateResponse,
};
use super::MAX_ATOMIC_GROUP_SIZE;
use super::encode::{process_method_call, validate_transaction};
use super::method_call::MethodCall;
use super::outcome::{
AbiMethodResult, AbiReturnDecodeError, ExecuteOutcome, SimulateOutcome,
get_return_value_with_return_type,
};
use super::signing::{placeholder_group, poll_until_confirmed, sign_group, transaction_ids};
#[derive(Debug, Clone)]
pub struct TransactionWithSigner {
pub transaction: Transaction,
pub signer: Option<Arc<dyn Signer>>,
}
impl TransactionWithSigner {
pub fn new(transaction: Transaction, signer: Arc<dyn Signer>) -> Self {
Self {
transaction,
signer: Some(signer),
}
}
pub fn unsigned(transaction: Transaction) -> Self {
Self {
transaction,
signer: None,
}
}
}
#[derive(Debug, Clone)]
enum AtomicGroupEntry {
Transaction(TransactionWithSigner),
MethodCall(MethodCall),
}
#[derive(Debug, Clone, Default)]
pub struct AtomicGroupBuilder {
entries: Vec<AtomicGroupEntry>,
}
impl AtomicGroupBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_transaction(mut self, transaction_with_signer: TransactionWithSigner) -> Self {
self.entries
.push(AtomicGroupEntry::Transaction(transaction_with_signer));
self
}
pub fn add_method_call(mut self, call: MethodCall) -> Self {
self.entries.push(AtomicGroupEntry::MethodCall(call));
self
}
pub fn build(self) -> Result<UnsignedAtomicGroup, Error> {
let mut txs: Vec<TransactionWithSigner> = Vec::new();
let mut method_map: HashMap<usize, AbiMethod> = HashMap::new();
for entry in self.entries {
match entry {
AtomicGroupEntry::Transaction(transaction_with_signer) => {
if txs.len() == MAX_ATOMIC_GROUP_SIZE {
return Err(Error::ComposerGroupFull {
max: MAX_ATOMIC_GROUP_SIZE,
});
}
validate_transaction(
&transaction_with_signer.transaction,
TransactionArgType::Any,
)?;
txs.push(transaction_with_signer);
}
AtomicGroupEntry::MethodCall(call) => {
process_method_call(call, &mut txs, &mut method_map)?;
}
}
}
if txs.is_empty() {
return Err(Error::EmptyTransactionGroup);
}
if txs.len() > 1 {
let mut group_txs: Vec<&mut Transaction> =
txs.iter_mut().map(|t| &mut t.transaction).collect();
assign_in_place(&mut group_txs)?;
}
Ok(UnsignedAtomicGroup { txs, method_map })
}
}
#[derive(Debug, Clone)]
pub struct UnsignedAtomicGroup {
txs: Vec<TransactionWithSigner>,
method_map: HashMap<usize, AbiMethod>,
}
impl UnsignedAtomicGroup {
pub fn transactions(&self) -> &[TransactionWithSigner] {
&self.txs
}
pub async fn sign(self) -> Result<SignedAtomicGroup, Error> {
let signed_txs = sign_group(&self.txs).await?;
Ok(SignedAtomicGroup {
signed_txs,
method_map: self.method_map,
})
}
pub async fn simulate(&self, algod: &Algod) -> Result<SimulateOutcome, Error> {
self.simulate_with(algod, SimulateRequest::new(vec![]))
.await
}
pub async fn simulate_with(
&self,
algod: &Algod,
mut request: SimulateRequest,
) -> Result<SimulateOutcome, Error> {
let signed_txs = placeholder_group(&self.txs)?;
let api_txns = signed_txs
.iter()
.cloned()
.map(ApiSignedTransaction::try_from)
.collect::<Result<Vec<_>, _>>()?;
request.txn_groups = vec![SimulateRequestTransactionGroup::new(api_txns)];
request.allow_empty_signatures = Some(true);
let response: SimulateTransactionResponse = algod.simulate(request).await?;
let mut method_results: Vec<AbiMethodResult> = Vec::new();
if let Some(group) = response.txn_groups.first() {
for (i, txn_result) in group.txn_results.iter().enumerate() {
if !self.method_map.contains_key(&i) {
continue;
}
let transaction_id = signed_txs[i].transaction_id().clone();
let return_type = self.method_map[&i].returns.clone().type_()?;
method_results.push(get_return_value_with_return_type(
&txn_result.txn_result,
&transaction_id,
return_type,
)?);
}
}
Ok(SimulateOutcome {
transaction_ids: transaction_ids(&signed_txs),
method_results,
simulate_response: SimulateResponse::new(response),
})
}
}
#[derive(Debug, Clone)]
pub struct SignedAtomicGroup {
signed_txs: Vec<SignedTransaction>,
method_map: HashMap<usize, AbiMethod>,
}
impl SignedAtomicGroup {
pub fn signed_transactions(&self) -> &[SignedTransaction] {
&self.signed_txs
}
pub async fn submit(self, algod: &Algod) -> Result<PendingSubmission, Error> {
algod.submit_transactions(&self.signed_txs).await
}
pub async fn execute(self, algod: &Algod) -> Result<ExecuteOutcome, Error> {
algod.send_transactions(&self.signed_txs).await?;
let index_to_wait = (0..self.signed_txs.len())
.find(|i| self.method_map.contains_key(i))
.unwrap_or(0);
let transaction_id = self.signed_txs[index_to_wait].transaction_id().clone();
let pending_tx = poll_until_confirmed(algod, &transaction_id).await?;
let mut method_results: Vec<AbiMethodResult> = vec![];
for i in 0..self.signed_txs.len() {
if !self.method_map.contains_key(&i) {
continue;
}
let return_type = self.method_map[&i].returns.clone().type_()?;
if i == index_to_wait {
method_results.push(get_return_value_with_return_type(
&pending_tx,
&transaction_id,
return_type,
)?);
continue;
}
let other_transaction_id = self.signed_txs[i].transaction_id().clone();
match algod.pending_transaction(&other_transaction_id).await {
Ok(other_pending_tx) => method_results.push(get_return_value_with_return_type(
&other_pending_tx,
&other_transaction_id,
return_type,
)?),
Err(e) => method_results.push(AbiMethodResult {
transaction_id: other_transaction_id,
transaction_info: pending_tx.clone(),
return_value: Err(AbiReturnDecodeError(format!("{e:?}"))),
}),
}
}
Ok(ExecuteOutcome {
confirmed_round: pending_tx.confirmed_round,
transaction_ids: transaction_ids(&self.signed_txs),
method_results,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use algonaut_core::{Address, MicroAlgos};
use algonaut_crypto::HashDigest;
use algonaut_transaction::account::Account;
use algonaut_transaction::builder::{Pay, TransactionParams};
use algonaut_transaction::{SigningFuture, SigningRequest};
use std::sync::atomic::{AtomicUsize, Ordering};
struct StubParams {
genesis_id: String,
}
impl TransactionParams for StubParams {
fn last_round(&self) -> u64 {
1
}
fn min_fee(&self) -> u64 {
1_000
}
fn genesis_hash(&self) -> HashDigest {
HashDigest([0; 32])
}
fn genesis_id(&self) -> &String {
&self.genesis_id
}
}
fn pay(sender: &Account, receiver: Address) -> Transaction {
let params = StubParams {
genesis_id: "testnet-v1.0".to_owned(),
};
Pay::new(sender.address(), receiver, MicroAlgos(1_000))
.build(¶ms)
.expect("failed to build payment transaction")
}
#[derive(Debug)]
struct CountingSigner {
inner: Account,
calls: AtomicUsize,
}
impl Signer for CountingSigner {
fn sign_transactions<'a>(&'a self, request: SigningRequest<'a>) -> SigningFuture<'a> {
self.calls.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
request
.indexes
.iter()
.map(|&i| self.inner.sign(request.transactions[i].clone()))
.collect()
})
}
}
#[derive(Debug)]
struct CannedSigner {
output: Vec<SignedTransaction>,
}
impl Signer for CannedSigner {
fn sign_transactions<'a>(&'a self, _request: SigningRequest<'a>) -> SigningFuture<'a> {
let output = self.output.clone();
Box::pin(async move { Ok(output) })
}
}
#[tokio::test]
async fn sign_groups_slots_sharing_one_signer_into_one_call() {
let alice = Account::generate();
let bob = Account::generate();
let signer = Arc::new(CountingSigner {
inner: alice.clone(),
calls: AtomicUsize::new(0),
});
let signed = AtomicGroupBuilder::new()
.add_transaction(TransactionWithSigner::new(
pay(&alice, bob.address()),
signer.clone(),
))
.add_transaction(TransactionWithSigner::new(
pay(&alice, alice.address()),
signer.clone(),
))
.build()
.unwrap()
.sign()
.await
.unwrap();
assert_eq!(signed.signed_transactions().len(), 2);
assert_eq!(
signer.calls.load(Ordering::SeqCst),
1,
"slots sharing one signer instance must be signed in a single call"
);
}
#[tokio::test]
async fn sign_rejects_signer_returning_wrong_transaction() {
let alice = Account::generate();
let bob = Account::generate();
let decoy = bob.sign(pay(&bob, alice.address())).unwrap();
let signer = Arc::new(CannedSigner {
output: vec![decoy],
});
let err = AtomicGroupBuilder::new()
.add_transaction(TransactionWithSigner::new(
pay(&alice, bob.address()),
signer,
))
.build()
.unwrap()
.sign()
.await
.unwrap_err();
assert!(matches!(err, Error::SignerOutputInvalid { .. }));
}
#[tokio::test]
async fn sign_rejects_signer_returning_wrong_count() {
let alice = Account::generate();
let bob = Account::generate();
let signer = Arc::new(CannedSigner { output: vec![] });
let err = AtomicGroupBuilder::new()
.add_transaction(TransactionWithSigner::new(
pay(&alice, bob.address()),
signer,
))
.build()
.unwrap()
.sign()
.await
.unwrap_err();
assert!(matches!(err, Error::SignerOutputInvalid { .. }));
}
#[tokio::test]
async fn sign_rejects_unsigned_slot() {
let alice = Account::generate();
let bob = Account::generate();
let err = AtomicGroupBuilder::new()
.add_transaction(TransactionWithSigner::new(
pay(&alice, bob.address()),
Arc::new(alice.clone()),
))
.add_transaction(TransactionWithSigner::unsigned(pay(&bob, alice.address())))
.build()
.unwrap()
.sign()
.await
.unwrap_err();
assert!(matches!(err, Error::MissingSigner { index: 1 }));
}
#[tokio::test]
async fn sign_signs_every_tx_with_distinct_signers() {
let alice = Account::generate();
let bob = Account::generate();
let signed = AtomicGroupBuilder::new()
.add_transaction(TransactionWithSigner::new(
pay(&alice, bob.address()),
Arc::new(alice.clone()),
))
.add_transaction(TransactionWithSigner::new(
pay(&bob, alice.address()),
Arc::new(bob.clone()),
))
.build()
.unwrap()
.sign()
.await
.unwrap();
let txs = signed.signed_transactions();
assert_eq!(
txs.len(),
2,
"every input transaction must be signed exactly once"
);
assert_eq!(txs[0].transaction().sender(), alice.address());
assert_eq!(txs[1].transaction().sender(), bob.address());
}
#[test]
fn build_rejects_empty_group() {
let err = AtomicGroupBuilder::new().build().unwrap_err();
assert!(matches!(err, Error::EmptyTransactionGroup));
}
}