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 testingpull/1081/head
parent
a1ce16b425
commit
ea5b584fe8
@ -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); |
||||
} |
||||
} |
Loading…
Reference in new issue