Support new Gelato relay API (#1041)

* All compiles

* Rename files fwd request -> sponsored call

* More scaffolding to get the API key in there

* Cleaning up

* Rename task_status_call to task_status

* Finish rename

* rm useForDisabledOriginChains

* Introduce TransactionSubmitterType

* submitter type -> submission type

* Pass around gelato_config instead of the sponsor api key

* Final cleanup

* Nit

* Use default tx submission type

* Some renames

* Nits after some testing
pull/1081/head
Trevor Porter 2 years ago committed by GitHub
parent a1ce16b425
commit ea5b584fe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      rust/Cargo.lock
  2. 21
      rust/abacus-base/src/settings/chains.rs
  3. 5
      rust/abacus-base/src/settings/mod.rs
  4. 64
      rust/agents/relayer/src/msg/gelato_submitter/mod.rs
  5. 161
      rust/agents/relayer/src/msg/gelato_submitter/sponsored_call_op.rs
  6. 34
      rust/agents/relayer/src/relayer.rs
  7. 1
      rust/gelato/Cargo.toml
  8. 184
      rust/gelato/src/chains.rs
  9. 27
      rust/gelato/src/err.rs
  10. 165
      rust/gelato/src/fwd_req_call.rs
  11. 115
      rust/gelato/src/fwd_req_sig.rs
  12. 11
      rust/gelato/src/lib.rs
  13. 150
      rust/gelato/src/sponsored_call.rs
  14. 75
      rust/gelato/src/task_status.rs
  15. 122
      rust/gelato/src/task_status_call.rs
  16. 94
      rust/gelato/src/test_data.rs
  17. 81
      rust/gelato/src/types.rs
  18. 12
      rust/helm/abacus-agent/templates/external-secret.yaml
  19. 1
      rust/helm/abacus-agent/values.yaml
  20. 3
      typescript/infra/config/environments/testnet2/agent.ts
  21. 15
      typescript/infra/src/agents/index.ts
  22. 43
      typescript/infra/src/config/agent.ts

12
rust/Cargo.lock generated

@ -1913,6 +1913,7 @@ dependencies = [
"rustc-hex",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tokio",
"tracing",
@ -4162,6 +4163,17 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"

@ -32,13 +32,23 @@ impl Default for ChainConf {
}
}
/// Ways in which transactions can be submitted to a blockchain.
#[derive(Copy, Clone, Debug, Default, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum TransactionSubmissionType {
/// Use the configured signer to sign and submit transactions in the "default" manner.
#[default]
Signer,
/// Submit transactions via the Gelato relay.
Gelato,
}
/// Configuration for using the Gelato Relay to interact with some chain.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GelatoConf {
/// Whether to use the Gelato Relay service for transactions submitted to
/// the chain.
pub enabled: String,
/// The sponsor API key for submitting sponsored calls
pub sponsorapikey: String,
}
/// Addresses for outbox chain contracts
@ -77,8 +87,9 @@ pub struct ChainSetup<T> {
/// The chain connection details
#[serde(flatten)]
pub chain: ChainConf,
/// Gelato configuration for this chain (Gelato unused if None)
pub gelato: Option<GelatoConf>,
/// How transactions to this chain are submitted.
#[serde(default)]
pub txsubmission: TransactionSubmissionType,
/// Set this key to disable the inbox. Does nothing for outboxes.
#[serde(default)]
pub disabled: Option<String>,

@ -100,6 +100,8 @@ pub use chains::{ChainConf, ChainSetup, InboxAddresses, OutboxAddresses};
use crate::{settings::trace::TracingConfig, CachingInterchainGasPaymaster};
use crate::{AbacusAgentCore, CachingInbox, CachingOutbox, CoreMetrics, InboxContracts};
use self::chains::GelatoConf;
/// Chain configuration
pub mod chains;
@ -232,6 +234,8 @@ pub struct Settings {
pub tracing: TracingConfig,
/// Transaction signers
pub signers: HashMap<String, SignerConf>,
/// Gelato config
pub gelato: Option<GelatoConf>,
}
impl Settings {
@ -246,6 +250,7 @@ impl Settings {
inboxes: self.inboxes.clone(),
tracing: self.tracing.clone(),
signers: self.signers.clone(),
gelato: self.gelato.clone(),
}
}
}

