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::{ChainType, TidesChainId};
14use tonic::codegen::StdError;
15use tonic::transport::{Channel, Endpoint};
16use trident_proto_types::trident::attester::v1::attester_service_client::AttesterServiceClient;
17use trident_proto_types::trident::attester::v1::{GetChainTokenMappingsRequest, SpendRequest};
18use trident_proto_types::trident::attester::v1::{
19    GetDeploymentsRequest, GetLastFinalizedBlockRequest, GetSupportedDepositTokensRequest,
20};
21use trident_proto_types::trident::core::v1::SpendIntentSigned;
22use trident_types::core::{FixedBytes32, SpendIntent, SpendSpec, TridentDeployment};
23use trident_types::evm::abi::{TridentDelivery, TridentDeposits};
24
25/// Client for interacting with Trident attester services and EVM contracts.
26/// Maintains deployments, supported tokens, and exposes high-level operations.
27pub struct TridentClient {
28    deployments: HashMap<TidesChainId, TridentDeployment>,
29    supported_tokens: HashMap<TidesChainId, HashSet<Address>>,
30    // Expressed as percentage points.
31    max_block_height_buffer_percentage: u64,
32    /// gRPC client to the Trident Attester service.
33    pub attester: AttesterServiceClient<Channel>,
34}
35
36impl TridentClient {
37    /// Default buffer percentage applied to the minimum validity blocks
38    /// when suggesting a spend intent's max block height.
39    pub const DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE: u64 = 10;
40    /// Connects to the attester endpoint, loads deployments and supported deposit tokens.
41    pub async fn connect<D>(endpoint: D) -> Result<Self, Error>
42    where
43        D: TryInto<Endpoint>,
44        D::Error: Into<StdError>,
45    {
46        let mut attester = AttesterServiceClient::connect(endpoint).await?;
47
48        // get deployments
49        let deployments = attester
50            .get_deployments(GetDeploymentsRequest {})
51            .await?
52            .into_inner()
53            .deployments
54            .into_iter()
55            .map(|d| d.deployment.ok_or(Error::InvalidService))
56            .map(|d| Ok(TridentDeployment::try_from(d?)?))
57            .map(|d| d.map(|d| (d.chain_id, d)))
58            .collect::<Result<HashMap<_, _>, Error>>()?;
59
60        // get supported deposit tokens per deployment/chain
61        let mut supported_tokens: HashMap<TidesChainId, HashSet<Address>> = HashMap::new();
62        for (chain_id, _) in deployments.iter() {
63            let resp = attester
64                .get_supported_deposit_tokens(GetSupportedDepositTokensRequest {
65                    chain_id: *chain_id,
66                })
67                .await?
68                .into_inner();
69            for t in resp.tokens.into_iter() {
70                if t.address.len() == 32 {
71                    let addr = Address::from_slice(&t.address[12..]);
72                    supported_tokens.entry(*chain_id).or_default().insert(addr);
73                }
74            }
75        }
76
77        Ok(Self {
78            attester,
79            deployments,
80            supported_tokens,
81            max_block_height_buffer_percentage: Self::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
82        })
83    }
84
85    pub async fn connect_testnet() -> Result<Self, Error> {
86        Self::connect(crate::envs::testnet::trident::ENDPOINT).await
87    }
88
89    /// Sets the buffer percentage applied to the minimum validity blocks
90    /// when suggesting spend intent expirations.
91    pub fn with_max_block_height_buffer_percentage(mut self, buffer: u64) -> Self {
92        self.max_block_height_buffer_percentage = buffer;
93        self
94    }
95
96    /// Approves and deposits `amount` of `token` into the Trident deposits contract on an EVM chain.
97    /// Returns receipts for both the approval and deposit transactions.
98    pub async fn evm_deposit_with_approval<N: Network, P: Provider<N>>(
99        &self,
100        provider: P,
101        tides_chain_id: u32,
102        token: Address,
103        amount: U256,
104    ) -> Result<DepositInEvmChainWithApprovalResponse<N>, Error> {
105        let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
106        let approval_tx_receipt = self
107            .evm_approve_token(&provider, token, deposits_contract, amount)
108            .await?;
109        let deposit_tx_receipt = self
110            .evm_deposit_token(&provider, deposits_contract, token, amount)
111            .await?;
112
113        Ok(DepositInEvmChainWithApprovalResponse {
114            approval_tx_receipt,
115            deposit_tx_receipt,
116        })
117    }
118
119    fn validate_evm_deposit(
120        &self,
121        tides_chain_id: TidesChainId,
122        token: Address,
123    ) -> Result<Address, Error> {
124        let deployment = self.ensure_evm_chain(tides_chain_id)?;
125
126        self.ensure_supported_token(tides_chain_id, token)?;
127
128        let dep_addr_bytes = &deployment.trident_deposits_contract;
129        if dep_addr_bytes.len() != 32 {
130            return Err(Error::InvalidService);
131        }
132        Ok(Address::from_slice(&dep_addr_bytes[12..]))
133    }
134
135    async fn evm_approve_token<N: Network, P: Provider<N>>(
136        &self,
137        provider: &P,
138        token: Address,
139        spender: Address,
140        amount: U256,
141    ) -> Result<<N as Network>::ReceiptResponse, Error> {
142        let erc20 = IERC20::new(token, provider);
143        let tx = erc20.approve(spender, amount).send().await?;
144        Ok(tx.get_receipt().await?)
145    }
146
147    /// Submits a deposit transaction to the Trident deposits contract for the given ERC-20 `token` and `amount`.
148    pub async fn evm_deposit_token<N: Network, P: Provider<N>>(
149        &self,
150        provider: &P,
151        deposits_contract: Address,
152        token: Address,
153        amount: U256,
154    ) -> Result<<N as Network>::ReceiptResponse, Error> {
155        let deposits = TridentDeposits::new(deposits_contract, provider);
156        let tx = deposits.deposit(token, amount).send().await?;
157        Ok(tx.get_receipt().await?)
158    }
159
160    pub async fn evm_sign_and_deposit_with_permit<
161        N: Network,
162        P: Provider<N>,
163        S: alloy_signer::Signer + Send + Sync,
164    >(
165        &self,
166        provider: P,
167        mut signer: S,
168        tides_chain_id: u32,
169        token: Address,
170        amount: U256,
171        deadline: U256,
172    ) -> Result<N::ReceiptResponse, Error> {
173        let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
174        let owner = signer.address();
175
176        let signature = {
177            let domain = EIP712::query_domain(&provider, token).await?;
178            let nonce = EIP7597::get_nonce(&provider, token, owner).await?;
179            self.evm_sign_deposit_with_permit_request(
180                &mut signer,
181                domain,
182                deposits_contract,
183                amount,
184                nonce,
185                deadline,
186            )
187            .await?
188        };
189
190        let deposit_tx_receipt = self
191            .evm_deposit_with_permit(
192                &provider,
193                deposits_contract,
194                token,
195                owner,
196                amount,
197                deadline,
198                Bytes::from(signature.as_bytes()),
199            )
200            .await?;
201
202        Ok(deposit_tx_receipt)
203    }
204
205    pub async fn evm_sign_deposit_with_permit_request<S: alloy_signer::Signer + Send + Sync>(
206        &self,
207        mut signer: S,
208        domain: Eip712Domain,
209        spender: Address,
210        amount: U256,
211        nonce: U256,
212        deadline: U256,
213    ) -> Result<Signature, Error> {
214        let typed_data = EIP7597::permit_typed_data(
215            domain,
216            EIP7597PermitMessage {
217                owner: signer.address(),
218                spender,
219                value: amount,
220                nonce,
221                deadline,
222            },
223        );
224
225        let sig = EIP712::sign(typed_data, &mut signer).await?;
226
227        Ok(sig)
228    }
229
230    /// Deposit tokens with an EIP-7597 permit, passing the signature as bytes to allow for SCA deposits
231    #[allow(clippy::too_many_arguments)]
232    pub async fn evm_deposit_with_permit<N: Network, P: Provider<N>>(
233        &self,
234        provider: &P,
235        deposits_contract: Address,
236        token: Address,
237        owner: Address,
238        amount: U256,
239        deadline: U256,
240        signature: Bytes,
241    ) -> Result<<N as Network>::ReceiptResponse, Error> {
242        let deposits = TridentDeposits::new(deposits_contract, provider);
243        let tx = deposits
244            .depositWithPermit(token, owner, amount, deadline, signature)
245            .send()
246            .await?;
247        Ok(tx.get_receipt().await?)
248    }
249
250    /// Executes a spend attestation on the destination chain by calling `TridentDelivery.deliverToken`.
251    pub async fn evm_deliver<N: Network, P: Provider<N>>(
252        &mut self,
253        provider: &P,
254        destination_chain: TidesChainId,
255        attestation_payload: Vec<u8>,
256        signature: Vec<u8>,
257    ) -> Result<<N as Network>::ReceiptResponse, Error> {
258        // Ensure destination chain is EVM and fetch delivery contract
259        let dep = self.ensure_evm_chain(destination_chain)?;
260        let delivery_addr = Address::try_from(dep.trident_delivery_contract)?;
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: src_dep.trident_deposits_contract,
558            source_token: source_token.into(),
559            source_token_amount,
560            source_depositor: FixedBytes32::from(source_depositor),
561            destination_chain,
562            destination_contract: dst_dep.trident_delivery_contract,
563            destination_token: destination_token.into(),
564            destination_recipient: destination_recipient.into(),
565            destination_caller: destination_caller.into(),
566            salt: salt.into(),
567            callback_data: callback_data.unwrap_or_default(),
568        };
569
570        Ok(spec)
571    }
572
573    /// Wraps a `SpendSpec` into a `SpendIntent` with fee cap and expiration height.
574    pub fn make_spend_intent(
575        &self,
576        max_fee: U256,
577        max_block_height: u64,
578        spend_spec: SpendSpec,
579    ) -> SpendIntent {
580        SpendIntent {
581            max_fee: max_fee.try_into().unwrap(),
582            max_block_height,
583            spend_spec,
584        }
585    }
586
587    fn generate_salt(&self) -> Result<FixedBytes32, Error> {
588        let mut rng = rand::rngs::OsRng;
589        let mut buf = [0u8; 32];
590        rng.try_fill_bytes(&mut buf)?;
591        Ok(FixedBytes32::from(buf))
592    }
593
594    /// Suggests a source-side fee for the given route and amount. Placeholder returning zero for now.
595    pub async fn get_suggested_fee(
596        &mut self,
597        _source_chain: TidesChainId,
598        _source_token: impl Into<FixedBytes32>,
599        _source_token_amount: U256,
600        _destination_chain: TidesChainId,
601        _destination_token: impl Into<FixedBytes32>,
602    ) -> Result<U256, Error> {
603        // TODO: impl
604        Ok(U256::ZERO)
605    }
606
607    /// Suggests a spend intent's max block height using:
608    /// last finalized block + (min validity blocks + buffered min validity blocks).
609    pub async fn suggest_spend_intent_max_block_height(
610        &mut self,
611        src_chain_id: TidesChainId,
612    ) -> Result<u64, Error> {
613        // 1) Get the min required validity blocks from deployment.
614        let min_blocks = {
615            let dep = self.ensure_known_chain(src_chain_id)?;
616            dep.min_spend_intent_validity_blocks
617        };
618
619        // 2) Immediately apply buffer to the min required blocks
620        let pct = self.max_block_height_buffer_percentage as u128;
621        let buffer_blocks_u128 = (min_blocks as u128)
622            .checked_mul(pct)
623            .ok_or(Error::BlockHeightOverflow)?
624            .checked_div(100u128)
625            .ok_or(Error::BlockHeightOverflow)?;
626        let buffer_blocks: u64 =
627            u64::try_from(buffer_blocks_u128).map_err(|_| Error::BlockHeightOverflow)?;
628
629        let min_blocks_with_buffer = min_blocks
630            .checked_add(buffer_blocks)
631            .ok_or(Error::BlockHeightOverflow)?;
632
633        // 3) Query last finalized block for the source chain
634        let last_finalized = self
635            .attester
636            .get_last_finalized_block(GetLastFinalizedBlockRequest {
637                chain_id: src_chain_id,
638            })
639            .await?
640            .into_inner()
641            .last_finalized_block;
642
643        // 4) Suggested max block height = last_finalized + min_blocks_with_buffer
644        let suggested = last_finalized
645            .checked_add(min_blocks_with_buffer)
646            .ok_or(Error::BlockHeightOverflow)?;
647
648        Ok(suggested)
649    }
650
651    fn ensure_known_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
652        self.deployments
653            .get(&chain_id)
654            .ok_or(Error::UnknownChain(chain_id))
655    }
656
657    fn ensure_evm_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
658        let dep = self.ensure_known_chain(chain_id)?;
659        if dep.chain_type != ChainType::Evm {
660            return Err(Error::UnsupportedChainType(dep.chain_type as i32));
661        }
662        Ok(dep)
663    }
664
665    fn ensure_supported_token(&self, chain_id: TidesChainId, token: Address) -> Result<(), Error> {
666        let is_supported = self
667            .supported_tokens
668            .get(&chain_id)
669            .map(|set| set.contains(&token))
670            .unwrap_or(false);
671        if is_supported {
672            Ok(())
673        } else {
674            Err(Error::UnsupportedToken(token))
675        }
676    }
677
678    /// Faucets tokens by calling `mint(address,uint256)` on an open‑mint token.
679    /// `amount` is provided in the token's smallest unit.
680    pub async fn evm_faucet<N: Network, P: Provider<N>>(
681        &self,
682        provider: &P,
683        token: Address,
684        recipient: Address,
685        amount: U256,
686    ) -> Result<N::ReceiptResponse, Error> {
687        sol! {
688            #[sol(rpc)]
689            contract Faucet {
690                function mint(address, uint256);
691            }
692        }
693
694        // Call mint(to, amount)
695        let faucet = Faucet::new(token, provider);
696        let pending = faucet.mint(recipient, amount).send().await?;
697        let receipt = pending.get_receipt().await?;
698
699        Ok(receipt)
700    }
701
702    pub fn get_deployments(&self) -> &HashMap<TidesChainId, TridentDeployment> {
703        &self.deployments
704    }
705
706    pub fn get_deployment_by_name(&self, name: impl AsRef<str>) -> Option<&TridentDeployment> {
707        let name = name.as_ref();
708        self.deployments.iter().find_map(|(_, value)| {
709            if value.chain_name.eq(name) {
710                Some(value)
711            } else {
712                None
713            }
714        })
715    }
716
717    pub async fn evm_balances(
718        &mut self,
719        chain_id: TidesChainId,
720        address: impl Into<Address>,
721    ) -> Result<HashMap<Address, U256>, Error> {
722        // ensures EVM chain.
723        self.ensure_evm_chain(chain_id)?;
724
725        // gets balances.
726        self.attester
727            .balances(
728                trident_proto_types::trident::attester::v1::BalancesRequest {
729                    chain_id,
730                    address: FixedBytes32::from(address.into()).to_vec(),
731                },
732            )
733            .await?
734            .into_inner()
735            .available_balances
736            .into_iter()
737            .map(|balance| {
738                let token: Address = FixedBytes32::try_from(balance.token)?.try_into()?;
739                let amount =
740                    U256::try_from_be_slice(&balance.amount).ok_or(Error::InvalidU256ByteSlice)?;
741                Ok((token, amount))
742            })
743            .collect()
744    }
745
746    pub async fn evm_balance(
747        &mut self,
748        chain_id: TidesChainId,
749        address: impl Into<Address>,
750        want: impl Into<Address>,
751    ) -> Result<U256, Error> {
752        let want = want.into();
753        self.evm_balances(chain_id, address).await.map(|v| {
754            v.into_iter()
755                .find_map(|(token, amount)| {
756                    if token == want {
757                        return Some(amount);
758                    }
759                    None
760                })
761                .unwrap_or_default()
762        })
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use crate::{Error, TridentClient};
769    use alloy_primitives::Address;
770    use rand_core::TryRngCore;
771    use std::collections::HashMap;
772    use tides_core_types::{ChainType, TidesChainId};
773    use tonic::transport::Channel;
774    use trident_proto_types::trident::attester::v1::attester_service_client::AttesterServiceClient;
775    use trident_types::core::{FixedBytes32, TridentDeployment};
776
777    fn make_fb32() -> FixedBytes32 {
778        let mut fb = FixedBytes32([0u8; 32]);
779        rand::rngs::OsRng
780            .try_fill_bytes(&mut fb.0)
781            .expect("OsRng.try_fill_bytes");
782        fb
783    }
784
785    fn make_deployment(chain_type: ChainType, chain_id: u32) -> TridentDeployment {
786        TridentDeployment {
787            chain_type,
788            chain_name: "ethereum".to_string(),
789            chain_id,
790            trident_deposits_contract: make_fb32(),
791            trident_claims_contract: make_fb32(),
792            trident_delivery_contract: make_fb32(),
793            min_spend_intent_validity_blocks: 10,
794            max_spend_attestation_validity_blocks: 20,
795            anchor_block: 1,
796        }
797    }
798
799    fn make_test_client(deployments: HashMap<TidesChainId, TridentDeployment>) -> TridentClient {
800        let attester =
801            AttesterServiceClient::new(Channel::from_static("http://localhost:1").connect_lazy());
802
803        TridentClient {
804            deployments,
805            supported_tokens: HashMap::new(),
806            max_block_height_buffer_percentage:
807                TridentClient::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
808            attester,
809        }
810    }
811
812    // ensure_known_chain tests (appears before ensure_evm_chain)
813    #[tokio::test]
814    async fn test_trident_client_ensure_known_chain_known_chain_success() {
815        let chain_id: TidesChainId = 1;
816        let mut deployments = HashMap::new();
817        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
818        let client = make_test_client(deployments);
819
820        let res = client.ensure_known_chain(chain_id);
821        assert!(res.is_ok());
822    }
823
824    #[tokio::test]
825    async fn test_trident_client_ensure_known_chain_unknown_chain_error() {
826        let client = make_test_client(HashMap::new());
827        let chain_id: TidesChainId = 42;
828
829        let res = client.ensure_known_chain(chain_id);
830        assert!(matches!(res, Err(Error::UnknownChain(id)) if id == chain_id));
831    }
832
833    // ensure_evm_chain tests
834    #[tokio::test]
835    async fn test_trident_client_ensure_evm_chain_evm_chain_success() {
836        let chain_id: TidesChainId = 5;
837        let mut deployments = HashMap::new();
838        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
839        let client = make_test_client(deployments);
840
841        let res = client.ensure_evm_chain(chain_id);
842        assert!(res.is_ok());
843    }
844
845    // ensure_supported_token tests
846    #[tokio::test]
847    async fn test_trident_client_ensure_supported_token_supported_success() {
848        let chain_id: TidesChainId = 9;
849        let mut deployments = HashMap::new();
850        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
851        let mut client = make_test_client(deployments);
852
853        // insert token
854        let token = Address::from([0x11u8; 20]);
855        client
856            .supported_tokens
857            .entry(chain_id)
858            .or_default()
859            .insert(token);
860
861        let res = client.ensure_supported_token(chain_id, token);
862        assert!(matches!(res, Ok(())));
863    }
864
865    #[tokio::test]
866    async fn test_trident_client_ensure_supported_token_unsupported_error() {
867        let chain_id: TidesChainId = 10;
868        let mut deployments = HashMap::new();
869        deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
870        let client = make_test_client(deployments);
871
872        let token = Address::from([0x22u8; 20]);
873        let res = client.ensure_supported_token(chain_id, token);
874        assert!(matches!(res, Err(Error::UnsupportedToken(t)) if t == token));
875    }
876
877    #[tokio::test]
878    async fn test_trident_client_resolve_destination_tokens_same_chain_success() {
879        let chain_id: TidesChainId = 123;
880
881        let mut client = {
882            // For same-chain case, no deployments or attester lookups are needed.
883            let deployments = HashMap::new();
884            make_test_client(deployments)
885        };
886
887        let src_token = Address::from([0xAAu8; 20]);
888
889        let res = client
890            .resolve_destination_tokens::<Address, Address>(chain_id, chain_id, src_token)
891            .await;
892
893        assert!(res.is_ok());
894        let v = res.unwrap();
895        assert_eq!(v.len(), 1);
896        assert_eq!(v[0], src_token);
897    }
898}