substrate-stellar-sdk 0.3.0

A Substrate compatible SDK for Stellar
Documentation
//! Transaction envelopes and signatures

use crate::{
    types::{
        FeeBumpTransactionEnvelope, FeeBumpTransactionExt, FeeBumpTransactionInnerTx, Memo, MuxedAccount, Operation,
        Preconditions, TimeBounds, Transaction, TransactionEnvelope, TransactionExt, TransactionV0, TransactionV0Ext,
        TransactionV1Envelope,
    },
    xdr::compound_types::LimitedVarArray,
    FeeBumpTransaction, IntoAmount, IntoMuxedAccountId, StellarSdkError, BASE_FEE_STROOPS,
};

// fee is not fee per operation but total fee
// sequence_number must be 1 + current account sequence numbere
impl Transaction {
    pub fn new<T: IntoMuxedAccountId>(
        source_account: T,
        sequence_number: i64,
        fee_per_operation: Option<u32>,
        preconditions: Preconditions,
        memo: Option<Memo>,
    ) -> Result<Self, StellarSdkError> {
        let transaction = Self {
            source_account: source_account.into_muxed_account_id()?,
            fee: fee_per_operation.unwrap_or(BASE_FEE_STROOPS),
            seq_num: sequence_number,
            cond: preconditions,
            memo: memo.unwrap_or(Memo::MemoNone),
            operations: LimitedVarArray::new_empty(),
            ext: TransactionExt::V0,
        };

        Ok(transaction)
    }

    pub fn append_operation(&mut self, operation: Operation) -> Result<(), StellarSdkError> {
        self.operations.push(operation)
    }

    // careful: this operation also multiplies the fees with the number of operations
    pub fn into_transaction_envelope(mut self) -> TransactionEnvelope {
        self.fee = self.fee.checked_mul(self.operations.len() as u32).unwrap_or(self.fee);

        TransactionEnvelope::EnvelopeTypeTx(TransactionV1Envelope {
            tx: self,
            signatures: LimitedVarArray::new_empty(),
        })
    }
}

impl From<TransactionV0> for Transaction {
    fn from(transaction: TransactionV0) -> Self {
        let time_bounds = transaction.time_bounds.unwrap_or(TimeBounds::from_time_points((), ()));

        Self {
            source_account: MuxedAccount::KeyTypeEd25519(transaction.source_account_ed25519),
            fee: transaction.fee,
            seq_num: transaction.seq_num,
            cond: Preconditions::PrecondTime(time_bounds),
            memo: transaction.memo,
            operations: transaction.operations,
            ext: match transaction.ext {
                TransactionV0Ext::V0 => TransactionExt::V0,
                TransactionV0Ext::Default(default) => TransactionExt::Default(default),
            },
        }
    }
}

impl FeeBumpTransaction {
    pub fn new<T: IntoMuxedAccountId, S: IntoAmount>(
        fee_source: T,
        new_total_fee: S,
        inner_transaction_envelope: TransactionEnvelope,
    ) -> Result<Self, StellarSdkError> {
        let v1_envelope = match inner_transaction_envelope {
            TransactionEnvelope::EnvelopeTypeTxV0(envelope) =>
                TransactionV1Envelope { tx: envelope.tx.into(), signatures: envelope.signatures },
            TransactionEnvelope::EnvelopeTypeTx(envelope) => envelope,
            TransactionEnvelope::EnvelopeTypeTxFeeBump(_) => return Err(StellarSdkError::CantWrapFeeBumpTransaction),
            TransactionEnvelope::Default(_) => unreachable!(),
        };

        let transaction = Self {
            fee_source: fee_source.into_muxed_account_id()?,
            fee: new_total_fee.into_stroop_amount(false)?,
            inner_tx: FeeBumpTransactionInnerTx::EnvelopeTypeTx(v1_envelope),
            ext: FeeBumpTransactionExt::V0,
        };

        Ok(transaction)
    }

    pub fn into_transaction_envelope(self) -> TransactionEnvelope {
        TransactionEnvelope::EnvelopeTypeTxFeeBump(FeeBumpTransactionEnvelope {
            tx: self,
            signatures: LimitedVarArray::new_empty(),
        })
    }
}

#[cfg(test)]
mod test {
    use crate::{
        network::TEST_NETWORK, types::Preconditions, Asset, IntoSecretKey, Memo, MilliSecondEpochTime, Operation,
        Price, SecondEpochTime, StroopAmount, TimeBounds, Transaction, TransactionEnvelope, XdrCodec,
    };

    const ACCOUNT_ID1: &str = "GDGRDTRINPF66FNC47H22NY6BNWMCD5Q4XZTVA2KG7PFZ64WHRIU62TQ";
    const ACCOUNT_ID2: &str = "GBNKQVTFRP25TIQRODMU5GJGSXDKHCEUDN7LNMOS5PNM427LMR77NV4M";
    const ACCOUNT_ID3: &str = "GCACWDM2VEYTXGUI3CUYLBJ453IBEPQ3XEJKA772ARAP5XDQ4NMGFZGJ";

    const SIGNER1: &str = "SCVKZEONBSU3XD6OTHXGAP6BTEWHOU4RPZQZJJ5AVAGPXUZ5A4D7MU6S";
    const SIGNER3: &str = "SDOKV37I4TI655LMEMDQFOWESJ3LK6DDFKIVTYKN4YYTSAYFIBPP7MYI";