@ -1,50 +1,45 @@
use std::sync::Arc;
use abacus_base::chains::GelatoConf;
use abacus_base::{CoreMetrics, InboxContracts};
use abacus_core::db::AbacusDB;
use abacus_core::AbacusCommon;
use abacus_core::{db::AbacusDB, Signers};
use ethers::signers::Signer;
use ethers::types::Address;
use eyre::{bail, Result};
use gelato::chains::Chain;
use gelato::types::Chain;
use prometheus::{Histogram, IntCounter, IntGauge};
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::time::{sleep, Duration, Instant};
use tokio::{sync::mpsc::error::TryRecvError, task::JoinHandle};
use tracing::{info_span, instrument::Instrumented, Instrument};
use crate::msg::gelato_submitter::fwd_req_op::{
ForwardRequestOp, ForwardRequestOpArgs, ForwardRequestOptions,
use crate::msg::gelato_submitter::sponsored_call_op::{
SponsoredCallOp, SponsoredCallOpArgs, SponsoredCallOptions,
};
use super::gas_payment_enforcer::GasPaymentEnforcer;
use super::SubmitMessageArgs;
mod fwd_req_op;
mod sponsored_call_op;
#[derive(Debug)]
pub(crate) struct GelatoSubmitter {
/// The Gelato config.
gelato_config: GelatoConf,
/// Source of messages to submit.
message_receiver: mpsc::UnboundedReceiver<SubmitMessageArgs>,
/// Inbox / InboxValidatorManager on the destination chain.
inbox_contracts: InboxContracts,
/// The outbox chain in the format expected by the Gelato crate.
outbox_gelato_chain: Chain,
/// The inbox chain in the format expected by the Gelato crate.
inbox_gelato_chain: Chain,
/// The signer of the Gelato sponsor, used for EIP-712 meta-transaction signatures.
gelato_sponsor_signer: Signers,
/// The address of the Gelato sponsor.
gelato_sponsor_address: Address,
/// Interface to agent rocks DB for e.g. writing delivery status upon completion.
db: AbacusDB,
/// Shared reqwest HTTP client to use for any ops to Gelato endpoints.
http_client: reqwest::Client,
/// Prometheus metrics.
metrics: GelatoSubmitterMetrics,
/// Channel used by ForwardRequestOps to send that their message has been successfully processed.
/// Channel used by SponsoredCallOps to send that their message has been successfully processed.
message_processed_sender: UnboundedSender<SubmitMessageArgs>,
/// Channel to receive from ForwardRequestOps that a message has been successfully processed.
/// Channel to receive from SponsoredCallOps that a message has been successfully processed.
message_processed_receiver: UnboundedReceiver<SubmitMessageArgs>,
/// Used to determine if messages have made sufficient gas payments.
gas_payment_enforcer: Arc<GasPaymentEnforcer>,
@ -53,10 +48,9 @@ pub(crate) struct GelatoSubmitter {
impl GelatoSubmitter {
pub fn new(
message_receiver: mpsc::UnboundedReceiver<SubmitMessageArgs>,
outbox_domain: u32,
inbox_contracts: InboxContracts,
abacus_db: AbacusDB,
gelato_sponsor_signer: Signers,
gelato_config: GelatoConf,
metrics: GelatoSubmitterMetrics,
gas_payment_enforcer: Arc<GasPaymentEnforcer>,
) -> Self {
@ -64,13 +58,11 @@ impl GelatoSubmitter {
mpsc::unbounded_channel::<SubmitMessageArgs>();
Self {
message_receiver,
outbox_gelato_chain: abacus_domain_to_gelato_chain(outbox_domain).unwrap(),
inbox_gelato_chain: abacus_domain_to_gelato_chain(inbox_contracts.inbox.local_domain())
.unwrap(),
inbox_contracts,
db: abacus_db,
gelato_sponsor_address: gelato_sponsor_signer.address(),
gelato_sponsor_signer,
gelato_config,
http_client: reqwest::Client::new(),
metrics,
message_processed_sender,
@ -109,27 +101,25 @@ impl GelatoSubmitter {
}
}
// Spawn a ForwardRequestOp for each received message.
// Spawn a SponsoredCallOp for each received message.
for msg in received_messages.into_iter() {
tracing::info!(msg=?msg, "Spawning forward request op for message");
let mut op = ForwardRequestOp::new(ForwardRequestOpArgs {
opts: ForwardRequestOptions::default(),
tracing::info!(msg=?msg, "Spawning sponsored call op for message");
let mut op = SponsoredCallOp::new(SponsoredCallOpArgs {
opts: SponsoredCallOptions::default(),
http: self.http_client.clone(),
message: msg,
inbox_contracts: self.inbox_contracts.clone(),
sponsor_signer: self.gelato_sponsor_signer.clone(),
sponsor_address: self.gelato_sponsor_address,
sponsor_chain: self.outbox_gelato_chain,
sponsor_api_key: self.gelato_config.sponsorapikey.clone(),
destination_chain: self.inbox_gelato_chain,
message_processed_sender: self.message_processed_sender.clone(),
gas_payment_enforcer: self.gas_payment_enforcer.clone(),
});
self.metrics.active_forward_request_ops_gauge.add(1);
self.metrics.active_sponsored_call_ops_gauge.add(1);
tokio::spawn(async move { op.run().await });
}
// Pull any messages that have been successfully processed by ForwardRequestOps
// Pull any messages that have been successfully processed by SponsoredCallOps
loop {
match self.message_processed_receiver.try_recv() {
Ok(msg) => {
@ -156,7 +146,7 @@ impl GelatoSubmitter {
tracing::info!(msg=?msg, "Recording message as successfully processed");
self.db.mark_leaf_as_processed(msg.leaf_index)?;
self.metrics.active_forward_request_ops_gauge.sub(1);
self.metrics.active_sponsored_call_ops_gauge.sub(1);
self.metrics
.queue_duration_hist
.observe((Instant::now() - msg.enqueue_time).as_secs_f64());
@ -175,7 +165,7 @@ pub(crate) struct GelatoSubmitterMetrics {
queue_duration_hist: Histogram,
processed_gauge: IntGauge,
messages_processed_count: IntCounter,
active_forward_request_ops_gauge: IntGauge,
active_sponsored_call_ops_gauge: IntGauge,
/// Private state used to update actual metrics each tick.
highest_submitted_leaf_index: u32,
}
@ -194,9 +184,11 @@ impl GelatoSubmitterMetrics {
outbox_chain,
inbox_chain,
]),
active_forward_request_ops_gauge: metrics
.submitter_queue_length()
.with_label_values(&[outbox_chain, inbox_chain, "active_forward_request_ops"]),
active_sponsored_call_ops_gauge: metrics.submitter_queue_length().with_label_values(&[
outbox_chain,
inbox_chain,
"active_sponsored_call_ops",
]),
highest_submitted_leaf_index: 0,
}
}
@ -210,10 +202,10 @@ fn abacus_domain_to_gelato_chain(domain: u32) -> Result<Chain> {
5 => Chain::Goerli,
1886350457 => Chain::Polygon,
80001 => Chain::PolygonMumbai,
80001 => Chain::Mumbai,
1635148152 => Chain::Avalanche,
43113 => Chain::AvalancheFuji,
43113 => Chain::Fuji,
6386274 => Chain::Arbitrum,
421611 => Chain::ArbitrumRinkeby,

@ -2,18 +2,11 @@ use std::{ops::Deref, sync::Arc, time::Duration};
use abacus_base::InboxContracts;
use abacus_core::{ChainCommunicationError, Inbox, InboxValidatorManager, MessageStatus};
use ethers::{
signers::Signer,
types::{H160, U256},
};
use eyre::Result;
use gelato::{
chains::Chain,
fwd_req_call::{
ForwardRequestArgs, ForwardRequestCall, ForwardRequestCallResult, PaymentType,
NATIVE_FEE_TOKEN_ADDRESS,
},
task_status_call::{TaskStatus, TaskStatusCall, TaskStatusCallArgs},
sponsored_call::{SponsoredCallApiCall, SponsoredCallApiCallResult, SponsoredCallArgs},
task_status::{TaskState, TaskStatusApiCall, TaskStatusApiCallArgs},
types::Chain,
};
use tokio::{
sync::mpsc::UnboundedSender,
@ -23,37 +16,21 @@ use tracing::instrument;
use crate::msg::{gas_payment_enforcer::GasPaymentEnforcer, SubmitMessageArgs};
/// The max fee to use for Gelato ForwardRequests.
/// Gelato isn't charging fees on testnet. For now, use this hardcoded value
/// of 1e18, or 1.0 ether.
/// TODO: revisit before running on mainnet and when we consider interchain
/// gas payments.
const DEFAULT_MAX_FEE: u64 = 10u64.pow(18);
/// The default gas limit to use for Gelato ForwardRequests, arbitrarily chose
/// to be 5M.
/// TODO: once Gelato fully deploys their new version, simply omit the gas
/// limit so that Gelato does the estimation for us.
const DEFAULT_GAS_LIMIT: u64 = 5000000;
/// The period to sleep between ticks, in seconds.
const TICK_SLEEP_PERIOD_SECS: u64 = 5;
// The number of seconds after a tick to sleep before attempting the next tick.
const TICK_SLEEP_DURATION_SECONDS: u64 = 30;
/// The period to sleep after observing the message's gas payment
/// as insufficient, in secs.
const INSUFFICIENT_GAS_PAYMENT_SLEEP_PERIOD_SECS: u64 = 15;
#[derive(Debug, Clone)]
pub struct ForwardRequestOpArgs<S> {
pub opts: ForwardRequestOptions,
pub struct SponsoredCallOpArgs {
pub opts: SponsoredCallOptions,
pub http: reqwest::Client,
pub message: SubmitMessageArgs,
pub inbox_contracts: InboxContracts,
pub sponsor_signer: S,
pub sponsor_address: H160,
// Currently unused due to a bug in Gelato's testnet relayer that is currently being upgraded.
pub sponsor_chain: Chain,
pub sponsor_api_key: String,
pub destination_chain: Chain,
pub gas_payment_enforcer: Arc<GasPaymentEnforcer>,
@ -63,22 +40,18 @@ pub struct ForwardRequestOpArgs<S> {
}
#[derive(Debug, Clone)]
pub struct ForwardRequestOp<S>(ForwardRequestOpArgs<S>);
pub struct SponsoredCallOp(SponsoredCallOpArgs);
impl<S> Deref for ForwardRequestOp<S> {
type Target = ForwardRequestOpArgs<S>;
impl Deref for SponsoredCallOp {
type Target = SponsoredCallOpArgs;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<S> ForwardRequestOp<S>
where
S: Signer,
S::Error: 'static,
{
pub fn new(args: ForwardRequestOpArgs<S>) -> Self {
impl SponsoredCallOp {
pub fn new(args: SponsoredCallOpArgs) -> Self {
Self(args)
}
@ -100,17 +73,19 @@ where
Err(err) => {
tracing::warn!(
err=?err,
"Error occurred in fwd_req_op tick",
"Error occurred in sponsored_call_op tick",
);
}
_ => {}
}
self.0.message.num_retries += 1;
sleep(Duration::from_secs(TICK_SLEEP_PERIOD_SECS)).await;
sleep(Duration::from_secs(TICK_SLEEP_DURATION_SECONDS)).await;
}
}
/// One tick will submit a sponsored call to Gelato and wait for a terminal state
/// or timeout.
async fn tick(&self) -> Result<MessageStatus> {
// Before doing anything, first check if the message has already been processed.
if let Ok(MessageStatus::Processed) = self.message_status().await {
@ -131,19 +106,18 @@ where
return Ok(MessageStatus::None);
}
// Send the forward request.
let fwd_req_result = self.send_forward_request_call().await?;
// Send the sponsored call.
let sponsored_call_result = self.send_sponsored_call_api_call().await?;
tracing::info!(
msg=?self.0.message,
gas_payment=?gas_payment,
task_id=fwd_req_result.task_id,
"Sent forward request",
task_id=sponsored_call_result.task_id,
"Sent sponsored call",
);
// Wait for a terminal state, timing out according to the retry_submit_interval.
match timeout(
self.0.opts.retry_submit_interval,
self.poll_for_terminal_state(fwd_req_result.task_id.clone()),
self.poll_for_terminal_state(sponsored_call_result.task_id.clone()),
)
.await
{
@ -154,7 +128,7 @@ where
// If a timeout occurred, don't bubble up an error, instead just log
// and set ourselves up for the next tick.
Err(err) => {
tracing::debug!(err=?err, "Forward request timed out, reattempting");
tracing::info!(err=?err, "Sponsored call timed out, reattempting");
Ok(MessageStatus::None)
}
}
@ -173,62 +147,56 @@ where
return Ok(MessageStatus::Processed);
}
// Get the status of the ForwardRequest task from Gelato for debugging.
// Get the status of the SponsoredCall task from Gelato for debugging.
// If the task was cancelled for some reason by Gelato, stop waiting.
let status_call = TaskStatusCall {
http: Arc::new(self.0.http.clone()),
args: TaskStatusCallArgs {
let task_status_api_call = TaskStatusApiCall {
http: self.0.http.clone(),
args: TaskStatusApiCallArgs {
task_id: task_id.clone(),
},
};
let status_result = status_call.run().await?;
if let [tx_status] = &status_result.data[..] {
tracing::info!(
task_id=task_id,
tx_status=?tx_status,
"Polled forward request status",
);
// The only terminal state status is if the task was cancelled, which happens after
// Gelato has known about the task for ~20 minutes and could not execute it.
if let TaskStatus::Cancelled = tx_status.task_state {
return Ok(MessageStatus::None);
}
} else {
tracing::warn!(
task_id=task_id,
status_result_data=?status_result.data,
"Unexpected forward request status data",
);
let task_status_result = task_status_api_call.run().await?;
let task_state = task_status_result.task_state();
tracing::info!(
task_id=task_id,
task_state=?task_state,
task_status_result=?task_status_result,
"Polled sponsored call status",
);
// The only terminal state status is if the task was cancelled, which happens after
// Gelato has reached the max # of retries for a task. Currently, the default is
// after about 30 seconds.
if let TaskState::Cancelled = task_state {
return Ok(MessageStatus::None);
}
}
}
// Once gas payments are enforced, we will likely fetch the gas payment from
// the DB here. This is why forward request args are created and signed for each
// forward request call.
async fn send_forward_request_call(&self) -> Result<ForwardRequestCallResult> {
let args = self.create_forward_request_args();
let signature = self.0.sponsor_signer.sign_typed_data(&args).await?;
let fwd_req_call = ForwardRequestCall {
args,
// the DB here. This is why sponsored call args are created and signed for each
// sponsored call call.
async fn send_sponsored_call_api_call(&self) -> Result<SponsoredCallApiCallResult> {
let args = self.create_sponsored_call_args();
let sponsored_call_api_call = SponsoredCallApiCall {
args: &args,
http: self.0.http.clone(),
signature,
sponsor_api_key: &self.sponsor_api_key,
};
Ok(fwd_req_call.run().await?)
Ok(sponsored_call_api_call.run().await?)
}
fn create_forward_request_args(&self) -> ForwardRequestArgs {
fn create_sponsored_call_args(&self) -> SponsoredCallArgs {
let calldata = self.0.inbox_contracts.validator_manager.process_calldata(
&self.0.message.checkpoint,
&self.0.message.committed_message.message,
&self.0.message.proof,
);
ForwardRequestArgs {
SponsoredCallArgs {
chain_id: self.0.destination_chain,
target: self
.inbox_contracts
@ -236,19 +204,8 @@ where
.contract_address()
.into(),
data: calldata.into(),
fee_token: NATIVE_FEE_TOKEN_ADDRESS,
payment_type: PaymentType::AsyncGasTank,
max_fee: DEFAULT_MAX_FEE.into(),
gas: DEFAULT_GAS_LIMIT.into(),
// At the moment, there's a bug with Gelato where environments that don't charge
// fees (i.e. testnet) require the sponsor chain ID to be the same as the chain
// in which the tx will be sent to.
// This will be fixed in an upcoming release they're doing.
sponsor_chain_id: self.0.destination_chain,
nonce: U256::zero(),
enforce_sponsor_nonce: false,
enforce_sponsor_nonce_ordering: false,
sponsor: self.0.sponsor_address,
gas_limit: None, // Gelato will handle gas estimation
retries: None, // Use Gelato's default of 5 retries, each ~5 seconds apart
}
}
@ -267,16 +224,16 @@ where
}
#[derive(Debug, Clone)]
pub struct ForwardRequestOptions {
pub struct SponsoredCallOptions {
pub poll_interval: Duration,
pub retry_submit_interval: Duration,
}
impl Default for ForwardRequestOptions {
impl Default for SponsoredCallOptions {
fn default() -> Self {
Self {
poll_interval: Duration::from_secs(60),
retry_submit_interval: Duration::from_secs(20 * 60),
poll_interval: Duration::from_secs(20),
retry_submit_interval: Duration::from_secs(60),
}
}
}

@ -1,5 +1,6 @@
use std::sync::Arc;
use abacus_base::chains::TransactionSubmissionType;
use async_trait::async_trait;
use eyre::Result;
use tokio::{sync::mpsc, sync::watch, task::JoinHandle};
@ -9,7 +10,7 @@ use abacus_base::{
chains::GelatoConf, run_all, AbacusAgentCore, Agent, BaseAgent, CachingInterchainGasPaymaster,
ContractSyncMetrics, InboxContracts, MultisigCheckpointSyncer,
};
use abacus_core::{AbacusCommon, AbacusContract, MultisigSignedCheckpoint, Signers};
use abacus_core::{AbacusContract, MultisigSignedCheckpoint, Signers};
use crate::msg::gas_payment_enforcer::GasPaymentEnforcer;
use crate::msg::gelato_submitter::{GelatoSubmitter, GelatoSubmitterMetrics};
@ -101,7 +102,8 @@ impl BaseAgent for Relayer {
tasks.push(self.run_inbox(
inbox_contracts.clone(),
signed_checkpoint_receiver.clone(),
self.core.settings.inboxes[inbox_name].gelato.as_ref(),
self.core.settings.inboxes[inbox_name].txsubmission,
self.core.settings.gelato.as_ref(),
signer,
gas_payment_enforcer.clone(),
));
@ -160,16 +162,15 @@ impl Relayer {
&self,
message_receiver: mpsc::UnboundedReceiver<SubmitMessageArgs>,
inbox_contracts: InboxContracts,
signer: Signers,
gelato_config: GelatoConf,
gas_payment_enforcer: Arc<GasPaymentEnforcer>,
) -> GelatoSubmitter {
let inbox_chain_name = inbox_contracts.inbox.chain_name().to_owned();
GelatoSubmitter::new(
message_receiver,
self.outbox().local_domain(),
inbox_contracts,
self.outbox().db().clone(),
signer,
gelato_config,
GelatoSubmitterMetrics::new(
&self.core.metrics,
self.outbox().outbox().chain_name(),
@ -184,28 +185,36 @@ impl Relayer {
&self,
inbox_contracts: InboxContracts,
signed_checkpoint_receiver: watch::Receiver<Option<MultisigSignedCheckpoint>>,
gelato: Option<&GelatoConf>,
tx_submission: TransactionSubmissionType,
gelato_config: Option<&GelatoConf>,
signer: Signers,
gas_payment_enforcer: Arc<GasPaymentEnforcer>,
) -> Instrumented<JoinHandle<Result<()>>> {
let outbox = self.outbox().outbox();
let outbox_name = outbox.chain_name();
let inbox_name = inbox_contracts.inbox.chain_name();
let metrics = MessageProcessorMetrics::new(
&self.core.metrics,
outbox_name,
inbox_contracts.inbox.chain_name(),
);
let (msg_send, msg_receive) = mpsc::unbounded_channel();
let submit_fut = match gelato {
Some(cfg) if cfg.enabled.parse::<bool>().unwrap() => self
.make_gelato_submitter_for_inbox(
let submit_fut = match tx_submission {
TransactionSubmissionType::Gelato => {
let gelato_config = gelato_config.unwrap_or_else(|| {
panic!("Expected GelatoConf for inbox {} using Gelato", inbox_name)
});
self.make_gelato_submitter_for_inbox(
msg_receive,
inbox_contracts.clone(),
signer,
gelato_config.clone(),
gas_payment_enforcer,
)
.spawn(),
_ => {
.spawn()
}
TransactionSubmissionType::Signer => {
let serial_submitter = SerialSubmitter::new(
msg_receive,
inbox_contracts.clone(),
@ -220,6 +229,7 @@ impl Relayer {
serial_submitter.spawn()
}
};
let message_processor = MessageProcessor::new(
outbox.clone(),
self.outbox().db().clone(),

@ -8,6 +8,7 @@ async-trait = { version = "0.1", default-features = false }
ethers = { git = "https://github.com/abacus-network/ethers-rs", tag = "2022-09-12-01" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", default-features = false }
serde_repr = "0.1.9"
tokio = { version = "1", features = ["macros"] }
reqwest = { version = "0", features = ["json"]}
rustc-hex = { version = "2" }

@ -1,184 +0,0 @@
// Ideally we would avoid duplicating ethers::types::Chain, but we have enough need to justify
// a separate, more complete type conversion setup, including some helpers for e.g. locating
// Gelato's verifying contracts. Converting from the chain's name in string format is useful
// for CLI usage so that we don't have to remember chain IDs and can instead refer to names.
use ethers::types::{Address, U256};
use std::{fmt, str::FromStr};
use crate::err::GelatoError;
// This list is currently trimmed to the *intersection* of
// {chains used by Abacus in any environment} and {chains included in ethers::types::Chain}.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Chain {
Ethereum = 1,
Rinkeby = 4,
Goerli = 5,
Kovan = 42,
Polygon = 137,
PolygonMumbai = 80001,
Avalanche = 43114,
AvalancheFuji = 43113,
Arbitrum = 42161,
ArbitrumRinkeby = 421611,
Optimism = 10,
OptimismKovan = 69,
BinanceSmartChain = 56,
BinanceSmartChainTestnet = 97,
Celo = 42220,
Alfajores = 44787,
MoonbaseAlpha = 1287,
}
impl fmt::Display for Chain {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "{:?}", self)
}
}
impl FromStr for Chain {
type Err = GelatoError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
// TODO: confirm the unusual chain name strings used by Gelato,
// e.g. mainnet for Ethereum and arbitrumtestnet for Arbitrum Rinkeby.
"mainnet" => Ok(Chain::Ethereum),
"rinkeby" => Ok(Chain::Rinkeby),
"goerli" => Ok(Chain::Goerli),
"kovan" => Ok(Chain::Kovan),
"polygon" => Ok(Chain::Polygon),
"polygonmumbai" => Ok(Chain::PolygonMumbai),
"avalanche" => Ok(Chain::Avalanche),
"avalanchefuji" => Ok(Chain::AvalancheFuji),
"arbitrum" => Ok(Chain::Arbitrum),
"arbitrumtestnet" => Ok(Chain::ArbitrumRinkeby),
"optimism" => Ok(Chain::Optimism),
"optimismkovan" => Ok(Chain::OptimismKovan),
"bsc" => Ok(Chain::BinanceSmartChain),
"bsc-testnet" => Ok(Chain::BinanceSmartChainTestnet),
_ => Err(GelatoError::UnknownChainNameError(String::from(s))),
}
}
}
impl From<Chain> for u32 {
fn from(chain: Chain) -> Self {
chain as u32
}
}
impl From<Chain> for U256 {
fn from(chain: Chain) -> Self {
u32::from(chain).into()
}
}
impl From<Chain> for u64 {
fn from(chain: Chain) -> Self {
u32::from(chain).into()
}
}
impl Chain {
// We also have to provide hardcoded verification contract addresses
// for Gelato-suppored chains, until a better / dynamic approach
// becomes available.
//
// See `getRelayForwarderAddress()` in the SDK file
// https://github.com/gelatodigital/relay-sdk/blob/8a9b9b2d0ef92ea9a3d6d64a230d9467a4b4da6d/src/constants/index.ts#L87.
pub fn relay_fwd_addr(&self) -> Result<Address, GelatoError> {
match self {
Chain::Ethereum => Ok(Address::from_str(
"5ca448e53e77499222741DcB6B3c959Fa829dAf2",
)?),
Chain::Rinkeby => Ok(Address::from_str(
"9B79b798563e538cc326D03696B3Be38b971D282",
)?),
Chain::Goerli => Ok(Address::from_str(
"61BF11e6641C289d4DA1D59dC3E03E15D2BA971c",
)?),
Chain::Kovan => Ok(Address::from_str(
"4F36f93F58d36DcbC1E60b9bdBE213482285C482",
)?),
Chain::Polygon => Ok(Address::from_str(
"c2336e796F77E4E57b6630b6dEdb01f5EE82383e",
)?),
Chain::PolygonMumbai => Ok(Address::from_str(
"3428E19A01E40333D5D51465A08476b8F61B86f3",
)?),
Chain::BinanceSmartChain => Ok(Address::from_str(
"eeea839E2435873adA11d5dD4CAE6032742C0445",
)?),
Chain::Alfajores => Ok(Address::from_str(
"c2336e796F77E4E57b6630b6dEdb01f5EE82383e",
)?),
Chain::Avalanche => Ok(Address::from_str(
"3456E168d2D7271847808463D6D383D079Bd5Eaa",
)?),
_ => Err(GelatoError::UnknownRelayForwardAddress(*self)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names() {
// FromStr provides both 'from_str' and a str.parse() implementation.
assert_eq!(Chain::from_str("MAINNET").unwrap(), Chain::Ethereum);
assert_eq!("MAINNET".parse::<Chain>().unwrap(), Chain::Ethereum);
// Conversions are case insensitive.
assert_eq!(
"polyGoNMuMBai".parse::<Chain>().unwrap(),
Chain::PolygonMumbai
);
// Error for unknown names.
assert!("notChain".parse::<Chain>().is_err());
}
#[test]
fn u32_from_chain() {
assert_eq!(u32::from(Chain::Ethereum), 1);
assert_eq!(u32::from(Chain::Celo), 42220);
}
#[test]
fn contracts() {
assert!(Chain::Ethereum.relay_fwd_addr().is_ok());
assert!(Chain::Rinkeby.relay_fwd_addr().is_ok());
assert!(Chain::Goerli.relay_fwd_addr().is_ok());
assert!(Chain::Kovan.relay_fwd_addr().is_ok());
assert!(Chain::Polygon.relay_fwd_addr().is_ok());
assert!(Chain::PolygonMumbai.relay_fwd_addr().is_ok());
assert!(Chain::BinanceSmartChain.relay_fwd_addr().is_ok());
assert!(!Chain::BinanceSmartChainTestnet.relay_fwd_addr().is_ok());
assert!(!Chain::Celo.relay_fwd_addr().is_ok());
assert!(Chain::Alfajores.relay_fwd_addr().is_ok());
assert!(Chain::Avalanche.relay_fwd_addr().is_ok());
assert!(!Chain::AvalancheFuji.relay_fwd_addr().is_ok());
assert!(!Chain::Optimism.relay_fwd_addr().is_ok());
assert!(!Chain::OptimismKovan.relay_fwd_addr().is_ok());
assert!(!Chain::Arbitrum.relay_fwd_addr().is_ok());
assert!(!Chain::ArbitrumRinkeby.relay_fwd_addr().is_ok());
}
}

@ -1,27 +0,0 @@
use crate::chains::Chain;
use rustc_hex::FromHexError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GelatoError {
#[error("Invalid hex address, couldn't parse '0x{0}'")]
RelayForwardAddressParseError(FromHexError),
#[error("No valid relay forward address for target chain '{0}'")]
UnknownRelayForwardAddress(Chain),
#[error("HTTP error: {0:#?}")]
RelayForwardHTTPError(reqwest::Error),
#[error("Unknown or unmapped chain with name '{0}'")]
UnknownChainNameError(String),
}
impl From<FromHexError> for GelatoError {
fn from(err: FromHexError) -> Self {
GelatoError::RelayForwardAddressParseError(err)
}
}
impl From<reqwest::Error> for GelatoError {
fn from(err: reqwest::Error) -> Self {
GelatoError::RelayForwardHTTPError(err)
}
}

@ -1,165 +0,0 @@
use crate::chains::Chain;
use crate::err::GelatoError;
use ethers::types::{Address, Bytes, Signature, U256};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use tracing::instrument;
const GATEWAY_URL: &str = "https://relay.gelato.digital";
pub const NATIVE_FEE_TOKEN_ADDRESS: ethers::types::Address = Address::repeat_byte(0xEE);
#[derive(Debug, Clone)]
pub struct ForwardRequestArgs {
pub chain_id: Chain,
pub target: Address,
pub data: Bytes,
pub fee_token: Address,
pub payment_type: PaymentType,
pub max_fee: U256,
pub gas: U256,
pub sponsor: Address,
pub sponsor_chain_id: Chain,
pub nonce: U256,
pub enforce_sponsor_nonce: bool,
pub enforce_sponsor_nonce_ordering: bool,
}
#[derive(Debug, Clone)]
pub struct ForwardRequestCall {
pub http: reqwest::Client,
pub args: ForwardRequestArgs,
pub signature: Signature,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ForwardRequestCallResult {
pub task_id: String,
}
impl ForwardRequestCall {
#[instrument]
pub async fn run(self) -> Result<ForwardRequestCallResult, GelatoError> {
let url = format!(
"{}/metabox-relays/{}",
GATEWAY_URL,
u32::from(self.args.chain_id)
);
let http_args = HTTPArgs {
args: self.args.clone(),
signature: self.signature,
};
let res = self.http.post(url).json(&http_args).send().await?;
let result: HTTPResult = res.json().await?;
Ok(ForwardRequestCallResult::from(result))
}
}
#[derive(Debug)]
struct HTTPArgs {
args: ForwardRequestArgs,
signature: Signature,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct HTTPResult {
pub task_id: String,
}
// We could try to get equivalent serde serialization for this type via the typical attributes,
// like #[serde(rename_all...)], #[serde(flatten)], etc, but altogether there are enough changes
// piled on top of one another that it seems more readable to just explicitly rewrite the relevant
// fields with inline modifications below.
//
// In total, we have to make the following logical changes from the default serde serialization:
// * add a new top-level dict field 'typeId', with const literal value 'ForwardRequest'.
// * hoist the two struct members (`args` and `signature`) up to the top-level dict (equiv. to
// `#[serde(flatten)]`).
// * make sure the integers for the fields `gas` and `maxfee` are enclosed within quotes,
// since Gelato-server-side, they will be interpreted as ~bignums.
// * ensure all hex-string-type fields are prefixed with '0x', rather than a string of
// ([0-9][a-f])+, which is expected server-side.
// * rewrite all field names to camelCase (equiv. to `#[serde(rename_all = "camelCase")]`).
impl Serialize for HTTPArgs {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("ForwardRequestHTTPArgs", 14)?;
state.serialize_field("typeId", "ForwardRequest")?;
state.serialize_field("chainId", &(u32::from(self.args.chain_id)))?;
state.serialize_field("target", &self.args.target)?;
state.serialize_field("data", &self.args.data)?;
state.serialize_field("feeToken", &self.args.fee_token)?;
// TODO(webbhorn): Get rid of the clone and cast.
state.serialize_field("paymentType", &(self.args.payment_type.clone() as u64))?;
state.serialize_field("maxFee", &self.args.max_fee.to_string())?;
state.serialize_field("gas", &self.args.gas.to_string())?;
state.serialize_field("sponsor", &self.args.sponsor)?;
state.serialize_field("sponsorChainId", &(u32::from(self.args.sponsor_chain_id)))?;
// TODO(webbhorn): Avoid narrowing conversion for serialization.
state.serialize_field("nonce", &self.args.nonce.as_u128())?;
state.serialize_field("enforceSponsorNonce", &self.args.enforce_sponsor_nonce)?;
state.serialize_field(
"enforceSponsorNonceOrdering",
&self.args.enforce_sponsor_nonce_ordering,
)?;
state.serialize_field("sponsorSignature", &format!("0x{}", self.signature))?;
state.end()
}
}
impl From<HTTPResult> for ForwardRequestCallResult {
fn from(http: HTTPResult) -> ForwardRequestCallResult {
ForwardRequestCallResult {
task_id: http.task_id,
}
}
}
#[derive(Debug, Clone)]
pub enum PaymentType {
Sync = 0,
AsyncGasTank = 1,
SyncGasTank = 2,
SyncPullFee = 3,
}
// TODO(webbhorn): Include tests near boundary of large int overflows, e.g. is nonce representation
// as u128 for serialization purposes correct given ethers::types::U256 representation in OpArgs?
#[cfg(test)]
mod tests {
use super::*;
use crate::test_data;
#[tokio::test]
async fn sdk_demo_data_request() {
use ethers::signers::{LocalWallet, Signer};
let args = test_data::sdk_demo_data::new_fwd_req_args();
let wallet = test_data::sdk_demo_data::WALLET_KEY
.parse::<LocalWallet>()
.unwrap();
let signature = wallet.sign_typed_data(&args).await.unwrap();
let http_args = HTTPArgs { args, signature };
assert_eq!(
serde_json::to_string(&http_args).unwrap(),
test_data::sdk_demo_data::EXPECTED_JSON_REQUEST_CONTENT
);
}
#[test]
fn sdk_demo_data_json_reply_parses() {
let reply_json =
r#"{"taskId": "0x053d975549b9298bb7672b20d3f7c0960df00d065e6f68c29abd8550b31cdbc2"}"#;
let parsed: HTTPResult = serde_json::from_str(&reply_json).unwrap();
assert_eq!(
parsed,
HTTPResult {
task_id: String::from(
"0x053d975549b9298bb7672b20d3f7c0960df00d065e6f68c29abd8550b31cdbc2"
),
}
);
}
}

@ -1,115 +0,0 @@
use ethers::abi::token::Token;
use ethers::types::transaction::eip712::{EIP712Domain, Eip712};
use ethers::types::U256;
use ethers::utils::keccak256;
use crate::err::GelatoError;
use crate::fwd_req_call::ForwardRequestArgs;
// See @gelatonetwork/gelato-relay-sdk/package/dist/lib/index.js.
const EIP_712_DOMAIN_NAME: &str = "GelatoRelayForwarder";
const EIP_712_VERSION: &str = "V1";
const EIP_712_TYPE_HASH_STR: &str = concat!(
"ForwardRequest(uint256 chainId,address target,bytes data,",
"address feeToken,uint256 paymentType,uint256 maxFee,",
"uint256 gas,address sponsor,uint256 sponsorChainId,",
"uint256 nonce,bool enforceSponsorNonce,",
"bool enforceSponsorNonceOrdering)"
);
impl Eip712 for ForwardRequestArgs {
type Error = GelatoError;
fn domain(&self) -> Result<EIP712Domain, Self::Error> {
Ok(EIP712Domain {
name: Some(String::from(EIP_712_DOMAIN_NAME)),
version: Some(String::from(EIP_712_VERSION)),
chain_id: Some(self.chain_id.into()),
verifying_contract: Some(self.chain_id.relay_fwd_addr()?),
salt: None,
})
}
fn type_hash() -> Result<[u8; 32], Self::Error> {
Ok(keccak256(EIP_712_TYPE_HASH_STR))
}
fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
Ok(keccak256(ethers::abi::encode(&[
Token::FixedBytes(ForwardRequestArgs::type_hash().unwrap().to_vec()),
Token::Int(U256::from(u32::from(self.chain_id))),
Token::Address(self.target),
Token::FixedBytes(keccak256(&self.data).to_vec()),
Token::Address(self.fee_token),
Token::Int(U256::from(self.payment_type.clone() as u64)),
Token::Int(self.max_fee),
Token::Int(self.gas),
Token::Address(self.sponsor),
Token::Int(U256::from(u32::from(self.sponsor_chain_id))),
Token::Int(self.nonce),
Token::Bool(self.enforce_sponsor_nonce),
Token::Bool(self.enforce_sponsor_nonce_ordering),
])))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_data;
use ethers::signers::{LocalWallet, Signer};
use ethers::types::transaction::eip712::Eip712;
use ethers::utils::hex;
// The EIP712 typehash for a ForwardRequest is invariant to the actual contents of the
// ForwardRequest message, and is instead deterministic of the ABI signature. So we
// can test it without constructing any interesting-looking message.
#[test]
fn eip712_type_hash_gelato_forward_request() {
assert_eq!(
hex::encode(&ForwardRequestArgs::type_hash().unwrap()),
"4aa193de33aca882aa52ebc7dcbdbd732ad1356422dea011f3a1fa08db2fac37"
);
}
#[tokio::test]
async fn sdk_demo_data_eip_712() {
use ethers::signers::{LocalWallet, Signer};
let args = test_data::sdk_demo_data::new_fwd_req_args();
assert_eq!(
hex::encode(&args.domain_separator().unwrap()),
test_data::sdk_demo_data::EXPECTED_DOMAIN_SEPARATOR
);
assert_eq!(
hex::encode(&args.struct_hash().unwrap()),
test_data::sdk_demo_data::EXPECTED_STRUCT_HASH
);
assert_eq!(
hex::encode(&args.encode_eip712().unwrap()),
test_data::sdk_demo_data::EXPECTED_EIP712_ENCODED_PAYLOAD
);
let wallet = test_data::sdk_demo_data::WALLET_KEY
.parse::<LocalWallet>()
.unwrap();
let sig = wallet.sign_typed_data(&args).await.unwrap();
assert_eq!(
sig.to_string(),
test_data::sdk_demo_data::EXPECTED_EIP712_SIGNATURE
);
}
// A test case provided to us from the Gelato team. The actual `ForwardRequest` message
// contents is *almost* the same as the `sdk_demo_data` test message. (The sponsor address
// differs, so we override in this test case.) OUtside of the message contents, the
// Gelato-provided LocalWallet private key differs as well.
#[tokio::test]
async fn gelato_provided_signature_matches() {
let mut args = test_data::sdk_demo_data::new_fwd_req_args();
args.sponsor = "97B503cb009670982ef9Ca472d66b3aB92fD6A9B".parse().unwrap();
let wallet = "c2fc8dc5512c1fb5df710c3320daa1e1ebc41701a9d5b489692e888228aaf813"
.parse::<LocalWallet>()
.unwrap();
let sig = wallet.sign_typed_data(&args).await.unwrap();
assert_eq!(
sig.to_string(),
"18bf6c6bb1a3410308cd5b395f5a3fac067835233f28f1b08d52b447179b72f40a50dc37ef7a785b0d5ed741e84a4375b3833cf43b4dba46686f15185f20f2541c"
);
}
}

@ -1,8 +1,5 @@
pub mod chains;
pub mod err;
pub mod fwd_req_call;
pub mod fwd_req_sig;
pub mod task_status_call;
const RELAY_URL: &str = "https://relay.gelato.digital";
#[cfg(test)]
pub mod test_data;
pub mod sponsored_call;
pub mod task_status;
pub mod types;

@ -0,0 +1,150 @@
use crate::types::Chain;
use crate::{types::serialize_as_decimal_str, RELAY_URL};
use ethers::types::{Address, Bytes, U256};
use serde::{Deserialize, Serialize};
use tracing::instrument;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SponsoredCallArgs {
pub chain_id: Chain,
pub target: Address,
pub data: Bytes,
// U256 by default serializes as a 0x-prefixed hexadecimal string.
// Gelato's API expects the gasLimit to be a decimal string.
/// Skip serializing if None - the Gelato API expects the parameter to either be
/// present as a string, or not at all.
#[serde(
serialize_with = "serialize_as_decimal_str",
skip_serializing_if = "Option::is_none"
)]
pub gas_limit: Option<U256>,
/// If None is provided, the Gelato API will use a default of 5.
/// Skip serializing if None - the Gelato API expects the parameter to either be
/// present as a number, or not at all.
#[serde(skip_serializing_if = "Option::is_none")]
pub retries: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct SponsoredCallApiCall<'a> {
pub http: reqwest::Client,
pub args: &'a SponsoredCallArgs,
pub sponsor_api_key: &'a str,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SponsoredCallApiCallResult {
pub task_id: String,
}
impl<'a> SponsoredCallApiCall<'a> {
#[instrument]
pub async fn run(self) -> Result<SponsoredCallApiCallResult, reqwest::Error> {
let url = format!("{}/relays/v2/sponsored-call", RELAY_URL);
let http_args = HTTPArgs {
args: self.args,
sponsor_api_key: self.sponsor_api_key,
};
let res = self.http.post(url).json(&http_args).send().await?;
let result: HTTPResult = res.json().await?;
Ok(SponsoredCallApiCallResult::from(result))
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HTTPArgs<'a> {
#[serde(flatten)]
args: &'a SponsoredCallArgs,
sponsor_api_key: &'a str,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct HTTPResult {
pub task_id: String,
}
impl From<HTTPResult> for SponsoredCallApiCallResult {
fn from(http: HTTPResult) -> SponsoredCallApiCallResult {
SponsoredCallApiCallResult {
task_id: http.task_id,
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn test_http_args_serialization() {
let sponsor_api_key = "foobar";
let mut args = SponsoredCallArgs {
chain_id: Chain::Alfajores,
target: Address::from_str("dead00000000000000000000000000000000beef").unwrap(),
data: Bytes::from_str("aabbccdd").unwrap(),
gas_limit: None,
retries: None,
};
// When gas_limit and retries are None, ensure they aren't serialized
assert_eq!(
serde_json::to_string(&HTTPArgs {
args: &args,
sponsor_api_key,
})
.unwrap(),
concat!(
"{",
r#""chainId":44787,"#,
r#""target":"0xdead00000000000000000000000000000000beef","#,
r#""data":"0xaabbccdd","#,
r#""sponsorApiKey":"foobar""#,
r#"}"#
),
);
// When the gas limit is specified, ensure it's serialized as a decimal *string*,
// and the retries are a number
args.gas_limit = Some(U256::from_dec_str("420000").unwrap());
args.retries = Some(5);
assert_eq!(
serde_json::to_string(&HTTPArgs {
args: &args,
sponsor_api_key,
})
.unwrap(),
concat!(
"{",
r#""chainId":44787,"#,
r#""target":"0xdead00000000000000000000000000000000beef","#,
r#""data":"0xaabbccdd","#,
r#""gasLimit":"420000","#,
r#""retries":5,"#,
r#""sponsorApiKey":"foobar""#,
r#"}"#
),
);
}
#[test]
fn test_http_result_deserialization() {
let reply_json =
r#"{"taskId": "0x053d975549b9298bb7672b20d3f7c0960df00d065e6f68c29abd8550b31cdbc2"}"#;
let parsed: HTTPResult = serde_json::from_str(&reply_json).unwrap();
assert_eq!(
parsed,
HTTPResult {
task_id: String::from(
"0x053d975549b9298bb7672b20d3f7c0960df00d065e6f68c29abd8550b31cdbc2"
),
}
);
}
}

@ -0,0 +1,75 @@
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::RELAY_URL;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum TaskState {
CheckPending,
ExecPending,
ExecSuccess,
ExecReverted,
WaitingForConfirmation,
Blacklisted,
Cancelled,
NotFound,
}
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatus {
pub chain_id: u64,
pub task_id: String,
pub task_state: TaskState,
pub creation_date: String,
/// Populated after the relay first simulates the task (taskState= CheckPending)
pub last_check_date: Option<String>,
/// Populated in case of simulation error or task cancellation (taskState= CheckPending | Cancelled)
pub last_check_message: Option<String>,
/// Populated as soon as the task is published to the mempool (taskState = WaitingForConfirmation)
pub transaction_hash: Option<String>,
/// Populated when the transaction is mined
pub execution_date: Option<String>,
/// Populated when the transaction is mined
pub block_number: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusApiCallArgs {
pub task_id: String,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusApiCallResult {
/// Typically present when a task cannot be found (also gives 404 HTTP status)
pub message: Option<String>,
/// Present when a task is found
pub task: Option<TaskStatus>,
}
impl TaskStatusApiCallResult {
pub fn task_state(&self) -> TaskState {
if let Some(task) = &self.task {
return task.task_state;
}
TaskState::NotFound
}
}
#[derive(Debug)]
pub struct TaskStatusApiCall {
pub http: reqwest::Client,
pub args: TaskStatusApiCallArgs,
}
impl TaskStatusApiCall {
#[instrument]
pub async fn run(&self) -> Result<TaskStatusApiCallResult, reqwest::Error> {
let url = format!("{}/tasks/status/{}", RELAY_URL, self.args.task_id);
let res = self.http.get(url).send().await?;
let result: TaskStatusApiCallResult = res.json().await?;
Ok(result)
}
}

@ -1,122 +0,0 @@
use crate::err::GelatoError;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::instrument;
const RELAY_URL: &str = "https://relay.gelato.digital";
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusCallArgs {
pub task_id: String,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusCallResult {
pub data: Vec<TransactionStatus>,
}
#[derive(Debug)]
pub struct TaskStatusCall {
pub http: Arc<reqwest::Client>,
pub args: TaskStatusCallArgs,
}
impl TaskStatusCall {
#[instrument]
pub async fn run(&self) -> Result<TaskStatusCallResult, GelatoError> {
let url = format!("{}/tasks/GelatoMetaBox/{}", RELAY_URL, self.args.task_id);
let res = self.http.get(url).send().await?;
let result: TaskStatusCallResult = res.json().await?;
Ok(result)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum TaskStatus {
CheckPending,
ExecPending,
ExecSuccess,
ExecReverted,
WaitingForConfirmation,
Blacklisted,
Cancelled,
NotFound,
}
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionStatus {
pub service: String,
pub chain: String,
pub task_id: String,
pub task_state: TaskStatus,
// TODO(webbhorn): Consider not even trying to parse as many of these optionals as we can
// get away with. It is kind of fragile and awkward since Gelato does not make any
// guarantees about which fields will be present in different scenarios.
pub created_at: Option<String>,
pub last_check: Option<Check>,
pub execution: Option<Execution>,
pub last_execution: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Execution {
pub status: String,
pub transaction_hash: String,
pub block_number: u64,
pub created_at: Option<String>,
}
// Sometimes the value corresponding to the 'last_check' key is a string timestamp, other times it
// is filled in with lots of detailed fields. Represent that with an enum and let serde figure it
// out.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum Check {
Timestamp(String),
CheckWithMetadata(Box<CheckInfo>),
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct CheckInfo {
// See `created_at` to understand why we rename this field
// rather than using `#[serde(rename_all = "camelCase")].
#[serde(rename = "taskState")]
pub task_state: TaskStatus,
pub message: String,
pub payload: Option<Payload>,
pub chain: Option<String>,
// Sadly, this is not serialized in camelCase by Gelato's API, and is
// named `created_at`.
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Payload {
pub to: String,
pub data: String,
pub type_: String,
pub fee_data: FeeData,
pub fee_token: String,
pub gas_limit: BigNumType,
pub is_flashbots: Option<bool>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FeeData {
pub gas_price: BigNumType,
pub max_fee_per_gas: BigNumType,
pub max_priority_fee_per_gas: BigNumType,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BigNumType {
pub hex: String,
pub type_: String,
}

@ -1,94 +0,0 @@
// The sample data / parameters below, along with corresponding expected digests and signatures,
// were validated by running the Gelato Relay SDK demo "hello world" app with instrumented
// logging, and recording the generated signatures and digests. A LocalWallet with a
// randomly-generated private key was also recorded.
//
// See https://docs.gelato.network/developer-products/gelato-relay-sdk/quick-start for more
// details.
//
// Since it is useful to refer to these parameters from a handful of places for testing any
// canonical request, it is shared with the whole crate from `test_data.rs`.
#[cfg(test)]
pub(crate) mod sdk_demo_data {
use ethers::types::U256;
use crate::{
chains::Chain,
fwd_req_call::{ForwardRequestArgs, PaymentType},
};
pub const CHAIN_ID: Chain = Chain::Goerli;
pub const TARGET_CONTRACT: &str = "0x8580995eb790a3002a55d249e92a8b6e5d0b384a";
pub const DATA: &str =
"0x4b327067000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeaeeeeeeeeeeeeeeeee";
pub const TOKEN: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
pub const PAYMENT_TYPE: PaymentType = PaymentType::AsyncGasTank;
pub const MAX_FEE: i64 = 1_000_000_000_000_000_000;
pub const GAS: i64 = 200_000;
pub const SPONSOR_CONTRACT: &str = "0xeed5ea7e25257a272cb3bf37b6169156d37fb908";
pub const SPONSOR_CHAIN_ID: Chain = Chain::Goerli;
pub const NONCE: U256 = U256::zero();
pub const ENFORCE_SPONSOR_NONCE: bool = false;
pub const ENFORCE_SPONSOR_NONCE_ORDERING: bool = true;
// An actual ForwardRequestArgs struct built from the above data.
pub fn new_fwd_req_args() -> ForwardRequestArgs {
ForwardRequestArgs {
chain_id: CHAIN_ID,
target: TARGET_CONTRACT.parse().unwrap(),
data: DATA.parse().unwrap(),
fee_token: TOKEN.parse().unwrap(),
payment_type: PAYMENT_TYPE,
max_fee: U256::from(MAX_FEE),
gas: U256::from(GAS),
sponsor: SPONSOR_CONTRACT.parse().unwrap(),
sponsor_chain_id: SPONSOR_CHAIN_ID,
nonce: NONCE,
enforce_sponsor_nonce: ENFORCE_SPONSOR_NONCE,
enforce_sponsor_nonce_ordering: ENFORCE_SPONSOR_NONCE_ORDERING,
}
}
// Expected EIP-712 data for ForwardRequest messages built with the above data, i.e. those
// returned by `new_fwd_req_args()`. Signing implementation tested in the `fwd_req_sig`
// module.
pub const EXPECTED_DOMAIN_SEPARATOR: &str =
"5b86c8e692a12ffedb26520fb1cc801f537517ee74d7730a1d806daf2b0c2688";
pub const EXPECTED_STRUCT_HASH: &str =
"6a2d78b78f47d56209a1b28617f9aee0ead447384cbc6b55f66247991d4462b6";
pub const EXPECTED_EIP712_ENCODED_PAYLOAD: &str =
"e9841a12928faf38821e924705b2fae99936a23086a0555d57fac07880bebc74";
// An EIP-712 signature over `EXPECTED_EIP712_ENCODED_PAYLOAD` from a LocalWallet
// whose private key is `WALLET_KEY` should result in the EIP-712 signature
// `EXPECTED_EIP712_SIGNATURE`. Implementation is tested in `fwd_req_sig` module.
pub const WALLET_KEY: &str = "969e81320ae43e23660804b78647bd4de6a12b82e3b06873f11ddbe164ebf58b";
pub const EXPECTED_EIP712_SIGNATURE: &str =
"a0e6d94b1608d4d8888f72c9e1335def0d187e41dca0ffe9fcd9b4bf96c1c59a27447248fef6a70e53646c0a156656f642ff361f3ab14b9db5f446f3681538b91c";
// When sending a Gelato ForwardRequest built from the above
// contents with the above signature to the Gelato Gateway server, the HTTP request is expected
// to contain the following JSON contents in its body.
// Implementation of the special serialization rules is tested in `fwd_req_call` module.
pub const EXPECTED_JSON_REQUEST_CONTENT: &str = concat!(
"{",
r#""typeId":"ForwardRequest","#,
r#""chainId":5,"#,
r#""target":"0x8580995eb790a3002a55d249e92a8b6e5d0b384a","#,
r#""data":"0x4b327067000000000000000000000000eeeeeeeeeeeeeee"#,
r#"eeeeeeeeeaeeeeeeeeeeeeeeeee","#,
r#""feeToken":"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","#,
r#""paymentType":1,"#,
r#""maxFee":"1000000000000000000","#,
r#""gas":"200000","#,
r#""sponsor":"0xeed5ea7e25257a272cb3bf37b6169156d37fb908","#,
r#""sponsorChainId":5,"#,
r#""nonce":0,"#,
r#""enforceSponsorNonce":false,"#,
r#""enforceSponsorNonceOrdering":true,"#,
r#""sponsorSignature":"#,
r#""0xa0e6d94b1608d4d8888f72c9e1335def0d187e41dca0ffe"#,
r#"9fcd9b4bf96c1c59a27447248fef6a70e53646c0a156656f642"#,
r#"ff361f3ab14b9db5f446f3681538b91c"}"#
);
}

@ -0,0 +1,81 @@
// Ideally we would avoid duplicating ethers::types::Chain, but ethers::types::Chain doesn't
// include all chains we support.
use ethers::types::U256;
use serde::{Serialize, Serializer};
use serde_repr::Serialize_repr;
use std::fmt;
// Each chain and chain ID supported by Abacus
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize_repr)]
#[repr(u64)]
pub enum Chain {
Ethereum = 1,
Rinkeby = 4,
Goerli = 5,
Kovan = 42,
Polygon = 137,
Mumbai = 80001,
Avalanche = 43114,
Fuji = 43113,
Arbitrum = 42161,
ArbitrumRinkeby = 421611,
Optimism = 10,
OptimismKovan = 69,
BinanceSmartChain = 56,
BinanceSmartChainTestnet = 97,
Celo = 42220,
Alfajores = 44787,
MoonbaseAlpha = 1287,
}
impl fmt::Display for Chain {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "{:?}", self)
}
}
impl From<Chain> for u32 {
fn from(chain: Chain) -> Self {
chain as u32
}
}
impl From<Chain> for U256 {
fn from(chain: Chain) -> Self {
u32::from(chain).into()
}
}
impl From<Chain> for u64 {
fn from(chain: Chain) -> Self {
u32::from(chain).into()
}
}
pub fn serialize_as_decimal_str<S>(maybe_n: &Option<U256>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(n) = maybe_n {
return s.serialize_str(&format!("{}", n));
}
maybe_n.serialize(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn u32_from_chain() {
assert_eq!(u32::from(Chain::Ethereum), 1);
assert_eq!(u32::from(Chain::Celo), 42220);
}
}

@ -26,7 +26,10 @@ spec:
{{- else }}
ABC_BASE_OUTBOX_CONNECTION_URL: {{ print "'{{ .outbox_rpc | toString }}'" }}
{{- end }}
{{/*
{{- if .Values.abacus.gelatoApiKeyRequired }}
ABC_BASE_GELATO_SPONSORAPIKEY: {{ print "'{{ .gelato_sponsor_api_key | toString }}'" }}
{{- end }}
{{- /*
* For each network, create an environment variable with the RPC endpoint.
* The templating of external-secrets will use the data section below to know how
* to replace the correct value in the created secret.
@ -50,7 +53,12 @@ spec:
remoteRef:
key: {{ printf "%s-rpc-endpoint-%s" .Values.abacus.runEnv .Values.abacus.outboxChain.name }}
{{- end }}
{{/*
{{- if .Values.abacus.gelatoApiKeyRequired }}
- secretKey: gelato_sponsor_api_key
remoteRef:
key: {{ printf "%s-gelato-api-key" .Values.abacus.runEnv }}
{{- end }}
{{- /*
* For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network,
* and associate it with the secret key networkname_rpc.
*/}}

@ -56,6 +56,7 @@ abacus:
type: # "http"
# -- Connection string pointing to an RPC endpoint for the home chain
aws: # true | false
gelatoApiKeyRequired: # true | false
# -- Replica chain overrides, a sequence
inboxChains:

@ -126,8 +126,7 @@ export const releaseCandidate: AgentConfig<TestnetChains> = {
environmentChainNames: chainNames,
contextChainNames: chainNames,
gelato: {
enabledChains: ['alfajores', 'mumbai', 'kovan'],
useForDisabledOriginChains: true,
enabledChains: ['alfajores', 'mumbai'],
},
validatorSets: validators,
connectionType: ConnectionType.HttpQuorum,

@ -24,11 +24,8 @@ async function helmValuesForChain<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
) {
const chainAgentConfig = new ChainAgentConfig(agentConfig, chainName);
const gelatoSupportedOnOutboxChain = agentConfig.gelato
?.useForDisabledOriginChains
? true
: agentConfig.gelato?.enabledChains.includes(chainName) ?? false;
const gelatoApiKeyRequired =
await chainAgentConfig.ensureGelatoApiKeySecretExistsIfRequired();
// By default, if a context only enables a subset of chains, the
// connection url (or urls, when HttpQuorum is used) are not fetched
@ -65,17 +62,15 @@ async function helmValuesForChain<Chain extends ChainName>(
connection: baseConnectionConfig,
},
aws: !!agentConfig.aws,
gelatoApiKeyRequired,
inboxChains: agentConfig.environmentChainNames
.filter((name) => name !== chainName)
.map((remoteChainName) => {
return {
name: remoteChainName,
disabled: !agentConfig.contextChainNames.includes(remoteChainName),
gelato: {
enabled:
gelatoSupportedOnOutboxChain &&
(agentConfig.gelato?.enabledChains?.includes(remoteChainName) ??
false),
txsubmission: {
type: chainAgentConfig.transactionSubmissionType(remoteChainName),
},
connection: baseConnectionConfig,
};

@ -8,6 +8,7 @@ import {
ValidatorAgentAwsUser,
} from '../agents/aws';
import { KEY_ROLE_ENUM } from '../agents/roles';
import { gcpSecretExists } from '../utils/gcloud';
import { DeployEnvironment } from './environment';
@ -195,14 +196,11 @@ export interface DockerConfig {
export interface GelatoConfig<Chain extends ChainName> {
// List of chains in which using Gelato is enabled for
enabledChains: Chain[];
// If true, Gelato will still be used for messages whose
// origin chain is *not* supported by Gelato. If false,
// Gelato will not be used for any messages from a disabled
// origin chain, even if the destination chain is enabled.
// Because Gelato doesn't charge on testnets, this is likely
// to be true for testnet environments where the chain in which gas
// is paid on (the origin) doesn't need to be supported by Gelato.
useForDisabledOriginChains: boolean;
}
export enum TransactionSubmissionType {
Signer = 'signer',
Gelato = 'gelato',
}
export interface AgentConfig<Chain extends ChainName> {
@ -458,6 +456,35 @@ export class ChainAgentConfig<Chain extends ChainName> {
return this.agentConfig.relayer !== undefined;
}
// Returns if it's required, throws if it's required and isn't present.
async ensureGelatoApiKeySecretExistsIfRequired(): Promise<boolean> {
// No need to check anything if no chains require Gelato
if (
!this.agentConfig.gelato ||
this.agentConfig.gelato.enabledChains.length == 0
) {
return false;
}
// Check to see if the Gelato API key exists in GCP secret manager - throw if it doesn't
const secretName = `${this.agentConfig.runEnv}-gelato-api-key`;
const secretExists = await gcpSecretExists(secretName);
if (!secretExists) {
throw Error(
`Expected Gelato API Key GCP Secret named ${secretName} to exist, have you created it?`,
);
}
return true;
}
transactionSubmissionType(chain: Chain): TransactionSubmissionType {
if (this.agentConfig.gelato?.enabledChains.includes(chain)) {
return TransactionSubmissionType.Gelato;
}
return TransactionSubmissionType.Signer;
}
get validatorSet(): ValidatorSet {
return this.agentConfig.validatorSets[this.chainName];
}

Loading…
Cancel
Save