onemoney-protocol 0.18.0

Official Rust SDK for OneMoney Protocol - L1 blockchain network client
Documentation
//! Multi-signature account API endpoints.
//!
//! This module provides API methods for creating and managing multi-signature
//! accounts.

use alloy_primitives::{Address, SignatureError, U256};
use om_primitives_types::{
    core::types::Money,
    transaction::{
        MultiSigSignatureEntry, Signature, Signed,
        envelope::RawTransactionEnvelope,
        payload::{CreateMultiSigPayload, MultiSigSigner, PaymentPayload, TokenIssuePayload, TokenMintPayload},
    },
};

use crate::{
    Client, Error, Result,
    crypto::sign_transaction_payload,
    utils::{SignerConfig, ThresholdConfig, derive_multisig_address},
};

impl Client {
    /// Create a multi-signature account transaction payload.
    ///
    /// This method derives the multi-sig account address and creates the
    /// payload. The caller must sign this payload and submit via the
    /// standard transaction API.
    ///
    /// # Arguments
    /// * `signers` - List of authorized signers with their weights
    /// * `threshold` - Minimum total weight required for transaction approval
    /// * `chain_id` - Chain ID for the transaction
    /// * `nonce` - Transaction nonce (get from account state)
    ///
    /// # Returns
    /// Tuple of (multi-sig account address, unsigned transaction payload)
    ///
    /// # Errors
    /// Returns error if signer configuration is invalid (e.g., threshold
    /// exceeds total weight)
    ///
    /// # Example
    /// ```no_run
    /// use onemoney_protocol::{
    ///     Client,
    ///     NamedChain
    ///     utils::{SignerConfig, ThresholdConfig},
    /// };
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::testnet()?;
    ///
    /// let signer1 = SignerConfig::new(vec![2; 33], 1)?;
    /// let signer2 = SignerConfig::new(vec![3; 33], 1)?;
    /// let threshold = ThresholdConfig::new(2)?;
    ///
    /// let (_multisig_address, payload) = client.create_multisig_account_payload(
    ///     &[signer1, signer2],
    ///     &threshold,
    ///     NamedChain::MAINNET_CHAIN_ID, // chain_id
    ///     0,     // nonce
    /// )?;
    ///
    /// println!("Payload created: {:?}", payload);
    /// # Ok(())
    /// # }
    /// ```
    pub fn create_multisig_account_payload(
        &self,
        signers: &[SignerConfig],
        threshold: &ThresholdConfig,
        chain_id: u64,
        nonce: u64,
    ) -> Result<(Address, CreateMultiSigPayload)> {
        // Derive the multi-sig account address
        let multisig_address = derive_multisig_address(signers, threshold).map_err(|e| Error::InvalidParameter {
            parameter: "signers/threshold".to_string(),
            message: format!("Invalid multi-sig configuration: {}", e),
        })?;

        // Convert to wire format
        let wire_signers: Vec<MultiSigSigner> = signers
            .iter()
            .map(|s| MultiSigSigner {
                public_key: s.public_key.clone(),
                weight: s.weight,
            })
            .collect();

        // Create payload
        let payload = CreateMultiSigPayload {
            chain_id,
            nonce,
            signers: wire_signers,
            threshold: threshold.threshold,
        };

        Ok((multisig_address, payload))
    }

    /// Create and submit a multi-signature account creation transaction.
    ///
    /// This is a convenience method that creates the payload, signs it, and
    /// submits it.
    ///
    /// # Arguments
    /// * `signers` - List of authorized signers with their weights
    /// * `threshold` - Minimum total weight required for transaction approval
    /// * `chain_id` - Chain ID for the transaction
    /// * `nonce` - Transaction nonce (get from creator's account state)
    /// * `private_key` - Creator's private key (pays for account creation)
    ///
    /// # Returns
    /// Tuple of (multi-sig account address, transaction hash)
    pub async fn submit_create_multisig_account(
        &self,
        signers: &[SignerConfig],
        threshold: &ThresholdConfig,
        chain_id: u64,
        nonce: u64,
        private_key: &str,
    ) -> Result<(Address, om_rest_types::responses::TransactionResponse)> {
        // Create payload
        let (multisig_address, payload) = self.create_multisig_account_payload(signers, threshold, chain_id, nonce)?;

        // Sign the payload
        let rest_signature = sign_transaction_payload(&payload, private_key)?;
        let signature: Signature = rest_signature
            .try_into()
            .map_err(|e: SignatureError| Error::invalid_parameter("signature", e.to_string()))?;

        // Create signed transaction and envelope
        let signed_tx = Signed::new(payload, signature);
        let envelope = RawTransactionEnvelope::CreateMultiSig(signed_tx);

        let response = self.submit_raw_transaction(envelope).await?;

        Ok((multisig_address, response))
    }

    /// Submit a multi-signature payment transaction.
    ///
    /// This method assumes signatures have already been collected off-chain.
    pub async fn submit_multisig_payment_transaction(
        &self,
        payload: PaymentPayload,
        multisig_account: Address,
        signatures: Vec<MultiSigSignatureEntry>,
    ) -> Result<om_rest_types::responses::TransactionResponse> {
        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
        let envelope = RawTransactionEnvelope::Payment {
            signed: signed_tx,
            fee: Money::ZERO,
        };
        self.submit_raw_transaction(envelope).await
    }