    #[test]
    fn build_transaction1() {
        let mut transaction = Transaction::new(
            ACCOUNT_ID1,
            1980190376853505,
            Some(100),
            Preconditions::PrecondTime(TimeBounds::from_time_points(SecondEpochTime(0), SecondEpochTime(1626258131))),
            None,
        )
        .unwrap();

        transaction
            .append_operation(Operation::new_payment(ACCOUNT_ID2, Asset::native(), "123.456").unwrap())
            .unwrap();

        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaL\
            nz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAGQABwj5AAAAAQAAAAEA\
            AAAAAAAAAAAAAABg7rrTAAAAAAAAAAEAAAAAAAAAAQAAAABaqFZli\
            /XZohFw2U6ZJpXGo4iUG362sdLr2s5r62R/9gAAAAAAAAAASZXkAA\
            AAAAAAAAAA";
        let envelope = transaction.into_transaction_envelope();

        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
        assert_eq!(envelope.to_base64_xdr(), expexted_base64);
    }

    #[test]
    fn build_transaction2() {
        let mut transaction = Transaction::new(
            ACCOUNT_ID1,
            1980190376853505,
            Some(321),
            Preconditions::PrecondTime(TimeBounds::from_time_points(SecondEpochTime(0), SecondEpochTime(0))),
            Some(Memo::from_text_memo("Hello World!").unwrap()),
        )
        .unwrap();

        transaction
            .append_operation(
                Operation::new_payment(
                    ACCOUNT_ID2,
                    Asset::from_asset_code("USD", ACCOUNT_ID3).unwrap(),
                    StroopAmount(1234560000),
                )
                .unwrap(),
            )
            .unwrap();

        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAUEABwj5AAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAABAAAAAAAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAA=";
        let envelope = transaction.into_transaction_envelope();

        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
        assert_eq!(envelope.to_base64_xdr(), expexted_base64);
    }

    #[test]
    fn build_transaction3() {
        let mut transaction = Transaction::new(
            ACCOUNT_ID1,
            1980190376853505,
            Some(321),
            Preconditions::PrecondTime(TimeBounds::from_time_points(
                SecondEpochTime(162620000),
                MilliSecondEpochTime(1626263454_000),
            )),
            Some(Memo::from_text_memo("Hello World!").unwrap()),
        )
        .unwrap();

        transaction
            .append_operation(
                Operation::new_payment(
                    ACCOUNT_ID2,
                    Asset::from_asset_code("USD", ACCOUNT_ID3).unwrap(),
                    StroopAmount(1234560000),
                )
                .unwrap()
                .set_source_account(ACCOUNT_ID3)
                .unwrap(),
            )
            .unwrap();

        transaction
            .append_operation(
                Operation::new_manage_sell_offer(
                    Asset::from_asset_code("DOMINATION", ACCOUNT_ID2).unwrap(),
                    Asset::native(),
                    "152.103",
                    Price::from_float(4.58).unwrap(),
                    Some(123456789),
                )
                .unwrap(),
            )
            .unwrap();

        let expexted_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAoIABwj5AAAAAQAAAAEAAAAACbFiYAAAAABg7s+eAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAACAAAAAQAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAMAAAACRE9NSU5BVElPTgAAAAAAAFqoVmWL9dmiEXDZTpkmlcajiJQbfrax0uvazmvrZH/2AAAAAAAAAABaqRNwAAAA5QAAADIAAAAAB1vNFQAAAAAAAAAA";
        let mut envelope = transaction.into_transaction_envelope();

        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_base64).unwrap());
        assert_eq!(envelope.to_base64_xdr(), expexted_base64);

        envelope
            .sign(&TEST_NETWORK, vec![&SIGNER1.into_secret_key().unwrap(), &SIGNER3.into_secret_key().unwrap()])
            .unwrap();

        let expexted_singed_base64 = b"AAAAAgAAAADNEc4oa8vvFaLnz603HgtswQ+w5fM6g0o33lz7ljxRTwAAAoIABwj5AAAAAQAAAAEAAAAACbFiYAAAAABg7s+eAAAAAQAAAAxIZWxsbyBXb3JsZCEAAAACAAAAAQAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAAEAAAAAWqhWZYv12aIRcNlOmSaVxqOIlBt+trHS69rOa+tkf/YAAAABVVNEAAAAAACAKw2aqTE7mojYqYWFPO7QEj4buRKgf/oEQP7ccONYYgAAAABJleQAAAAAAAAAAAMAAAACRE9NSU5BVElPTgAAAAAAAFqoVmWL9dmiEXDZTpkmlcajiJQbfrax0uvazmvrZH/2AAAAAAAAAABaqRNwAAAA5QAAADIAAAAAB1vNFQAAAAAAAAACljxRTwAAAEB0B8vODxIESpa9H9f4QkPtFHVg4Xjx2A9aTncJOkW6BW0i1AxZFgMvrzEb7nO5UnXRvCKnBmuhpvA76YivAXYGcONYYgAAAECesooI2hhhuoOcLcXB76L58vMOrFvPFqpIeG+/zzLZXz0XYU6aELdtDxLAhK8GZCIZwlXdJ/RyZF9/2YzqbPMC";
        assert_eq!(envelope, TransactionEnvelope::from_base64_xdr(expexted_singed_base64).unwrap());
        assert_eq!(envelope.to_base64_xdr(), expexted_singed_base64);
    }
}