trident-sdk 0.2.0

End-user SDK for Trident deposits and cross-chain spends
Documentation
//! EVM-specific off-chain signing helpers for Trident spend intents.
//!
//! These helpers cover the EIP-712 domain and digest scheme used by Trident's
//! EVM spend-intent signing flow. They are kept separate from
//! [`crate::evm::abi`], which focuses on contract bindings and payload types.

use alloy_primitives::keccak256;
use alloy_signer::Signer;
use alloy_sol_types::SolStruct;

use crate::evm::EvmChainError;
use crate::evm::abi::SpendIntent;
use crate::proto::v1;

/// Trident EIP-712 domain separator.
///
/// This matches the on-chain `TridentEIP712::DOMAIN_SEPARATOR` constant and
/// intentionally omits `chainId` and `verifyingContract` for cross-chain reuse.
pub const TRIDENT_DOMAIN_SEPARATOR: alloy_primitives::B256 =
    alloy_primitives::b256!("c3e730f724f9f24a93c08888f26f828931ae0eeeb4f19c5469c4e56049b5b567");

/// EIP-712 version byte prefix: `\x19\x01`.
const EIP712_PREFIX: [u8; 2] = [0x19, 0x01];

/// Signs a spend intent using Trident's EIP-712 typed-data scheme.
pub async fn sign_spend_intent(
    signer: &(impl Signer + Sync + ?Sized),
    intent: v1::SpendIntent,
) -> Result<Vec<u8>, EvmChainError> {
    let abi_intent: SpendIntent = intent.try_into()?;
    let struct_hash = abi_intent.eip712_hash_struct();
    let digest = keccak256(
        [
            EIP712_PREFIX.as_slice(),
            TRIDENT_DOMAIN_SEPARATOR.as_slice(),
            struct_hash.as_slice(),
        ]
        .concat(),
    );
    let signature = signer
        .sign_hash(&digest)
        .await
        .map_err(EvmChainError::Signer)?;
    Ok(signature.as_bytes().to_vec())
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_primitives::{Address, U256};
    use alloy_signer_local::PrivateKeySigner;

    fn sample_proto_intent(src_spender: Address) -> v1::SpendIntent {
        v1::SpendIntent {
            spec: Some(v1::SpendSpec {
                src_spender: Some(src_spender.into()),
                src_domain: 1,
                src_token: Some(Address::from([0x02; 20]).into()),
                src_deposit_contract: Some(Address::from([0x03; 20]).into()),
                dst_domain: 2,
                dst_token: Some(Address::from([0x04; 20]).into()),
                dst_delivery_contract: Some(Address::from([0x05; 20]).into()),
                dst_recipient: Some(Address::from([0x06; 20]).into()),
                dst_caller: Some(Address::from([0x07; 20]).into()),
                amount: Some(U256::from(1000u64).into()),
                salt: Some([0xAA; 32].into()),
            }),
            max_fee: Some(U256::from(10u64).into()),
            expiry_height: 1000,
        }
    }

    #[tokio::test]
    async fn sign_spend_intent_success() {
        let signer: PrivateKeySigner =
            "0x0123456789012345678901234567890123456789012345678901234567890123"
                .parse()
                .unwrap();
        let intent = sample_proto_intent(signer.address());

        let signature = sign_spend_intent(&signer, intent).await.unwrap();

        assert_eq!(signature.len(), 65);
    }
}