use crate::eips::eip712::EIP712;
use crate::eips::eip7597::{EIP7597, EIP7597PermitMessage};
use crate::eips::erc20::IERC20;
use crate::error::Error;
use crate::types::{
DepositInEvmChainWithApprovalResponse, EvmToEvmSpendRequest, EvmToEvmSpendResponse,
};
use alloy_primitives::{Address, Bytes, Signature, U256};
use alloy_provider::{Network, Provider};
use alloy_sol_types::{Eip712Domain, sol};
use rand_core::TryRngCore;
use std::collections::{HashMap, HashSet};
use tides_core_types::TidesChainId;
use tonic::codegen::StdError;
use tonic::transport::{Channel, Endpoint};
use trident_proto_types::tides::core::v1::ChainType;
use trident_proto_types::trident::attester::v1::attester_service_client::AttesterServiceClient;
use trident_proto_types::trident::attester::v1::{GetChainTokenMappingsRequest, SpendRequest};
use trident_proto_types::trident::attester::v1::{
GetDeploymentsRequest, GetLastFinalizedBlockRequest, GetSupportedDepositTokensRequest,
};
use trident_proto_types::trident::core::v1::{SpendIntentSigned, TridentDeployment};
use trident_types::core::{FixedBytes32, SpendIntent, SpendSpec};
use trident_types::evm::abi::{TridentDelivery, TridentDeposits};
pub struct TridentClient {
deployments: HashMap<TidesChainId, TridentDeployment>,
supported_tokens: HashMap<TidesChainId, HashSet<Address>>,
max_block_height_buffer_percentage: u64,
pub attester: AttesterServiceClient<Channel>,
}
impl TridentClient {
pub const DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE: u64 = 10;
pub async fn connect<D>(endpoint: D) -> Result<Self, Error>
where
D: TryInto<Endpoint>,
D::Error: Into<StdError>,
{
let mut attester = AttesterServiceClient::connect(endpoint).await?;
let deployments: HashMap<TidesChainId, TridentDeployment> = attester
.get_deployments(GetDeploymentsRequest {})
.await?
.into_inner()
.deployments
.into_iter()
.map(|d| Ok((d.chain_id, d.deployment.ok_or(Error::InvalidService)?)))
.collect::<Result<_, Error>>()?;
let mut supported_tokens: HashMap<TidesChainId, HashSet<Address>> = HashMap::new();
for (chain_id, _) in deployments.iter() {
let resp = attester
.get_supported_deposit_tokens(GetSupportedDepositTokensRequest {
chain_id: *chain_id,
})
.await?
.into_inner();
for t in resp.tokens.into_iter() {
if t.address.len() == 32 {
let addr = Address::from_slice(&t.address[12..]);
supported_tokens.entry(*chain_id).or_default().insert(addr);
}
}
}
Ok(Self {
attester,
deployments,
supported_tokens,
max_block_height_buffer_percentage: Self::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
})
}
pub async fn connect_testnet() -> Result<Self, Error> {
Self::connect(crate::envs::testnet::trident::ENDPOINT).await
}
pub fn with_max_block_height_buffer_percentage(mut self, buffer: u64) -> Self {
self.max_block_height_buffer_percentage = buffer;
self
}
pub async fn evm_deposit_with_approval<N: Network, P: Provider<N>>(
&self,
provider: P,
tides_chain_id: u32,
token: Address,
amount: U256,
) -> Result<DepositInEvmChainWithApprovalResponse<N>, Error> {
let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
let approval_tx_receipt = self
.evm_approve_token(&provider, token, deposits_contract, amount)
.await?;
let deposit_tx_receipt = self
.evm_deposit_token(&provider, deposits_contract, token, amount)
.await?;
Ok(DepositInEvmChainWithApprovalResponse {
approval_tx_receipt,
deposit_tx_receipt,
})
}
fn validate_evm_deposit(
&self,
tides_chain_id: TidesChainId,
token: Address,
) -> Result<Address, Error> {
let deployment = self.ensure_evm_chain(tides_chain_id)?;
self.ensure_supported_token(tides_chain_id, token)?;
let dep_addr_bytes = &deployment.trident_deposits_contract;
if dep_addr_bytes.len() != 32 {
return Err(Error::InvalidService);
}
Ok(Address::from_slice(&dep_addr_bytes[12..]))
}
async fn evm_approve_token<N: Network, P: Provider<N>>(
&self,
provider: &P,
token: Address,
spender: Address,
amount: U256,
) -> Result<<N as Network>::ReceiptResponse, Error> {
let erc20 = IERC20::new(token, provider);
let tx = erc20.approve(spender, amount).send().await?;
Ok(tx.get_receipt().await?)
}
pub async fn evm_deposit_token<N: Network, P: Provider<N>>(
&self,
provider: &P,
deposits_contract: Address,
token: Address,
amount: U256,
) -> Result<<N as Network>::ReceiptResponse, Error> {
let deposits = TridentDeposits::new(deposits_contract, provider);
let tx = deposits.deposit(token, amount).send().await?;
Ok(tx.get_receipt().await?)
}
pub async fn evm_sign_and_deposit_with_permit<
N: Network,
P: Provider<N>,
S: alloy_signer::Signer + Send + Sync,
>(
&self,
provider: P,
mut signer: S,
tides_chain_id: u32,
token: Address,
amount: U256,
deadline: U256,
) -> Result<N::ReceiptResponse, Error> {
let deposits_contract = self.validate_evm_deposit(tides_chain_id, token)?;
let owner = signer.address();
let signature = {
let domain = EIP712::query_domain(&provider, token).await?;
let nonce = EIP7597::get_nonce(&provider, token, owner).await?;
self.evm_sign_deposit_with_permit_request(
&mut signer,
domain,
deposits_contract,
amount,
nonce,
deadline,
)
.await?
};
let deposit_tx_receipt = self
.evm_deposit_with_permit(
&provider,
deposits_contract,
token,
owner,
amount,
deadline,
Bytes::from(signature.as_bytes()),
)
.await?;
Ok(deposit_tx_receipt)
}
pub async fn evm_sign_deposit_with_permit_request<S: alloy_signer::Signer + Send + Sync>(
&self,
mut signer: S,
domain: Eip712Domain,
spender: Address,
amount: U256,
nonce: U256,
deadline: U256,
) -> Result<Signature, Error> {
let typed_data = EIP7597::permit_typed_data(
domain,
EIP7597PermitMessage {
owner: signer.address(),
spender,
value: amount,
nonce,
deadline,
},
);
let sig = EIP712::sign(typed_data, &mut signer).await?;
Ok(sig)
}
#[allow(clippy::too_many_arguments)]
pub async fn evm_deposit_with_permit<N: Network, P: Provider<N>>(
&self,
provider: &P,
deposits_contract: Address,
token: Address,
owner: Address,
amount: U256,
deadline: U256,
signature: Bytes,
) -> Result<<N as Network>::ReceiptResponse, Error> {
let deposits = TridentDeposits::new(deposits_contract, provider);
let tx = deposits
.depositWithPermit(token, owner, amount, deadline, signature)
.send()
.await?;
Ok(tx.get_receipt().await?)
}
pub async fn evm_deliver<N: Network, P: Provider<N>>(
&mut self,
provider: &P,
destination_chain: TidesChainId,
attestation_payload: Vec<u8>,
signature: Vec<u8>,
) -> Result<<N as Network>::ReceiptResponse, Error> {
let dep = self.ensure_evm_chain(destination_chain)?;
let delivery_fb32 = FixedBytes32::try_from(dep.trident_delivery_contract.clone())?;
let delivery_addr = Address::try_from(delivery_fb32)?;
let delivery = TridentDelivery::new(delivery_addr, provider);
let tx = delivery
.deliverToken(attestation_payload.into(), signature.into())
.send()
.await?;
let receipt = tx.get_receipt().await?;
Ok(receipt)
}
pub async fn evm_to_evm_spend<'a, DN: Network, S: alloy_signer::Signer, DP: Provider<DN>>(
&mut self,
req: EvmToEvmSpendRequest<'a, S, DP, DN>,
) -> Result<EvmToEvmSpendResponse<DN>, Error> {
let signer = req.source_signer.address();
self.ensure_evm_chain(req.source_chain_id)?;
self.ensure_evm_chain(req.destination_chain_id)?;
self.ensure_supported_token(req.source_chain_id, req.source_token)?;
let dst_token = if let Some(tok) = req.destination_token {
tok
} else {
self.resolve_single_destination_token(
req.source_chain_id,
req.destination_chain_id,
req.source_token,
)
.await?
};
self.ensure_supported_token(req.destination_chain_id, dst_token)?;
let spend_spec = self.make_spend_spec(
req.source_chain_id,
signer,
req.source_token,
req.source_token_amount,
req.destination_chain_id,
dst_token,
req.destination_recipient,
req.destination_caller.unwrap_or(signer),
req.salt.map(Ok).unwrap_or_else(|| self.generate_salt())?,
req.destination_callback.map(|v| v.to_vec()),
)?;
let fees = match req.source_max_fees {
Some(fees) => fees,
None => {
self.get_suggested_fee(
req.source_chain_id,
req.source_token,
req.source_token_amount,
req.destination_chain_id,
dst_token,
)
.await?
}
};
self.ensure_sufficient_source_balance(
req.source_chain_id,
signer,
req.source_token,
fees + req.source_token_amount,
)
.await?;
let max_block_height = match req.max_block_height {
Some(max_block_height) => max_block_height,
None => {
self.suggest_spend_intent_max_block_height(req.source_chain_id)
.await?
}
};
let spend_intent = self.make_spend_intent(fees, max_block_height, spend_spec);
let signature = trident_types::evm::spend_intent_signing::sign_spend_intent(
req.source_signer,
&spend_intent,
)
.await?
.as_bytes()
.to_vec();
let spend_attestation_signed = self
.attester
.spend(SpendRequest {
intent: Some(SpendIntentSigned {
intent: Some(spend_intent.clone().into()),
signature,
}),
})
.await?
.into_inner()
.attestation
.expect("attester.spend.attestation");
let delivery_tx_receipt = match self
.evm_deliver(
req.destination_provider,
req.destination_chain_id,
spend_attestation_signed.attestation_bytes.clone(),
spend_attestation_signed.signature.clone(),
)
.await
{
Ok(r) => r,
Err(e) => {
return Err(Error::EvmDeliveryFailed(Box::new(
crate::error::EvmDeliveryFailed {
spend_intent,
attestation_payload: spend_attestation_signed.attestation_bytes,
signature: spend_attestation_signed.signature,
cause: Box::new(e),
},
)));
}
};
Ok(EvmToEvmSpendResponse {
destination_token: dst_token,
applied_fees: fees,
max_block_height,
delivery_tx_receipt,
})
}
async fn resolve_single_destination_token(
&mut self,
source_chain_id: TidesChainId,
destination_chain_id: TidesChainId,
source_token: Address,
) -> Result<Address, Error> {
let candidates: Vec<Address> = self
.resolve_destination_tokens::<Address, Address>(
source_chain_id,
destination_chain_id,
source_token,
)
.await?;
match candidates.len() {
0 => Err(Error::NoDestinationTokenMapping(
source_token,
source_chain_id,
destination_chain_id,
)),
1 => Ok(candidates[0]),
_ => Err(Error::AmbiguousDestinationTokenMapping(
source_token,
source_chain_id,
destination_chain_id,
)),
}
}
pub async fn resolve_destination_tokens<T1, T2>(
&mut self,
source_chain_id: TidesChainId,
destination_chain_id: TidesChainId,
source_token: T1,
) -> Result<Vec<T2>, Error>
where
T1: Into<FixedBytes32>,
T2: TryFrom<FixedBytes32>,
<T2 as TryFrom<FixedBytes32>>::Error: core::fmt::Display,
{
if source_chain_id == destination_chain_id {
let fb32: FixedBytes32 = source_token.into();
let t2 = T2::try_from(fb32).map_err(|err: <T2 as TryFrom<FixedBytes32>>::Error| {
Error::TokenConversion(err.to_string())
})?;
return Ok(vec![t2]);
}
let mappings = self
.attester
.get_chain_token_mappings(GetChainTokenMappingsRequest {
chain_id: source_chain_id,
})
.await?
.into_inner()
.mappings;
let fb32: FixedBytes32 = source_token.into();
let fb32_bytes = fb32.to_vec();
let fb32s = mappings
.into_iter()
.filter(|v| match &v.token {
None => false,
Some(v) => v.address == fb32_bytes,
})
.flat_map(|v| v.mappings)
.filter(|v| v.chain_id == destination_chain_id)
.map(|v| Result::<_, Error>::Ok(FixedBytes32::try_from(v.address)?))
.collect::<Result<Vec<FixedBytes32>, Error>>()?;
fb32s
.into_iter()
.map(|v| T2::try_from(v).map_err(|err| Error::TokenConversion(err.to_string())))
.collect()
}
async fn ensure_sufficient_source_balance(
&mut self,
chain_id: TidesChainId,
account: Address,
token: Address,
amount: U256,
) -> Result<(), Error> {
let resp = self
.attester
.balances(
trident_proto_types::trident::attester::v1::BalancesRequest {
chain_id,
address: FixedBytes32::from(account).to_vec(),
},
)
.await?
.into_inner();
let mut available = U256::ZERO;
for b in resp.available_balances.into_iter() {
if b.token.len() == 32 {
let fb = FixedBytes32::try_from(b.token)?;
let tok_addr = Address::try_from(fb)?;
if tok_addr == token {
let arr: [u8; 32] = b.amount.as_slice().try_into()?;
available = U256::from_be_bytes(arr);
break;
}
}
}
if available < amount {
return Err(Error::InsufficientBalance(amount, available));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn make_spend_spec(
&self,
source_chain: TidesChainId,
source_depositor: Address,
source_token: impl Into<FixedBytes32>,
source_token_amount: U256,
destination_chain: TidesChainId,
destination_token: impl Into<FixedBytes32>,
destination_recipient: impl Into<FixedBytes32>,
destination_caller: impl Into<FixedBytes32>,
salt: impl Into<FixedBytes32>,
callback_data: Option<Vec<u8>>,
) -> Result<SpendSpec, Error> {
let src_dep = self.ensure_evm_chain(source_chain)?;
let dst_dep = self.ensure_evm_chain(destination_chain)?;
let spec = SpendSpec {
version: 0,
source_chain,
source_contract: FixedBytes32::try_from(src_dep.trident_deposits_contract.clone())?,
source_token: source_token.into(),
source_token_amount,
source_depositor: FixedBytes32::from(source_depositor),
destination_chain,
destination_contract: FixedBytes32::try_from(
dst_dep.trident_delivery_contract.clone(),
)?,
destination_token: destination_token.into(),
destination_recipient: destination_recipient.into(),
destination_caller: destination_caller.into(),
salt: salt.into(),
callback_data: callback_data.unwrap_or_default(),
};
Ok(spec)
}
pub fn make_spend_intent(
&self,
max_fee: U256,
max_block_height: u64,
spend_spec: SpendSpec,
) -> SpendIntent {
SpendIntent {
max_fee: max_fee.try_into().unwrap(),
max_block_height,
spend_spec,
}
}
fn generate_salt(&self) -> Result<FixedBytes32, Error> {
let mut rng = rand::rngs::OsRng;
let mut buf = [0u8; 32];
rng.try_fill_bytes(&mut buf)?;
Ok(FixedBytes32::from(buf))
}
pub async fn get_suggested_fee(
&mut self,
_source_chain: TidesChainId,
_source_token: impl Into<FixedBytes32>,
_source_token_amount: U256,
_destination_chain: TidesChainId,
_destination_token: impl Into<FixedBytes32>,
) -> Result<U256, Error> {
Ok(U256::ZERO)
}
pub async fn suggest_spend_intent_max_block_height(
&mut self,
src_chain_id: TidesChainId,
) -> Result<u64, Error> {
let min_blocks = {
let dep = self.ensure_known_chain(src_chain_id)?;
dep.min_spend_intent_validity_blocks
};
let pct = self.max_block_height_buffer_percentage as u128;
let buffer_blocks_u128 = (min_blocks as u128)
.checked_mul(pct)
.ok_or(Error::BlockHeightOverflow)?
.checked_div(100u128)
.ok_or(Error::BlockHeightOverflow)?;
let buffer_blocks: u64 =
u64::try_from(buffer_blocks_u128).map_err(|_| Error::BlockHeightOverflow)?;
let min_blocks_with_buffer = min_blocks
.checked_add(buffer_blocks)
.ok_or(Error::BlockHeightOverflow)?;
let last_finalized = self
.attester
.get_last_finalized_block(GetLastFinalizedBlockRequest {
chain_id: src_chain_id,
})
.await?
.into_inner()
.last_finalized_block;
let suggested = last_finalized
.checked_add(min_blocks_with_buffer)
.ok_or(Error::BlockHeightOverflow)?;
Ok(suggested)
}
fn ensure_known_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
self.deployments
.get(&chain_id)
.ok_or(Error::UnknownChain(chain_id))
}
fn ensure_evm_chain(&self, chain_id: TidesChainId) -> Result<&TridentDeployment, Error> {
let dep = self.ensure_known_chain(chain_id)?;
if dep.chain_type != (ChainType::Evm as i32) {
return Err(Error::UnsupportedChainType(dep.chain_type));
}
Ok(dep)
}
fn ensure_supported_token(&self, chain_id: TidesChainId, token: Address) -> Result<(), Error> {
let is_supported = self
.supported_tokens
.get(&chain_id)
.map(|set| set.contains(&token))
.unwrap_or(false);
if is_supported {
Ok(())
} else {
Err(Error::UnsupportedToken(token))
}
}
pub async fn evm_faucet<N: Network, P: Provider<N>>(
&self,
provider: &P,
token: Address,
recipient: Address,
amount: U256,
) -> Result<N::ReceiptResponse, Error> {
sol! {
#[sol(rpc)]
contract Faucet {
function mint(address, uint256);
}
}
let faucet = Faucet::new(token, provider);
let pending = faucet.mint(recipient, amount).send().await?;
let receipt = pending.get_receipt().await?;
Ok(receipt)
}
pub fn get_deployment_by_name(&self, name: impl AsRef<str>) -> Option<&TridentDeployment> {
let name = name.as_ref();
self.deployments.iter().find_map(|(_, value)| {
if value.chain_name.eq(name) {
Some(value)
} else {
None
}
})
}
pub async fn evm_balances(
&mut self,
chain_id: TidesChainId,
address: impl Into<Address>,
) -> Result<HashMap<Address, U256>, Error> {
self.ensure_evm_chain(chain_id)?;
self.attester
.balances(
trident_proto_types::trident::attester::v1::BalancesRequest {
chain_id,
address: FixedBytes32::from(address.into()).to_vec(),
},
)
.await?
.into_inner()
.available_balances
.into_iter()
.map(|balance| {
let token: Address = FixedBytes32::try_from(balance.token)?.try_into()?;
let amount =
U256::try_from_be_slice(&balance.amount).ok_or(Error::InvalidU256ByteSlice)?;
Ok((token, amount))
})
.collect()
}
pub async fn evm_balance(
&mut self,
chain_id: TidesChainId,
address: impl Into<Address>,
want: impl Into<Address>,
) -> Result<U256, Error> {
let want = want.into();
self.evm_balances(chain_id, address).await.map(|v| {
v.into_iter()
.find_map(|(token, amount)| {
if token == want {
return Some(amount);
}
None
})
.unwrap_or_default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::Address;
use std::collections::HashMap;
use tonic::transport::Channel;
use trident_proto_types::tides::core::v1::ChainType;
fn make_fb32() -> Vec<u8> {
vec![0u8; 32]
}
fn make_deployment(chain_type: ChainType, chain_id: u32) -> TridentDeployment {
TridentDeployment {
chain_type: chain_type as i32,
chain_name: "ethereum".to_string(),
tides_chain_id: chain_id,
trident_deposits_contract: make_fb32(),
trident_claims_contract: make_fb32(),
trident_delivery_contract: make_fb32(),
min_spend_intent_validity_blocks: 10,
max_spend_attestation_validity_blocks: 20,
anchor_block: 1,
}
}
fn make_test_client(deployments: HashMap<TidesChainId, TridentDeployment>) -> TridentClient {
let attester =
AttesterServiceClient::new(Channel::from_static("http://localhost:1").connect_lazy());
TridentClient {
deployments,
supported_tokens: HashMap::new(),
max_block_height_buffer_percentage:
TridentClient::DEFAULT_MAX_BLOCK_HEIGHT_BUFFER_PERCENTAGE,
attester,
}
}
#[tokio::test]
async fn test_trident_client_ensure_known_chain_known_chain_success() {
let chain_id: TidesChainId = 1;
let mut deployments = HashMap::new();
deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
let client = make_test_client(deployments);
let res = client.ensure_known_chain(chain_id);
assert!(res.is_ok());
}
#[tokio::test]
async fn test_trident_client_ensure_known_chain_unknown_chain_error() {
let client = make_test_client(HashMap::new());
let chain_id: TidesChainId = 42;
let res = client.ensure_known_chain(chain_id);
assert!(matches!(res, Err(Error::UnknownChain(id)) if id == chain_id));
}
#[tokio::test]
async fn test_trident_client_ensure_evm_chain_evm_chain_success() {
let chain_id: TidesChainId = 5;
let mut deployments = HashMap::new();
deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
let client = make_test_client(deployments);
let res = client.ensure_evm_chain(chain_id);
assert!(res.is_ok());
}
#[tokio::test]
async fn test_trident_client_ensure_evm_chain_unsupported_chain_type_error() {
let chain_id: TidesChainId = 7;
let mut deployments = HashMap::new();
deployments.insert(chain_id, make_deployment(ChainType::Unspecified, chain_id));
let client = make_test_client(deployments);
let res = client.ensure_evm_chain(chain_id);
assert!(
matches!(res, Err(Error::UnsupportedChainType(t)) if t == ChainType::Unspecified as i32)
);
}
#[tokio::test]
async fn test_trident_client_ensure_supported_token_supported_success() {
let chain_id: TidesChainId = 9;
let mut deployments = HashMap::new();
deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
let mut client = make_test_client(deployments);
let token = Address::from([0x11u8; 20]);
client
.supported_tokens
.entry(chain_id)
.or_default()
.insert(token);
let res = client.ensure_supported_token(chain_id, token);
assert!(matches!(res, Ok(())));
}
#[tokio::test]
async fn test_trident_client_ensure_supported_token_unsupported_error() {
let chain_id: TidesChainId = 10;
let mut deployments = HashMap::new();
deployments.insert(chain_id, make_deployment(ChainType::Evm, chain_id));
let client = make_test_client(deployments);
let token = Address::from([0x22u8; 20]);
let res = client.ensure_supported_token(chain_id, token);
assert!(matches!(res, Err(Error::UnsupportedToken(t)) if t == token));
}
#[tokio::test]
async fn test_trident_client_resolve_destination_tokens_same_chain_success() {
let chain_id: TidesChainId = 123;
let mut client = {
let deployments = HashMap::new();
make_test_client(deployments)
};
let src_token = Address::from([0xAAu8; 20]);
let res = client
.resolve_destination_tokens::<Address, Address>(chain_id, chain_id, src_token)
.await;
assert!(res.is_ok());
let v = res.unwrap();
assert_eq!(v.len(), 1);
assert_eq!(v[0], src_token);
}
}