Files
serai/processor/ethereum/router/src/lib.rs

722 lines
24 KiB
Rust
Raw Normal View History

2024-09-17 01:04:08 -04:00
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::{sync::Arc, collections::HashSet};
use borsh::{BorshSerialize, BorshDeserialize};
2024-09-17 01:04:08 -04:00
use group::ff::PrimeField;
use alloy_core::primitives::{
hex::{self, FromHex},
Address, U256, Bytes, TxKind,
};
2024-09-17 01:04:08 -04:00
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
2024-10-31 02:23:59 -04:00
use alloy_consensus::TxLegacy;
use alloy_rpc_types_eth::{BlockId, Log, Filter, TransactionInput, TransactionRequest};
2024-09-17 01:04:08 -04:00
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
use ethereum_primitives::LogIndex;
2024-09-17 01:04:08 -04:00
use ethereum_schnorr::{PublicKey, Signature};
use ethereum_deployer::Deployer;
use erc20::{Transfer, Erc20};
2024-09-17 01:04:08 -04:00
use serai_client::networks::ethereum::Address as SeraiAddress;
2024-09-17 01:04:08 -04:00
#[rustfmt::skip]
#[expect(warnings)]
#[expect(needless_pass_by_value)]
#[expect(clippy::all)]
#[expect(clippy::ignored_unit_patterns)]
#[expect(clippy::redundant_closure_for_method_calls)]
mod _irouter_abi {
alloy_sol_macro::sol!("contracts/IRouter.sol");
}
#[rustfmt::skip]
#[expect(warnings)]
#[expect(needless_pass_by_value)]
#[expect(clippy::all)]
#[expect(clippy::ignored_unit_patterns)]
#[expect(clippy::redundant_closure_for_method_calls)]
mod _router_abi {
2024-09-17 01:04:08 -04:00
include!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/router.rs"));
}
2024-11-02 13:19:07 -04:00
mod abi {
pub use super::_router_abi::IRouterWithoutCollisions::*;
pub use super::_router_abi::IRouter::*;
pub use super::_router_abi::Router::constructorCall;
2024-11-02 13:19:07 -04:00
}
2024-09-17 01:04:08 -04:00
use abi::{
NextSeraiKeySet as NextSeraiKeySetEvent, SeraiKeyUpdated as SeraiKeyUpdatedEvent,
InInstruction as InInstructionEvent, Batch as BatchEvent, EscapeHatch as EscapeHatchEvent,
Escaped as EscapedEvent,
2024-09-17 01:04:08 -04:00
};
2024-10-31 02:23:59 -04:00
#[cfg(test)]
mod tests;
2024-09-17 01:04:08 -04:00
impl From<&Signature> for abi::Signature {
fn from(signature: &Signature) -> Self {
Self {
c: <[u8; 32]>::from(signature.c().to_repr()).into(),
s: <[u8; 32]>::from(signature.s().to_repr()).into(),
}
}
}
/// A coin on Ethereum.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub enum Coin {
/// Ether, the native coin of Ethereum.
Ether,
/// An ERC20 token.
Erc20(
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
Address,
),
2024-09-17 01:04:08 -04:00
}
impl From<Coin> for Address {
fn from(coin: Coin) -> Address {
match coin {
Coin::Ether => Address::ZERO,
Coin::Erc20(address) => address,
}
}
}
impl From<Address> for Coin {
fn from(address: Address) -> Coin {
if address == Address::ZERO {
Coin::Ether
} else {
Coin::Erc20(address)
2024-12-09 02:00:17 -05:00
}
}
2024-09-17 01:04:08 -04:00
}
/// An InInstruction from the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub struct InInstruction {
/// The ID for this `InInstruction`.
pub id: LogIndex,
/// The hash of the transaction which caused this.
pub transaction_hash: [u8; 32],
2024-09-17 01:04:08 -04:00
/// The address which transferred these coins to Serai.
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
pub from: Address,
2024-09-17 01:04:08 -04:00
/// The coin transferred.
pub coin: Coin,
/// The amount transferred.
#[borsh(
serialize_with = "ethereum_primitives::serialize_u256",
deserialize_with = "ethereum_primitives::deserialize_u256"
)]
2024-09-17 01:04:08 -04:00
pub amount: U256,
/// The data associated with the transfer.
pub data: Vec<u8>,
}
/// A list of `OutInstruction`s.
#[derive(Clone)]
pub struct OutInstructions(Vec<abi::OutInstruction>);
impl From<&[(SeraiAddress, U256)]> for OutInstructions {
fn from(outs: &[(SeraiAddress, U256)]) -> Self {
Self(
outs
.iter()
.map(|(address, amount)| {
#[allow(non_snake_case)]
let (destinationType, destination) = match address {
SeraiAddress::Address(address) => {
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded
// address
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
}
SeraiAddress::Contract(contract) => (
abi::DestinationType::Code,
(abi::CodeDestination {
gasLimit: contract.gas_limit(),
code: contract.code().to_vec().into(),
})
.abi_encode(),
),
};
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
})
.collect(),
)
}
}
/// An action which was executed by the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub enum Executed {
/// Next key was set.
NextSeraiKeySet {
/// The nonce this was done with.
nonce: u64,
/// The key set.
key: [u8; 32],
},
/// The next key was updated to.
SeraiKeyUpdated {
2024-09-17 01:04:08 -04:00
/// The nonce this was done with.
nonce: u64,
/// The key set.
key: [u8; 32],
},
/// Executed batch of `OutInstruction`s.
2024-09-17 01:04:08 -04:00
Batch {
/// The nonce this was done with.
nonce: u64,
/// The hash of the signed message for the Batch executed.
message_hash: [u8; 32],
},
/// The escape hatch was set.
EscapeHatch {
/// The nonce this was done with.
nonce: u64,
/// The address set to escape to.
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
escape_to: Address,
},
2024-09-17 01:04:08 -04:00
}
impl Executed {
/// The nonce consumed by this executed event.
///
/// This is a `u64` despite the contract defining the nonce as a `u256`. Since the nonce is
/// incremental, the u64 will never be exhausted.
2024-09-17 01:04:08 -04:00
pub fn nonce(&self) -> u64 {
match self {
Executed::NextSeraiKeySet { nonce, .. } |
Executed::SeraiKeyUpdated { nonce, .. } |
Executed::Batch { nonce, .. } |
Executed::EscapeHatch { nonce, .. } => *nonce,
2024-09-17 01:04:08 -04:00
}
}
}
/// An Escape from the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct Escape {
/// The coin escaped.
pub coin: Coin,
/// The amount escaped.
#[borsh(
serialize_with = "ethereum_primitives::serialize_u256",
deserialize_with = "ethereum_primitives::deserialize_u256"
)]
pub amount: U256,
}
2024-09-17 01:04:08 -04:00
/// A view of the Router for Serai.
#[derive(Clone, Debug)]
pub struct Router {
provider: Arc<RootProvider<SimpleRequest>>,
address: Address,
}
2024-09-17 01:04:08 -04:00
impl Router {
/*
The gas limits to use for transactions.
These are expected to be constant as a distributed group signs the transactions invoking these
calls. Having the gas be constant prevents needing to run a protocol to determine what gas to
use.
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
expected to be modified with these made parameters. The caller would then be expected to pass
the correct set of prices for the network they're operating on.
*/
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
const EXECUTE_BASE_GAS: u64 = 48_000;
const ESCAPE_HATCH_GAS: u64 = 61_238;
fn code() -> Vec<u8> {
2024-09-17 01:04:08 -04:00
const BYTECODE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
Bytes::from_hex(BYTECODE).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
}
fn init_code(key: &PublicKey) -> Vec<u8> {
2024-09-17 01:04:08 -04:00
let mut bytecode = Self::code();
// Append the constructor arguments
bytecode.extend((abi::constructorCall { initialSeraiKey: key.eth_repr().into() }).abi_encode());
bytecode
}
2024-10-31 02:23:59 -04:00
/// Obtain the transaction to deploy this contract.
///
/// This transaction assumes the `Deployer` has already been deployed. The gas limit and gas
/// price are not set and are left to the caller.
2024-10-31 02:23:59 -04:00
pub fn deployment_tx(initial_serai_key: &PublicKey) -> TxLegacy {
Deployer::deploy_tx(Self::init_code(initial_serai_key))
2024-10-31 02:23:59 -04:00
}
2024-09-17 01:04:08 -04:00
/// Create a new view of the Router.
///
/// This performs an on-chain lookup for the first deployed Router constructed with this public
/// key. This lookup is of a constant amount of calls and does not read any logs.
pub async fn new(
provider: Arc<RootProvider<SimpleRequest>>,
initial_serai_key: &PublicKey,
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
let Some(deployer) = Deployer::new(provider.clone()).await? else {
return Ok(None);
};
let Some(address) = deployer
2024-09-17 01:04:08 -04:00
.find_deployment(ethereum_primitives::keccak256(Self::init_code(initial_serai_key)))
.await?
else {
return Ok(None);
};
Ok(Some(Self { provider, address }))
2024-09-17 01:04:08 -04:00
}
/// The address of the router.
pub fn address(&self) -> Address {
self.address
2024-09-17 01:04:08 -04:00
}
/// Get the message to be signed in order to confirm the next key for Serai.
pub fn confirm_next_serai_key_message(chain_id: U256, nonce: u64) -> Vec<u8> {
abi::confirmNextSeraiKeyCall::new((abi::Signature {
c: chain_id.into(),
s: U256::try_from(nonce).unwrap().into(),
},))
.abi_encode()
}
/// Construct a transaction to confirm the next key representing Serai.
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
..Default::default()
}
}
/// Get the message to be signed in order to update the key for Serai.
pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> {
abi::updateSeraiKeyCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
key.eth_repr().into(),
))
.abi_encode()
}
2024-09-17 01:04:08 -04:00
/// Construct a transaction to update the key representing Serai.
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::updateSeraiKeyCall::new((
abi::Signature::from(sig),
public_key.eth_repr().into(),
))
.abi_encode()
.into(),
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
2024-09-17 01:04:08 -04:00
..Default::default()
}
}
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
pub fn execute_message(
chain_id: U256,
nonce: u64,
coin: Coin,
fee: U256,
outs: OutInstructions,
) -> Vec<u8> {
abi::executeCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
Address::from(coin),
fee,
outs.0,
))
.abi_encode()
}
2024-09-17 01:04:08 -04:00
/// Construct a transaction to execute a batch of `OutInstruction`s.
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
// TODO
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
2024-09-17 01:04:08 -04:00
TxLegacy {
to: TxKind::Call(self.address),
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
.abi_encode()
.into(),
gas_limit: gas_limit * 120 / 100,
2024-09-17 01:04:08 -04:00
..Default::default()
}
}
2024-12-09 02:00:17 -05:00
/// Get the message to be signed in order to trigger the escape hatch.
pub fn escape_hatch_message(chain_id: U256, nonce: u64, escape_to: Address) -> Vec<u8> {
2024-12-09 02:00:17 -05:00
abi::escapeHatchCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
2024-12-09 02:00:17 -05:00
escape_to,
))
.abi_encode()
}
/// Construct a transaction to trigger the escape hatch.
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
2024-12-09 02:00:17 -05:00
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
..Default::default()
}
}
/// Construct a transaction to escape coins via the escape hatch.
pub fn escape(&self, coin: Coin) -> TxLegacy {
2024-12-09 02:00:17 -05:00
TxLegacy {
to: TxKind::Call(self.address),
input: abi::escapeCall::new((Address::from(coin),)).abi_encode().into(),
2024-12-09 02:00:17 -05:00
..Default::default()
}
}
2024-09-17 01:04:08 -04:00
/// Fetch the `InInstruction`s emitted by the Router from this block.
pub async fn in_instructions(
&self,
block: u64,
2024-12-09 02:00:17 -05:00
allowed_tokens: &HashSet<Address>,
2024-09-17 01:04:08 -04:00
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// The InInstruction events for this block
let filter = Filter::new().from_block(block).to_block(block).address(self.address);
2024-09-17 01:04:08 -04:00
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
let mut logs = self.provider.get_logs(&filter).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
2024-09-17 01:04:08 -04:00
/*
We check that for all InInstructions for ERC20s emitted, a corresponding transfer occurred.
In order to prevent a transfer from being used to justify multiple distinct InInstructions,
we insert the transfer's log index into this HashSet.
*/
let mut transfer_check = HashSet::new();
let mut in_instructions = vec![];
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
2024-09-17 01:04:08 -04:00
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
let id = LogIndex {
block_hash: log
2024-09-17 01:04:08 -04:00
.block_hash
.ok_or_else(|| {
TransportErrorKind::Custom("log didn't have its block hash set".to_string().into())
})?
.into(),
index_within_block: log.log_index.ok_or_else(|| {
2024-09-17 01:04:08 -04:00
TransportErrorKind::Custom("log didn't have its index set".to_string().into())
})?,
};
2024-09-17 01:04:08 -04:00
let transaction_hash = log.transaction_hash.ok_or_else(|| {
2024-09-17 01:04:08 -04:00
TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into())
})?;
let log = log
.log_decode::<InInstructionEvent>()
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to InInstructionEvent yet couldn't decode log: {e:?}").into(),
)
})?
.inner
.data;
let coin = Coin::from(log.coin);
if let Coin::Erc20(token) = coin {
2024-09-17 01:04:08 -04:00
if !allowed_tokens.contains(&token) {
continue;
}
// Get all logs for this TX
let receipt =
self.provider.get_transaction_receipt(transaction_hash).await?.ok_or_else(|| {
TransportErrorKind::Custom(
"node didn't have the receipt for a transaction it had".to_string().into(),
)
})?;
2024-09-17 01:04:08 -04:00
let tx_logs = receipt.inner.logs();
/*
The transfer which causes an InInstruction event won't be a top-level transfer.
Accordingly, when looking for the matching transfer, disregard the top-level transfer (if
one exists).
2024-09-17 01:04:08 -04:00
*/
if let Some(matched) =
Erc20::match_top_level_transfer(&self.provider, transaction_hash, self.address).await?
{
// Mark this log index as used so it isn't used again
transfer_check.insert(matched.id.index_within_block);
}
2024-09-17 01:04:08 -04:00
// Find a matching transfer log
let mut found_transfer = false;
for tx_log in tx_logs {
let log_index = tx_log.log_index.ok_or_else(|| {
TransportErrorKind::Custom(
"log in transaction receipt didn't have its log index set".to_string().into(),
)
})?;
2024-09-17 01:04:08 -04:00
// Ensure we didn't already use this transfer to check a distinct InInstruction event
if transfer_check.contains(&log_index) {
continue;
}
// Check if this log is from the token we expected to be transferred
2024-12-09 02:00:17 -05:00
if tx_log.address() != token {
2024-09-17 01:04:08 -04:00
continue;
}
// Check if this is a transfer log
// https://github.com/alloy-rs/core/issues/589
if tx_log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
2024-09-17 01:04:08 -04:00
continue;
}
let Ok(transfer) = Transfer::decode_log(&tx_log.inner.clone(), true) else { continue };
// Check if this is a transfer to us for the expected amount
if (transfer.to == self.address) && (transfer.value == log.amount) {
2024-09-17 01:04:08 -04:00
transfer_check.insert(log_index);
found_transfer = true;
break;
}
}
if !found_transfer {
// This shouldn't be a simple error
// This is an exploit, a non-conforming ERC20, or a malicious connection
// This should halt the process. While this is sufficient, it's sub-optimal
// TODO
Err(TransportErrorKind::Custom(
"ERC20 InInstruction with no matching transfer log".to_string().into(),
))?;
}
};
in_instructions.push(InInstruction {
id,
transaction_hash: *transaction_hash,
from: log.from,
2024-09-17 01:04:08 -04:00
coin,
amount: log.amount,
data: log.instruction.as_ref().to_vec(),
});
}
Ok(in_instructions)
}
/// Fetch the executed actions from this block.
pub async fn executed(
&self,
from_block: u64,
to_block: u64,
) -> Result<Vec<Executed>, RpcError<TransportErrorKind>> {
fn decode<E: SolEvent>(log: &Log) -> Result<E, RpcError<TransportErrorKind>> {
Ok(
log
.log_decode::<E>()
2024-09-17 01:04:08 -04:00
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to event yet couldn't decode log: {e:?}").into(),
2024-09-17 01:04:08 -04:00
)
})?
.inner
.data,
)
2024-09-17 01:04:08 -04:00
}
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let mut logs = self.provider.get_logs(&filter).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
2024-09-17 01:04:08 -04:00
let mut res = vec![];
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
2024-09-17 01:04:08 -04:00
match log.topics().first() {
Some(&NextSeraiKeySetEvent::SIGNATURE_HASH) => {
let event = decode::<NextSeraiKeySetEvent>(&log)?;
res.push(Executed::NextSeraiKeySet {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
key: event.key.into(),
});
}
Some(&SeraiKeyUpdatedEvent::SIGNATURE_HASH) => {
let event = decode::<SeraiKeyUpdatedEvent>(&log)?;
res.push(Executed::SeraiKeyUpdated {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
key: event.key.into(),
});
}
Some(&BatchEvent::SIGNATURE_HASH) => {
let event = decode::<BatchEvent>(&log)?;
res.push(Executed::Batch {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
message_hash: event.messageHash.into(),
});
}
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
let event = decode::<EscapeHatchEvent>(&log)?;
res.push(Executed::EscapeHatch {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
escape_to: event.escapeTo,
});
}
Some(&InInstructionEvent::SIGNATURE_HASH | &EscapedEvent::SIGNATURE_HASH) => {}
unrecognized => Err(TransportErrorKind::Custom(
format!("unrecognized event yielded by the Router: {:?}", unrecognized.map(hex::encode))
.into(),
))?,
2024-09-17 01:04:08 -04:00
}
}
Ok(res)
}
/// Fetch the `Escape`s from the smart contract through the escape hatch.
pub async fn escapes(
&self,
from_block: u64,
to_block: u64,
) -> Result<Vec<Escape>, RpcError<TransportErrorKind>> {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let mut logs =
self.provider.get_logs(&filter.event_signature(EscapedEvent::SIGNATURE_HASH)).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
let mut res = vec![];
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
// Double check the topic
if log.topics().first() != Some(&EscapedEvent::SIGNATURE_HASH) {
Err(TransportErrorKind::Custom(
"node returned a log for a different topic than filtered to".to_string().into(),
))?;
}
let log = log
.log_decode::<EscapedEvent>()
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to event yet couldn't decode log: {e:?}").into(),
)
})?
.inner
.data;
res.push(Escape { coin: Coin::from(log.coin), amount: log.amount });
}
2024-09-17 01:04:08 -04:00
Ok(res)
}
2024-10-31 02:23:59 -04:00
async fn fetch_key(
&self,
block: BlockId,
call: Vec<u8>,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
let call =
TransactionRequest::default().to(self.address).input(TransactionInput::new(call.into()));
let bytes = self.provider.call(&call).block(block).await?;
// This is fine as both key calls share a return type
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
let eth_repr = <[u8; 32]>::from(res._0);
Ok(if eth_repr == [0; 32] {
None
} else {
Some(PublicKey::from_eth_repr(eth_repr).ok_or_else(|| {
2024-10-31 02:23:59 -04:00
TransportErrorKind::Custom("invalid key set on router".to_string().into())
})?)
})
}
/// Fetch the next key for Serai's Ethereum validators
pub async fn next_key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::nextSeraiKeyCall::new(()).abi_encode()).await
}
/// Fetch the current key for Serai's Ethereum validators
pub async fn key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::seraiKeyCall::new(()).abi_encode()).await
2024-10-31 02:23:59 -04:00
}
/// Fetch the nonce of the next action to execute
pub async fn next_nonce(&self, block: BlockId) -> Result<u64, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default()
.to(self.address)
2024-10-31 02:23:59 -04:00
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
let bytes = self.provider.call(&call).block(block).await?;
2024-10-31 02:23:59 -04:00
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
2024-10-31 02:23:59 -04:00
Ok(u64::try_from(res._0).map_err(|_| {
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
})?)
}
/// Fetch the address the escape hatch was set to
pub async fn escaped_to(
&self,
block: BlockId,
) -> Result<Option<Address>, RpcError<TransportErrorKind>> {
2024-10-31 02:23:59 -04:00
let call = TransactionRequest::default()
.to(self.address)
2024-10-31 02:23:59 -04:00
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
let bytes = self.provider.call(&call).block(block).await?;
2024-10-31 02:23:59 -04:00
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
2024-10-31 02:23:59 -04:00
})?;
Ok(if res._0 == Address([0; 20].into()) { None } else { Some(res._0) })
2024-10-31 02:23:59 -04:00
}
2024-09-17 01:04:08 -04:00
}