use crate::account::Account;
use crate::error::{AptosError, AptosResult};
use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
use crate::transaction::builder::{
DEFAULT_EXPIRATION_SECONDS, DEFAULT_GAS_UNIT_PRICE, DEFAULT_MAX_GAS_AMOUNT,
};
use crate::transaction::payload::TransactionPayload;
use crate::transaction::types::{FeePayerRawTransaction, RawTransaction, SignedTransaction};
use crate::types::{AccountAddress, ChainId};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Default)]
pub struct SponsoredTransactionBuilder {
sender_address: Option<AccountAddress>,
sequence_number: Option<u64>,
secondary_addresses: Vec<AccountAddress>,
fee_payer_address: Option<AccountAddress>,
payload: Option<TransactionPayload>,
max_gas_amount: u64,
gas_unit_price: u64,
expiration_timestamp_secs: Option<u64>,
chain_id: Option<ChainId>,
}
impl SponsoredTransactionBuilder {
#[must_use]
pub fn new() -> Self {
Self {
sender_address: None,
sequence_number: None,
secondary_addresses: Vec::new(),
fee_payer_address: None,
payload: None,
max_gas_amount: DEFAULT_MAX_GAS_AMOUNT,
gas_unit_price: DEFAULT_GAS_UNIT_PRICE,
expiration_timestamp_secs: None,
chain_id: None,
}
}
#[must_use]
pub fn sender(mut self, address: AccountAddress) -> Self {
self.sender_address = Some(address);
self
}
#[must_use]
pub fn sequence_number(mut self, sequence_number: u64) -> Self {
self.sequence_number = Some(sequence_number);
self
}
#[must_use]
pub fn secondary_signer(mut self, address: AccountAddress) -> Self {
self.secondary_addresses.push(address);
self
}
#[must_use]
pub fn secondary_signers(mut self, addresses: &[AccountAddress]) -> Self {
self.secondary_addresses.extend(addresses);
self
}
#[must_use]
pub fn fee_payer(mut self, address: AccountAddress) -> Self {
self.fee_payer_address = Some(address);
self
}
#[must_use]
pub fn payload(mut self, payload: TransactionPayload) -> Self {
self.payload = Some(payload);
self
}
#[must_use]
pub fn max_gas_amount(mut self, max_gas_amount: u64) -> Self {
self.max_gas_amount = max_gas_amount;
self
}
#[must_use]
pub fn gas_unit_price(mut self, gas_unit_price: u64) -> Self {
self.gas_unit_price = gas_unit_price;
self
}
#[must_use]
pub fn expiration_timestamp_secs(mut self, expiration_timestamp_secs: u64) -> Self {
self.expiration_timestamp_secs = Some(expiration_timestamp_secs);
self
}
#[must_use]
pub fn expiration_from_now(mut self, seconds: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.expiration_timestamp_secs = Some(now + seconds);
self
}
#[must_use]
pub fn chain_id(mut self, chain_id: ChainId) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn build(self) -> AptosResult<FeePayerRawTransaction> {
let sender = self
.sender_address
.ok_or_else(|| AptosError::transaction("sender is required"))?;
let sequence_number = self
.sequence_number
.ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
let payload = self
.payload
.ok_or_else(|| AptosError::transaction("payload is required"))?;
let chain_id = self
.chain_id
.ok_or_else(|| AptosError::transaction("chain_id is required"))?;
let fee_payer_address = self
.fee_payer_address
.ok_or_else(|| AptosError::transaction("fee_payer is required"))?;
let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.saturating_add(DEFAULT_EXPIRATION_SECONDS)
});
let raw_txn = RawTransaction::new(
sender,
sequence_number,
payload,
self.max_gas_amount,
self.gas_unit_price,
expiration_timestamp_secs,
chain_id,
);
Ok(FeePayerRawTransaction {
raw_txn,
secondary_signer_addresses: self.secondary_addresses,
fee_payer_address,
})
}
pub fn build_and_sign<S, F>(
self,
sender: &S,
secondary_signers: &[&dyn Account],
fee_payer: &F,
) -> AptosResult<SignedTransaction>
where
S: Account,
F: Account,
{
let fee_payer_txn = self.build()?;
sign_sponsored_transaction(&fee_payer_txn, sender, secondary_signers, fee_payer)
}
}
pub fn sign_sponsored_transaction<S, F>(
fee_payer_txn: &FeePayerRawTransaction,
sender: &S,
secondary_signers: &[&dyn Account],
fee_payer: &F,
) -> AptosResult<SignedTransaction>
where
S: Account,
F: Account,
{
let signing_message = fee_payer_txn.signing_message()?;
let sender_signature = sender.sign(&signing_message)?;
let sender_public_key = sender.public_key_bytes();
let sender_auth = make_account_authenticator(
sender.signature_scheme(),
sender_public_key,
sender_signature,
)?;
let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
for signer in secondary_signers {
let signature = signer.sign(&signing_message)?;
let public_key = signer.public_key_bytes();
secondary_auths.push(make_account_authenticator(
signer.signature_scheme(),
public_key,
signature,
)?);
}
let fee_payer_signature = fee_payer.sign(&signing_message)?;
let fee_payer_public_key = fee_payer.public_key_bytes();
let fee_payer_auth = make_account_authenticator(
fee_payer.signature_scheme(),
fee_payer_public_key,
fee_payer_signature,
)?;
let authenticator = TransactionAuthenticator::fee_payer(
sender_auth,
fee_payer_txn.secondary_signer_addresses.clone(),
secondary_auths,
fee_payer_txn.fee_payer_address,
fee_payer_auth,
);
Ok(SignedTransaction::new(
fee_payer_txn.raw_txn.clone(),
authenticator,
))
}
fn make_account_authenticator(
scheme: u8,
public_key: Vec<u8>,
signature: Vec<u8>,
) -> AptosResult<AccountAuthenticator> {
match scheme {
crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
public_key,
signature,
}),
crate::crypto::SINGLE_KEY_SCHEME => {
Ok(AccountAuthenticator::single_key(public_key, signature))
}
crate::crypto::MULTI_KEY_SCHEME => {
Ok(AccountAuthenticator::multi_key(public_key, signature))
}
_ => Err(AptosError::InvalidSignature(format!(
"unknown signature scheme: {scheme}"
))),
}
}
#[derive(Debug, Clone)]
pub struct PartiallySigned {
pub fee_payer_txn: FeePayerRawTransaction,
pub sender_auth: Option<AccountAuthenticator>,
pub secondary_auths: Vec<Option<AccountAuthenticator>>,
pub fee_payer_auth: Option<AccountAuthenticator>,
}
impl PartiallySigned {
pub fn new(fee_payer_txn: FeePayerRawTransaction) -> Self {
let num_secondary = fee_payer_txn.secondary_signer_addresses.len();
Self {
fee_payer_txn,
sender_auth: None,
secondary_auths: vec![None; num_secondary],
fee_payer_auth: None,
}
}
pub fn sign_as_sender<A: Account>(&mut self, sender: &A) -> AptosResult<()> {
let signing_message = self.fee_payer_txn.signing_message()?;
let signature = sender.sign(&signing_message)?;
let public_key = sender.public_key_bytes();
self.sender_auth = Some(make_account_authenticator(
sender.signature_scheme(),
public_key,
signature,
)?);
Ok(())
}
pub fn sign_as_secondary<A: Account>(&mut self, index: usize, signer: &A) -> AptosResult<()> {
if index >= self.secondary_auths.len() {
return Err(AptosError::transaction(format!(
"secondary signer index {} out of bounds (max {})",
index,
self.secondary_auths.len()
)));
}
let signing_message = self.fee_payer_txn.signing_message()?;
let signature = signer.sign(&signing_message)?;
let public_key = signer.public_key_bytes();
self.secondary_auths[index] = Some(make_account_authenticator(
signer.signature_scheme(),
public_key,
signature,
)?);
Ok(())
}
pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> AptosResult<()> {
let signing_message = self.fee_payer_txn.signing_message()?;
let signature = fee_payer.sign(&signing_message)?;
let public_key = fee_payer.public_key_bytes();
self.fee_payer_auth = Some(make_account_authenticator(
fee_payer.signature_scheme(),
public_key,
signature,
)?);
Ok(())
}
pub fn is_complete(&self) -> bool {
self.sender_auth.is_some()
&& self.fee_payer_auth.is_some()
&& self.secondary_auths.iter().all(Option::is_some)
}
pub fn finalize(self) -> AptosResult<SignedTransaction> {
let sender_auth = self
.sender_auth
.ok_or_else(|| AptosError::transaction("missing sender signature"))?;
let fee_payer_auth = self
.fee_payer_auth
.ok_or_else(|| AptosError::transaction("missing fee payer signature"))?;
let secondary_auths: Result<Vec<_>, _> = self
.secondary_auths
.into_iter()
.enumerate()
.map(|(i, auth)| {
auth.ok_or_else(|| {
AptosError::transaction(format!("missing secondary signer {i} signature"))
})
})
.collect();
let secondary_auths = secondary_auths?;
let authenticator = TransactionAuthenticator::fee_payer(
sender_auth,
self.fee_payer_txn.secondary_signer_addresses.clone(),
secondary_auths,
self.fee_payer_txn.fee_payer_address,
fee_payer_auth,
);
Ok(SignedTransaction::new(
self.fee_payer_txn.raw_txn,
authenticator,
))
}
}
pub trait Sponsor: Account + Sized {
fn sponsor<S: Account>(
&self,
sender: &S,
sender_sequence_number: u64,
payload: TransactionPayload,
chain_id: ChainId,
) -> AptosResult<SignedTransaction> {
SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(sender_sequence_number)
.fee_payer(self.address())
.payload(payload)
.chain_id(chain_id)
.build_and_sign(sender, &[], self)
}
fn sponsor_with_gas<S: Account>(
&self,
sender: &S,
sender_sequence_number: u64,
payload: TransactionPayload,
chain_id: ChainId,
max_gas_amount: u64,
gas_unit_price: u64,
) -> AptosResult<SignedTransaction> {
SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(sender_sequence_number)
.fee_payer(self.address())
.payload(payload)
.chain_id(chain_id)
.max_gas_amount(max_gas_amount)
.gas_unit_price(gas_unit_price)
.build_and_sign(sender, &[], self)
}
}
impl<A: Account + Sized> Sponsor for A {}
pub fn sponsor_transaction<S, F>(
sender: &S,
sender_sequence_number: u64,
fee_payer: &F,
payload: TransactionPayload,
chain_id: ChainId,
) -> AptosResult<SignedTransaction>
where
S: Account,
F: Account,
{
SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(sender_sequence_number)
.fee_payer(fee_payer.address())
.payload(payload)
.chain_id(chain_id)
.build_and_sign(sender, &[], fee_payer)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transaction::payload::EntryFunction;
#[test]
fn test_builder_missing_sender() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let result = SponsoredTransactionBuilder::new()
.sequence_number(0)
.fee_payer(AccountAddress::ONE)
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("sender"));
}
#[test]
fn test_builder_missing_fee_payer() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let result = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("fee_payer"));
}
#[test]
fn test_builder_complete() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(5)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(payload.into())
.chain_id(ChainId::testnet())
.max_gas_amount(100_000)
.gas_unit_price(150)
.build()
.unwrap();
assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
assert_eq!(
fee_payer_txn.fee_payer_address,
AccountAddress::from_hex("0x3").unwrap()
);
}
#[test]
fn test_partially_signed_completion_check() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let partially_signed = PartiallySigned::new(fee_payer_txn);
assert!(!partially_signed.is_complete());
}
#[test]
fn test_partially_signed_finalize_incomplete() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let partially_signed = PartiallySigned::new(fee_payer_txn);
let result = partially_signed.finalize();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("missing"));
}
#[cfg(feature = "ed25519")]
#[test]
fn test_full_sponsored_transaction() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let signed_txn = SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(0)
.fee_payer(fee_payer.address())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build_and_sign(&sender, &[], &fee_payer)
.unwrap();
assert_eq!(signed_txn.raw_txn.sender, sender.address());
assert!(matches!(
signed_txn.authenticator,
TransactionAuthenticator::FeePayer { .. }
));
}
#[cfg(feature = "ed25519")]
#[test]
fn test_sponsor_trait() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let sponsor = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let signed_txn = sponsor
.sponsor(&sender, 0, payload.into(), ChainId::testnet())
.unwrap();
assert_eq!(signed_txn.raw_txn.sender, sender.address());
}
#[cfg(feature = "ed25519")]
#[test]
fn test_sponsor_transaction_fn() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let signed_txn =
sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
.unwrap();
assert_eq!(signed_txn.raw_txn.sender, sender.address());
}
#[cfg(feature = "ed25519")]
#[test]
fn test_partially_signed_flow() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(0)
.fee_payer(fee_payer.address())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let mut partially_signed = PartiallySigned::new(fee_payer_txn);
assert!(!partially_signed.is_complete());
partially_signed.sign_as_sender(&sender).unwrap();
assert!(!partially_signed.is_complete());
partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
assert!(partially_signed.is_complete());
let signed_txn = partially_signed.finalize().unwrap();
assert_eq!(signed_txn.raw_txn.sender, sender.address());
}
#[test]
fn test_builder_missing_sequence_number() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let result = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("sequence_number"));
}
#[test]
fn test_builder_missing_payload() {
let result = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.chain_id(ChainId::testnet())
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("payload"));
}
#[test]
fn test_builder_missing_chain_id() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let result = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("chain_id"));
}
#[test]
fn test_builder_secondary_signers() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let secondary1 = AccountAddress::from_hex("0x4").unwrap();
let secondary2 = AccountAddress::from_hex("0x5").unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.secondary_signer(secondary1)
.secondary_signers(&[secondary2])
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.build()
.unwrap();
assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
}
#[test]
fn test_builder_expiration_timestamp() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let expiration = 1_234_567_890_u64;
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.expiration_timestamp_secs(expiration)
.build()
.unwrap();
assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
}
#[test]
fn test_builder_expiration_from_now() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(TransactionPayload::EntryFunction(
EntryFunction::apt_transfer(recipient, 1000).unwrap(),
))
.chain_id(ChainId::testnet())
.expiration_from_now(60)
.build()
.unwrap();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
}
#[test]
fn test_builder_default() {
let builder = SponsoredTransactionBuilder::default();
assert!(builder.sender_address.is_none());
assert!(builder.sequence_number.is_none());
assert!(builder.fee_payer_address.is_none());
assert!(builder.payload.is_none());
assert!(builder.chain_id.is_none());
}
#[test]
fn test_builder_new_defaults() {
let builder = SponsoredTransactionBuilder::new();
assert!(builder.sender_address.is_none());
assert!(builder.sequence_number.is_none());
assert!(builder.fee_payer_address.is_none());
assert!(builder.payload.is_none());
assert!(builder.chain_id.is_none());
assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
}
#[test]
fn test_builder_debug() {
let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
let debug = format!("{builder:?}");
assert!(debug.contains("SponsoredTransactionBuilder"));
}
#[cfg(feature = "ed25519")]
#[test]
fn test_partially_signed_with_secondary_signers() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let secondary = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(0)
.secondary_signer(secondary.address())
.fee_payer(fee_payer.address())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let mut partially_signed = PartiallySigned::new(fee_payer_txn);
assert!(!partially_signed.is_complete());
partially_signed.sign_as_sender(&sender).unwrap();
assert!(!partially_signed.is_complete());
partially_signed.sign_as_secondary(0, &secondary).unwrap();
assert!(!partially_signed.is_complete());
partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
assert!(partially_signed.is_complete());
let signed = partially_signed.finalize().unwrap();
assert_eq!(signed.raw_txn.sender, sender.address());
}
#[cfg(feature = "ed25519")]
#[test]
fn test_partially_signed_secondary_index_out_of_bounds() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let secondary = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(0)
.fee_payer(fee_payer.address())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let mut partially_signed = PartiallySigned::new(fee_payer_txn);
let result = partially_signed.sign_as_secondary(0, &secondary);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[cfg(feature = "ed25519")]
#[test]
fn test_partially_signed_finalize_missing_secondary() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let fee_payer = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(sender.address())
.sequence_number(0)
.secondary_signer(AccountAddress::from_hex("0x5").unwrap())
.fee_payer(fee_payer.address())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let mut partially_signed = PartiallySigned::new(fee_payer_txn);
partially_signed.sign_as_sender(&sender).unwrap();
partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
let result = partially_signed.finalize();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("secondary signer"));
}
#[cfg(feature = "ed25519")]
#[test]
fn test_sponsor_with_gas() {
use crate::account::Ed25519Account;
let sender = Ed25519Account::generate();
let sponsor = Ed25519Account::generate();
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let signed_txn = sponsor
.sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
.unwrap();
assert_eq!(signed_txn.raw_txn.sender, sender.address());
assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
}
#[test]
fn test_partially_signed_debug() {
let recipient = AccountAddress::from_hex("0x123").unwrap();
let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
let fee_payer_txn = SponsoredTransactionBuilder::new()
.sender(AccountAddress::ONE)
.sequence_number(0)
.fee_payer(AccountAddress::from_hex("0x3").unwrap())
.payload(payload.into())
.chain_id(ChainId::testnet())
.build()
.unwrap();
let partially_signed = PartiallySigned::new(fee_payer_txn);
let debug = format!("{partially_signed:?}");
assert!(debug.contains("PartiallySigned"));
}
}