    /// Submit a multi-signature token issue transaction.
    ///
    /// This method assumes signatures have already been collected off-chain.
    pub async fn submit_multisig_token_issue_transaction(
        &self,
        payload: TokenIssuePayload,
        multisig_account: Address,
        signatures: Vec<MultiSigSignatureEntry>,
    ) -> Result<om_rest_types::responses::TransactionResponse> {
        let mint_address = payload.derive_mint_address();
        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
        let envelope = RawTransactionEnvelope::TokenIssue(signed_tx, mint_address);
        self.submit_raw_transaction(envelope).await
    }

    /// Submit a multi-signature token mint transaction.
    ///
    /// This method assumes signatures have already been collected off-chain.
    pub async fn submit_multisig_token_mint_transaction(
        &self,
        payload: TokenMintPayload,
        multisig_account: Address,
        signatures: Vec<MultiSigSignatureEntry>,
    ) -> Result<om_rest_types::responses::TransactionResponse> {
        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
        let envelope = RawTransactionEnvelope::TokenMint(signed_tx);
        self.submit_raw_transaction(envelope).await
    }

    /// Create a multi-signature payment transaction.
    ///
    /// This helper creates a payment transaction payload that needs to be
    /// signed by multiple signers of a multi-sig account.
    ///
    /// # Workflow
    /// 1. Create payment payload with this method
    /// 2. Each signer signs the payload independently (offline signing
    ///    supported)
    /// 3. Collect signatures using `MultiSigSignatureCollector`
    /// 4. Create signed transaction with `Signed::new_multi_sig()`
    /// 5. Submit transaction
    ///
    /// # Arguments
    /// * `recipient` - Recipient address
    /// * `token` - Token mint address
    /// * `amount` - Amount to send
    /// * `chain_id` - Chain ID
    /// * `nonce` - Multi-sig account's nonce
    ///
    /// # Returns
    /// Unsigned payment payload ready for signing
    ///
    /// # Example
    /// ```no_run
    /// use alloy_primitives::{Address, U256};
    /// use onemoney_protocol::{
    ///     Client, NamedChain, crypto::sign_multisig_transaction_payload,
    ///     utils::MultiSigSignatureCollector,
    /// };
    ///
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = Client::testnet()?;
    ///
    /// // Create payment payload
    /// let multisig_account = Address::ZERO; // Your multi-sig account
    /// let recipient = Address::ZERO;
    /// let payload = client.create_multisig_payment_payload(
    ///     recipient,
    ///     Address::repeat_byte(1),
    ///     U256::from(1000),
    ///     NamedChain::MAINNET_CHAIN_ID, // chain_id
    ///     5,                            // nonce
    /// );
    ///
    /// // Collect signatures from signers
    /// let mut collector = MultiSigSignatureCollector::new();
    ///
    /// // Signer 1 signs (can be offline)
    /// let sig1 =
    ///     sign_multisig_transaction_payload(&payload, multisig_account, "signer1_private_key")?;
    /// collector.add_signature(signer1_pubkey, sig1);
    ///
    /// // Signer 2 signs (can be offline)
    /// let sig2 =
    ///     sign_multisig_transaction_payload(&payload, multisig_account, "signer2_private_key")?;
    /// collector.add_signature(signer2_pubkey, sig2);
    ///
    /// // Create multi-sig transaction
    /// let signatures = collector.signatures();
    /// let signed_tx = om_primitives_types::transaction::Signed::new_multi_sig(
    ///     payload,
    ///     multisig_account,
    ///     signatures,
    /// );
    ///
    /// // Submit via RawTransactionEnvelope::Payment...
    /// # Ok(())
    /// # }
    /// ```
    pub fn create_multisig_payment_payload(
        &self,
        recipient: Address,
        token: Address,
        amount: U256,
        chain_id: u64,
        nonce: u64,
    ) -> PaymentPayload {
        PaymentPayload {
            chain_id,
            nonce,
            token,
            recipient,
            value: amount,
        }
    }
}

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

    #[test]
    fn test_create_multisig_account_payload() {
        let client = Client::local().unwrap();

        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
        let signer2 = SignerConfig::new(vec![3; 33], 1).unwrap();
        let threshold = ThresholdConfig::new(2).unwrap();

        let result = client.create_multisig_account_payload(&[signer1, signer2], &threshold, 1, 0);
        assert!(result.is_ok());

        let (address, payload) = result.unwrap();
        assert_eq!(payload.signers.len(), 2);
        assert_eq!(payload.threshold, 2);
        assert_ne!(address, Address::ZERO);
    }

    #[test]
    fn test_create_multisig_account_invalid_threshold() {
        let client = Client::local().unwrap();

        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
        let threshold = ThresholdConfig::new(2).unwrap(); // Exceeds total weight

        let result = client.create_multisig_account_payload(&[signer1], &threshold, 1, 0);
        assert!(result.is_err());
    }

    #[test]
    fn test_create_multisig_payment_payload() {
        use alloy_primitives::U256;

        let client = Client::local().unwrap();

        let recipient = Address::ZERO;
        let payload = client.create_multisig_payment_payload(
            recipient,
            Address::ZERO,
            U256::from(1000),
            NamedChain::TESTNET_CHAIN_ID,
            5,
        );

        assert_eq!(payload.chain_id, NamedChain::TESTNET_CHAIN_ID);
        assert_eq!(payload.nonce, 5);
        assert_eq!(payload.recipient, recipient);
        assert_eq!(payload.value, U256::from(1000));
    }
}