trident_sdk/
client.rs

1use crate::eips::eip712::EIP712;
2use crate::eips::eip7597::{EIP7597, EIP7597PermitMessage};
3use crate::eips::erc20::IERC20;
4use crate::error::Error;
5use crate::types::{
6    DepositInEvmChainWithApprovalResponse, EvmToEvmSpendRequest, EvmToEvmSpendResponse,
7};
8use alloy_primitives::{Address, Bytes, Signature, U256};
9use alloy_provider::{Network, Provider};
10use alloy_sol_types::{Eip712Domain, sol};
11use rand_core::TryRngCore;
12use std::collections::{HashMap, HashSet};
13use tides_core_types::TidesChainId;
14use tonic::codegen::StdError;
15use tonic::transport::{Channel, Endpoint};
16use trident_proto_types::tides::core::v1::ChainType;
17use trident_proto_types::trident::attester::v1::attester_service_client::AttesterServiceClient;
18use trident_proto_types::trident::attester::v1::{GetChainTokenMappingsRequest, SpendRequest};
19use trident_proto_types::trident::attester::v1::{
20    GetDeploymentsRequest, GetLastFinalizedBlockRequest, GetSupportedDepositTokensRequest,
21};
22use trident_proto_types::trident::core::v1::{SpendIntentSigned, TridentDeployment};
23use trident_types::core::{FixedBytes32, SpendIntent, SpendSpec};
24use trident_types::evm::abi::{TridentDelivery, TridentDeposits};
25
26/// Client for interacting with Trident attester services and EVM contracts.
27/// Maintains deployments, supported tokens, and exposes high-level operations.
28pub struct TridentClient {
29    deployments: HashMap<TidesChainId, TridentDeployment>,
30    supported_tokens: HashMap<TidesChainId, HashSet<Address>>,
31    // Expressed as percentage points.
32    max_block_height_buffer_percentage: u64,
33    /// gRPC client to the Trident Attester service.
34    pub attester: AttesterServiceClient<Channel>,
35}
36
37impl TridentClient {
38    /// Default buffer percentage applied to the minimum validity blocks
39    /// when suggesting a spend intent's max block height.
40    pub const DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE: u64 = 10;
41    /// Connects to the attester endpoint, loads deployments and supported deposit tokens.
42    pub async fn connect<D>(endpoint: D) -> Result<Self, Error>
43    where
44        D: TryInto<Endpoint>,
45        D::Error: Into<StdError>,
46    {
47        let mut attester = AttesterServiceClient::connect(endpoint).await?;
48
49        // get deployments
50        let deployments: HashMap<TidesChainId, TridentDeployment> = attester
51            .get_deployments(GetDeploymentsRequest {})
52            .await?
53            .into_inner()
54            .deployments
55            .into_iter()
56            .map(|d| Ok((d.chain_id, d.deployment.ok_or(Error::InvalidService)?)))
57            .collect::<Result<_, Error>>()?;
58
59        // get supported deposit tokens per deployment/chain
60        let mut supported_tokens: HashMap<TidesChainId, HashSet<Address>> = HashMap::new();
61        for (chain_id, _) in deployments.iter() {
62            let resp = attester
63                .get_supported_deposit_tokens(GetSupportedDepositTokensRequest {
64                    chain_id: *chain_id,
65                })
66                .await?
67                .into_inner();
68            for t in resp.tokens.into_iter() {
69                if t.address.len() == 32 {
70                    let addr = Address::from_slice(&t.address[12..]);
71                    supported_tokens.entry(*chain_id).or_default().insert(addr);
72                }
73            }
74        }
75
76        Ok(Self {
77            attester,
78            deployments,
79            supported_tokens,
80            max_block_height_buffer_percentage: Self::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
81        })
82    }
83
84    pub async fn connect_testnet() -> Result<Self, Error> {
85        Self::connect(crate::envs::testnet::trident::ENDPOINT).await
86    }
87
88    /// Sets the buffer percentage applied to the minimum validity blocks
89    /// when suggesting spend intent expirations.
90    pub fn with_max_block_height_buffer_percentage(mut self, buffer: u64) -> Self {
91        self.max_block_height_buffer_percentage = buffer;
92        self
93    }
94
95    /// Approves and deposits `amount` of `token` into the Trident deposits contract on an EVM chain.
96    /// Returns receipts for both the approval and deposit transactions.
97    pub async fn evm_deposit_with_approval<N: Network, P: Provider<N>>(
98        &self,
99        provider: P,
100        tides_chain_id: u32,
101        token: Address,
102        amount: U256,
103    ) -> Result<DepositInEvmChainWithApprovalResponse<N>, Error> {
104        let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
105        let approval_tx_receipt = self
106            .evm_approve_token(&provider, token, deposits_contract, amount)
107            .await?;
108        let deposit_tx_receipt = self
109            .evm_deposit_token(&provider, deposits_contract, token, amount)
110            .await?;
111
112        Ok(DepositInEvmChainWithApprovalResponse {
113            approval_tx_receipt,
114            deposit_tx_receipt,
115        })
116    }
117
118    fn validate_evm_deposit(
119        &self,
120        tides_chain_id: TidesChainId,
121        token: Address,
122    ) -> Result<Address, Error> {
123        let deployment = self.ensure_evm_chain(tides_chain_id)?;
124
125        self.ensure_supported_token(tides_chain_id, token)?;
126
127        let dep_addr_bytes = &deployment.trident_deposits_contract;
128        if dep_addr_bytes.len() != 32 {
129            return Err(Error::InvalidService);
130        }
131        Ok(Address::from_slice(&dep_addr_bytes[12..]))
132    }
133
134    async fn evm_approve_token<N: Network, P: Provider<N>>(
135        &self,
136        provider: &P,
137        token: Address,
138        spender: Address,
139        amount: U256,
140    ) -> Result<<N as Network>::ReceiptResponse, Error> {
141        let erc20 = IERC20::new(token, provider);
142        let tx = erc20.approve(spender, amount).send().await?;
143        Ok(tx.get_receipt().await?)
144    }
145
146    /// Submits a deposit transaction to the Trident deposits contract for the given ERC-20 `token` and `amount`.
147    pub async fn evm_deposit_token<N: Network, P: Provider<N>>(
148        &self,
149        provider: &P,
150        deposits_contract: Address,
151        token: Address,
152        amount: U256,
153    ) -> Result<<N as Network>::ReceiptResponse, Error> {
154        let deposits = TridentDeposits::new(deposits_contract, provider);
155        let tx = deposits.deposit(token, amount).send().await?;
156        Ok(tx.get_receipt().await?)
157    }
158
159    pub async fn evm_sign_and_deposit_with_permit<
160        N: Network,
161        P: Provider<N>,
162        S: alloy_signer::Signer + Send + Sync,
163    >(
164        &self,
165        provider: P,
166        mut signer: S,
167        tides_chain_id: u32,
168        token: Address,
169        amount: U256,
170        deadline: U256,
171    ) -> Result<N::ReceiptResponse, Error> {
172        let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
173        let owner = signer.address();
174
175        let signature = {
176            let domain = EIP712::query_domain(&provider, token).await?;
177            let nonce = EIP7597::get_nonce(&provider, token, owner).await?;
178            self.evm_sign_deposit_with_permit_request(
179                &mut signer,
180                domain,
181                deposits_contract,
182                amount,
183                nonce,
184                deadline,
185            )
186            .await?
187        };
188
189        let deposit_tx_receipt = self
190            .evm_deposit_with_permit(
191                &provider,
192                deposits_contract,
193                token,
194                owner,
195                amount,
196                deadline,
197                Bytes::from(signature.as_bytes()),
198            )
199            .await?;
200
201        Ok(deposit_tx_receipt)
202    }
203
204    pub async fn evm_sign_deposit_with_permit_request<S: alloy_signer::Signer + Send + Sync>(
205        &self,
206        mut signer: S,
207        domain: Eip712Domain,
208        spender: Address,
209        amount: U256,
210        nonce: U256,
211        deadline: U256,
212    ) -> Result<Signature, Error> {
213        let typed_data = EIP7597::permit_typed_data(
214            domain,
215            EIP7597PermitMessage {
216                owner: signer.address(),
217                spender,
218                value: amount,
219                nonce,
220                deadline,
221            },
222        );
223
224        let sig = EIP712::sign(typed_data, &mut signer).await?;
225
226        Ok(sig)
227    }
228
229    /// Deposit tokens with an EIP-7597 permit, passing the signature as bytes to allow for SCA deposits
230    #[allow(clippy::too_many_arguments)]
231    pub async fn evm_deposit_with_permit<N: Network, P: Provider<N>>(
232        &self,
233        provider: &P,
234        deposits_contract: Address,
235        token: Address,
236        owner: Address,
237        amount: U256,
238        deadline: U256,
239        signature: Bytes,
240    ) -> Result<<N as Network>::ReceiptResponse, Error> {
241        let deposits = TridentDeposits::new(deposits_contract, provider);
242        let tx = deposits
243            .depositWithPermit(token, owner, amount, deadline, signature)
244            .send()
245            .await?;
246        Ok(tx.get_receipt().await?)
247    }
248
249    /// Executes a spend attestation on the destination chain by calling `TridentDelivery.deliverToken`.
250    pub async fn evm_deliver<N: Network, P: Provider<N>>(
251        &mut self,
252        provider: &P,
253        destination_chain: TidesChainId,
254        attestation_payload: Vec<u8>,
255        signature: Vec<u8>,
256    ) -> Result<<N as Network>::ReceiptResponse, Error> {
257        // Ensure destination chain is EVM and fetch delivery contract
258        let dep = self.ensure_evm_chain(destination_chain)?;
259        let delivery_fb32 = FixedBytes32::try_from(dep.trident_delivery_contract.clone())?;
260        let delivery_addr = Address::try_from(delivery_fb32)?;
261
262        // Call deliverToken(attestationPayload, signature)
263        let delivery = TridentDelivery::new(delivery_addr, provider);
264        let tx = delivery
265            .deliverToken(attestation_payload.into(), signature.into())
266            .send()
267            .await?;
268        let receipt = tx.get_receipt().await?;
269        Ok(receipt)
270    }
271
272    /// Creates, signs, and submits an EVM-to-EVM spend intent, then triggers delivery on the destination chain.
273    /// Resolves destination token (if omitted), checks balances/fees, and returns the delivery receipt.
274    pub async fn evm_to_evm_spend<'a, DN: Network, S: alloy_signer::Signer, DP: Provider<DN>>(
275        &mut self,
276        req: EvmToEvmSpendRequest<'a, S, DP, DN>,
277    ) -> Result<EvmToEvmSpendResponse<DN>, Error> {
278        let signer = req.source_signer.address();
279
280        // Validate source and destination chains are known and EVM
281        self.ensure_evm_chain(req.source_chain_id)?;
282        self.ensure_evm_chain(req.destination_chain_id)?;
283
284        // Validate source token supported
285        self.ensure_supported_token(req.source_chain_id, req.source_token)?;
286
287        // Resolve destination token mapping
288        let dst_token = if let Some(tok) = req.destination_token {
289            tok
290        } else {
291            self.resolve_single_destination_token(
292                req.source_chain_id,
293                req.destination_chain_id,
294                req.source_token,
295            )
296            .await?
297        };
298
299        // Ensure destination token (resolved or provided) is supported on destination chain.
300        // This will be relevant only in case the user provides its own tokens.
301        self.ensure_supported_token(req.destination_chain_id, dst_token)?;
302
303        // make spend spec
304        let spend_spec = self.make_spend_spec(
305            req.source_chain_id,
306            signer,
307            req.source_token,
308            req.source_token_amount,
309            req.destination_chain_id,
310            dst_token,
311            req.destination_recipient,
312            req.destination_caller.unwrap_or(signer),
313            req.salt.map(Ok).unwrap_or_else(|| self.generate_salt())?,
314            req.destination_callback.map(|v| v.to_vec()),
315        )?;
316
317        // get suggested fee
318        let fees = match req.source_max_fees {
319            Some(fees) => fees,
320            None => {
321                self.get_suggested_fee(
322                    req.source_chain_id,
323                    req.source_token,
324                    req.source_token_amount,
325                    req.destination_chain_id,
326                    dst_token,
327                )
328                .await?
329            }
330        };
331
332        // ensure sufficient balance
333        self.ensure_sufficient_source_balance(
334            req.source_chain_id,
335            signer,
336            req.source_token,
337            fees + req.source_token_amount,
338        )
339        .await?;
340
341        // infer expiration height
342        let max_block_height = match req.max_block_height {
343            Some(max_block_height) => max_block_height,
344            None => {
345                self.suggest_spend_intent_max_block_height(req.source_chain_id)
346                    .await?
347            }
348        };
349
350        // make spend intent
351        let spend_intent = self.make_spend_intent(fees, max_block_height, spend_spec);
352
353        // sign spend intent
354        let signature = trident_types::evm::spend_intent_signing::sign_spend_intent(
355            req.source_signer,
356            &spend_intent,
357        )
358        .await?
359        .as_bytes()
360        .to_vec();
361
362        // send the spend intent to the attester
363        let spend_attestation_signed = self
364            .attester
365            .spend(SpendRequest {
366                intent: Some(SpendIntentSigned {
367                    intent: Some(spend_intent.clone().into()),
368                    signature,
369                }),
370            })
371            .await?
372            .into_inner()
373            .attestation
374            .expect("attester.spend.attestation");
375
376        // invoke the EVM contract for delivery now.
377        let delivery_tx_receipt = match self
378            .evm_deliver(
379                req.destination_provider,
380                req.destination_chain_id,
381                spend_attestation_signed.attestation_bytes.clone(),
382                spend_attestation_signed.signature.clone(),
383            )
384            .await
385        {
386            Ok(r) => r,
387            Err(e) => {
388                return Err(Error::EvmDeliveryFailed(Box::new(
389                    crate::error::EvmDeliveryFailed {
390                        spend_intent,
391                        attestation_payload: spend_attestation_signed.attestation_bytes,
392                        signature: spend_attestation_signed.signature,
393                        cause: Box::new(e),
394                    },
395                )));
396            }
397        };
398
399        Ok(EvmToEvmSpendResponse {
400            destination_token: dst_token,
401            applied_fees: fees,
402            max_block_height,
403            delivery_tx_receipt,
404        })
405    }
406
407    async fn resolve_single_destination_token(
408        &mut self,
409        source_chain_id: TidesChainId,
410        destination_chain_id: TidesChainId,
411        source_token: Address,
412    ) -> Result<Address, Error> {
413        let candidates: Vec<Address> = self
414            .resolve_destination_tokens::<Address, Address>(
415                source_chain_id,
416                destination_chain_id,
417                source_token,
418            )
419            .await?;
420
421        match candidates.len() {
422            0 => Err(Error::NoDestinationTokenMapping(
423                source_token,
424                source_chain_id,
425                destination_chain_id,
426            )),
427            1 => Ok(candidates[0]),
428            _ => Err(Error::AmbiguousDestinationTokenMapping(
429                source_token,
430                source_chain_id,
431                destination_chain_id,
432            )),
433        }
434    }
435
436    /// Resolves the given source token into destination token(s) for `destination_chain`.
437    /// `T1` must convert into `FixedBytes32`; `T2` must convert from `FixedBytes32`.
438    pub async fn resolve_destination_tokens<T1, T2>(
439        &mut self,
440        source_chain_id: TidesChainId,
441        destination_chain_id: TidesChainId,
442        source_token: T1,
443    ) -> Result<Vec<T2>, Error>
444    where
445        T1: Into<FixedBytes32>,
446        T2: TryFrom<FixedBytes32>,
447        <T2 as TryFrom<FixedBytes32>>::Error: core::fmt::Display,
448    {
449        if source_chain_id == destination_chain_id {
450            let fb32: FixedBytes32 = source_token.into();
451            let t2 = T2::try_from(fb32).map_err(|err: <T2 as TryFrom<FixedBytes32>>::Error| {
452                Error::TokenConversion(err.to_string())
453            })?;
454            return Ok(vec![t2]);
455        }
456
457        let mappings = self
458            .attester
459            .get_chain_token_mappings(GetChainTokenMappingsRequest {
460                chain_id: source_chain_id,
461            })
462            .await?
463            .into_inner()
464            .mappings;
465
466        // find the entry for the source token
467        let fb32: FixedBytes32 = source_token.into();
468        let fb32_bytes = fb32.to_vec();
469
470        let fb32s = mappings
471            .into_iter()
472            // get only source token.
473            .filter(|v| match &v.token {
474                None => false,
475                Some(v) => v.address == fb32_bytes,
476            })
477            // collect only mappings, and flatten them
478            .flat_map(|v| v.mappings)
479            // collect only for the given destination chain
480            .filter(|v| v.chain_id == destination_chain_id)
481            // convert all the destination tokens into FixedBytes32
482            .map(|v| Result::<_, Error>::Ok(FixedBytes32::try_from(v.address)?))
483            // collect
484            .collect::<Result<Vec<FixedBytes32>, Error>>()?;
485
486        // finally convert all the fixed bytes 32 into T2.
487        fb32s
488            .into_iter()
489            .map(|v| T2::try_from(v).map_err(|err| Error::TokenConversion(err.to_string())))
490            .collect()
491    }
492
493    /// Ensures the given `account` has at least `amount` of `token` available on `chain_id`.
494    async fn ensure_sufficient_source_balance(
495        &mut self,
496        chain_id: TidesChainId,
497        account: Address,
498        token: Address,
499        amount: U256,
500    ) -> Result<(), Error> {
501        let resp = self
502            .attester
503            .balances(
504                trident_proto_types::trident::attester::v1::BalancesRequest {
505                    chain_id,
506                    address: FixedBytes32::from(account).to_vec(),
507                },
508            )
509            .await?
510            .into_inner();
511
512        // Find available balance for the specified token
513        let mut available = U256::ZERO;
514        for b in resp.available_balances.into_iter() {
515            // token bytes are 32-byte left-padded address
516            if b.token.len() == 32 {
517                let fb = FixedBytes32::try_from(b.token)?;
518                let tok_addr = Address::try_from(fb)?;
519                if tok_addr == token {
520                    // amount is 32-byte big-endian U256
521                    let arr: [u8; 32] = b.amount.as_slice().try_into()?;
522                    available = U256::from_be_bytes(arr);
523                    break;
524                }
525            }
526        }
527
528        if available < amount {
529            return Err(Error::InsufficientBalance(amount, available));
530        }
531        Ok(())
532    }
533
534    #[allow(clippy::too_many_arguments)]
535    /// Builds a `SpendSpec` after validating the source and destination chains.
536    pub fn make_spend_spec(
537        &self,
538        source_chain: TidesChainId,
539        source_depositor: Address,
540        source_token: impl Into<FixedBytes32>,
541        source_token_amount: U256,
542        destination_chain: TidesChainId,
543        destination_token: impl Into<FixedBytes32>,
544        destination_recipient: impl Into<FixedBytes32>,
545        destination_caller: impl Into<FixedBytes32>,
546        salt: impl Into<FixedBytes32>,
547        callback_data: Option<Vec<u8>>,
548    ) -> Result<SpendSpec, Error> {
549        // Validate chains are known and EVM
550        let src_dep = self.ensure_evm_chain(source_chain)?;
551        let dst_dep = self.ensure_evm_chain(destination_chain)?;
552
553        // Build spec
554        let spec = SpendSpec {
555            version: 0,
556            source_chain,
557            source_contract: FixedBytes32::try_from(src_dep.trident_deposits_contract.clone())?,
558            source_token: source_token.into(),
559            source_token_amount,
560            source_depositor: FixedBytes32::from(source_depositor),
561            destination_chain,
562            destination_contract: FixedBytes32::try_from(
563                dst_dep.trident_delivery_contract.clone(),
564            )?,
565            destination_token: destination_token.into(),
566            destination_recipient: destination_recipient.into(),
567            destination_caller: destination_caller.into(),
568            salt: salt.into(),
569            callback_data: callback_data.unwrap_or_default(),
570        };
571
572        Ok(spec)
573    }
574
575    /// Wraps a `SpendSpec` into a `SpendIntent` with fee cap and expiration height.
576    pub fn make_spend_intent(
577        &self,
578        max_fee: U256,
579        max_block_height: u64,
580        spend_spec: SpendSpec,
581    ) -> SpendIntent {
582        SpendIntent {
583            max_fee: max_fee.try_into().unwrap(),
584            max_block_height,
585            spend_spec,
586        }
587    }
588
589    fn generate_salt(&self) -> Result<FixedBytes32, Error> {
590        let mut rng = rand::rngs::OsRng;
591        let mut buf = [0u8; 32];
592        rng.try_fill_bytes(&mut buf)?;
593        Ok(FixedBytes32::from(buf))
594    }
595
596    /// Suggests a source-side fee for the given route and amount. Placeholder returning zero for now.
597    pub async fn get_suggested_fee(
598        &mut self,
599        _source_chain: TidesChainId,
600        _source_token: impl Into<FixedBytes32>,
601        _source_token_amount: U256,
602        _destination_chain: TidesChainId,
603        _destination_token: impl Into<FixedBytes32>,
604    ) -> Result<U256, Error> {
605        // TODO: impl
606        Ok(U256::ZERO)
607    }
608
609    /// Suggests a spend intent's max block height using:
610    /// last finalized block + (min validity blocks + buffered min validity blocks).
611    pub async fn suggest_spend_intent_max_block_height(
612        &mut self,
613        src_chain_id: TidesChainId,
614    ) -> Result<u64, Error> {
615        // 1) Get the min required validity blocks from deployment.
616        let min_blocks = {
617            let dep = self.ensure_known_chain(src_chain_id)?;
618            dep.min_spend_intent_validity_blocks
619        };
620
621        // 2) Immediately apply buffer to the min required blocks
622        let pct = self.max_block_height_buffer_percentage as u128;
623        let buffer_blocks_u128 = (min_blocks as u128)
624            .checked_mul(pct)
625            .ok_or(Error::BlockHeightOverflow)?
626            .checked_div(100u128)
627            .ok_or(Error::BlockHeightOverflow)?;
628        let buffer_blocks: u64 =
629            u64::try_from(buffer_blocks_u128).map_err(|_| Error::BlockHeightOverflow)?;
630
631        let min_blocks_with_buffer = min_blocks
632            .checked_add(buffer_blocks)
633            .ok_or(Error::BlockHeightOverflow)?;
634
635        // 3) Query last finalized block for the source chain
636        let last_finalized = self
637            .attester
638            .get_last_finalized_block(GetLastFinalizedBlockRequest {
639                chain_id: src_chain_id,
640            })
641            .await?
642            .into_inner()
643            .last_finalized_block;
644
645        // 4) Suggested max block height = last_finalized + min_blocks_with_buffer
646        let suggested = last_finalized
647            .checked_add(min_blocks_with_buffer)
648            .ok_or(Error::BlockHeightOverflow)?;
649
650        Ok(suggested)
651    }
652
653    fn ensure_known_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
654        self.deployments
655            .get(&chain_id)
656            .ok_or(Error::UnknownChain(chain_id))
657    }
658
659    fn ensure_evm_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
660        let dep = self.ensure_known_chain(chain_id)?;
661        if dep.chain_type != (ChainType::Evm as i32) {
662            return Err(Error::UnsupportedChainType(dep.chain_type));
663        }
664        Ok(dep)
665    }
666
667    fn ensure_supported_token(&self, chain_id: TidesChainId, token: Address) -> Result<(), Error> {
668        let is_supported = self
669            .supported_tokens
670            .get(&chain_id)
671            .map(|set| set.contains(&token))
672            .unwrap_or(false);
673        if is_supported {
674            Ok(())
675        } else {
676            Err(Error::UnsupportedToken(token))
677        }
678    }
679
680    /// Faucets tokens by calling `mint(address,uint256)` on an open‑mint token.
681    /// `amount` is provided in the token's smallest unit.
682    pub async fn evm_faucet<N: Network, P: Provider<N>>(
683        &self,
684        provider: &P,
685        token: Address,
686        recipient: Address,
687        amount: U256,
688    ) -> Result<N::ReceiptResponse, Error> {
689        sol! {
690            #[sol(rpc)]
691            contract Faucet {
692                function mint(address, uint256);
693            }
694        }
695
696        // Call mint(to, amount)
697        let faucet = Faucet::new(token, provider);
698        let pending = faucet.mint(recipient, amount).send().await?;
699        let receipt = pending.get_receipt().await?;
700
701        Ok(receipt)
702    }
703
704    pub fn get_deployment_by_name(&self, name: impl AsRef<str>) -> Option<&TridentDeployment> {
705        let name = name.as_ref();
706        self.deployments.iter().find_map(|(_, value)| {
707            if value.chain_name.eq(name) {
708                Some(value)
709            } else {
710                None
711            }
712        })
713    }
714
715    pub async fn evm_balances(
716        &mut self,
717        chain_id: TidesChainId,
718        address: impl Into<Address>,
719    ) -> Result<HashMap<Address, U256>, Error> {
720        // ensures EVM chain.
721        self.ensure_evm_chain(chain_id)?;
722
723        // gets balances.
724        self.attester
725            .balances(
726                trident_proto_types::trident::attester::v1::BalancesRequest {
727                    chain_id,
728                    address: FixedBytes32::from(address.into()).to_vec(),
729                },
730            )
731            .await?
732            .into_inner()
733            .available_balances
734            .into_iter()
735            .map(|balance| {
736                let token: Address = FixedBytes32::try_from(balance.token)?.try_into()?;
737                let amount =
738                    U256::try_from_be_slice(&balance.amount).ok_or(Error::InvalidU256ByteSlice)?;
739                Ok((token, amount))
740            })
741            .collect()
742    }
743
744    pub async fn evm_balance(
745        &mut self,
746        chain_id: TidesChainId,
747        address: impl Into<Address>,
748        want: impl Into<Address>,
749    ) -> Result<U256, Error> {
750        let want = want.into();
751        self.evm_balances(chain_id, address).await.map(|v| {
752            v.into_iter()
753                .find_map(|(token, amount)| {
754                    if token == want {
755                        return Some(amount);
756                    }
757                    None
758                })
759                .unwrap_or_default()
760        })
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767    use alloy_primitives::Address;
768    use std::collections::HashMap;
769    use tonic::transport::Channel;
770    use trident_proto_types::tides::core::v1::ChainType;
771
772    fn make_fb32() -> Vec<u8> {
773        vec![0u8; 32]
774    }
775
776    fn make_deployment(chain_type: ChainType, chain_id: u32) -> TridentDeployment {
777        TridentDeployment {
778            chain_type: chain_type as i32,
779            chain_name: "ethereum".to_string(),
780            tides_chain_id: chain_id,
781            trident_deposits_contract: make_fb32(),
782            trident_claims_contract: make_fb32(),
783            trident_delivery_contract: make_fb32(),
784            min_spend_intent_validity_blocks: 10,
785            max_spend_attestation_validity_blocks: 20,
786            anchor_block: 1,
787        }
788    }
789
790    fn make_test_client(deployments: HashMap<TidesChainId, TridentDeployment>) -> TridentClient {
791        let attester =
792            AttesterServiceClient::new(Channel::from_static("http://localhost:1").connect_lazy());
793
794        TridentClient {
795            deployments,
796            supported_tokens: HashMap::new(),
797            max_block_height_buffer_percentage:
798                TridentClient::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
799            attester,
800        }
801    }
802
803    // ensure_known_chain tests (appears before ensure_evm_chain)
804    #[tokio::test]
805    async fn test_trident_client_ensure_known_chain_known_chain_success() {
806        let chain_id: TidesChainId = 1;
807        let mut deployments = HashMap::new();
808        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
809        let client = make_test_client(deployments);
810
811        let res = client.ensure_known_chain(chain_id);
812        assert!(res.is_ok());
813    }
814
815    #[tokio::test]
816    async fn test_trident_client_ensure_known_chain_unknown_chain_error() {
817        let client = make_test_client(HashMap::new());
818        let chain_id: TidesChainId = 42;
819
820        let res = client.ensure_known_chain(chain_id);
821        assert!(matches!(res, Err(Error::UnknownChain(id)) if id == chain_id));
822    }
823
824    // ensure_evm_chain tests
825    #[tokio::test]
826    async fn test_trident_client_ensure_evm_chain_evm_chain_success() {
827        let chain_id: TidesChainId = 5;
828        let mut deployments = HashMap::new();
829        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
830        let client = make_test_client(deployments);
831
832        let res = client.ensure_evm_chain(chain_id);
833        assert!(res.is_ok());
834    }
835
836    #[tokio::test]
837    async fn test_trident_client_ensure_evm_chain_unsupported_chain_type_error() {
838        let chain_id: TidesChainId = 7;
839        let mut deployments = HashMap::new();
840        deployments.insert(chain_id, make_deployment(ChainType::Unspecified, chain_id));
841        let client = make_test_client(deployments);
842
843        let res = client.ensure_evm_chain(chain_id);
844        assert!(
845            matches!(res, Err(Error::UnsupportedChainType(t)) if t == ChainType::Unspecified as i32)
846        );
847    }
848
849    // ensure_supported_token tests
850    #[tokio::test]
851    async fn test_trident_client_ensure_supported_token_supported_success() {
852        let chain_id: TidesChainId = 9;
853        let mut deployments = HashMap::new();
854        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
855        let mut client = make_test_client(deployments);
856
857        // insert token
858        let token = Address::from([0x11u8; 20]);
859        client
860            .supported_tokens
861            .entry(chain_id)
862            .or_default()
863            .insert(token);
864
865        let res = client.ensure_supported_token(chain_id, token);
866        assert!(matches!(res, Ok(())));
867    }
868
869    #[tokio::test]
870    async fn test_trident_client_ensure_supported_token_unsupported_error() {
871        let chain_id: TidesChainId = 10;
872        let mut deployments = HashMap::new();
873        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
874        let client = make_test_client(deployments);
875
876        let token = Address::from([0x22u8; 20]);
877        let res = client.ensure_supported_token(chain_id, token);
878        assert!(matches!(res, Err(Error::UnsupportedToken(t)) if t == token));
879    }
880
881    #[tokio::test]
882    async fn test_trident_client_resolve_destination_tokens_same_chain_success() {
883        let chain_id: TidesChainId = 123;
884
885        let mut client = {
886            // For same-chain case, no deployments or attester lookups are needed.
887            let deployments = HashMap::new();
888            make_test_client(deployments)
889        };
890
891        let src_token = Address::from([0xAAu8; 20]);
892
893        let res = client
894            .resolve_destination_tokens::<Address, Address>(chain_id, chain_id, src_token)
895            .await;
896
897        assert!(res.is_ok());
898        let v = res.unwrap();
899        assert_eq!(v.len(), 1);
900        assert_eq!(v[0], src_token);
901    }
902}