Skip to main content

polymarket_relayer/
direct.rs

1//! Direct on-chain execution — fallback when relayer quota is exhausted.
2//!
3//! Supports two wallet types:
4//! - **Safe** (signature_type=2): execTransaction via Gnosis Safe
5//! - **Proxy** (signature_type=1): direct call to proxy wallet (magic.link)
6//!
7//! Safe flow:
8//! 1. Read Safe nonce via `nonce()` view call
9//! 2. Get Safe transaction hash via `getTransactionHash()` on-chain call
10//! 3. ECDSA-sign the raw hash (no eth_sign prefix)
11//! 4. Pack signature r + s + v (v = 27 or 28)
12//! 5. Call `execTransaction` on the Safe
13//!
14//! Proxy flow:
15//! 1. Encode calls as proxy((uint8,address,uint256,bytes)[])
16//! 2. Send directly from EOA to proxy wallet address
17
18use ethers::abi::{encode, Token};
19use ethers::middleware::SignerMiddleware;
20use ethers::providers::{Http, Middleware, Provider};
21use ethers::signers::{LocalWallet, Signer};
22use ethers::types::{
23    Address, Bytes, Eip1559TransactionRequest, H256, TransactionReceipt, U256,
24};
25use ethers::utils::keccak256;
26
27use crate::builder::derive;
28use crate::error::{RelayerError, Result};
29use crate::types::{RelayerTxType, Transaction};
30
31const DEFAULT_GAS_LIMIT: u64 = 500_000;
32
33/// Result of a direct on-chain redemption.
34#[derive(Debug)]
35pub struct DirectTxResult {
36    pub tx_hash: String,
37    pub success: bool,
38    pub gas_used: u64,
39    pub gas_cost_matic: f64,
40    pub block_number: u64,
41}
42
43/// Executor for direct on-chain transactions (no relayer).
44///
45/// Supports both Safe and Proxy wallet types.
46pub struct DirectExecutor {
47    provider: SignerMiddleware<Provider<Http>, LocalWallet>,
48    signer_address: Address,
49    wallet_address: Address,
50    wallet_type: RelayerTxType,
51    #[allow(dead_code)]
52    chain_id: u64,
53}
54
55impl DirectExecutor {
56    /// Create a new DirectExecutor for a **Safe** wallet (backward compatible).
57    pub fn new(rpc_url: &str, signer: LocalWallet, chain_id: u64) -> Result<Self> {
58        Self::with_type(rpc_url, signer, chain_id, RelayerTxType::Safe)
59    }
60
61    /// Create a new DirectExecutor for a **Proxy** wallet.
62    pub fn new_proxy(rpc_url: &str, signer: LocalWallet, chain_id: u64) -> Result<Self> {
63        Self::with_type(rpc_url, signer, chain_id, RelayerTxType::Proxy)
64    }
65
66    /// Create a DirectExecutor with explicit wallet type.
67    pub fn with_type(
68        rpc_url: &str,
69        signer: LocalWallet,
70        chain_id: u64,
71        wallet_type: RelayerTxType,
72    ) -> Result<Self> {
73        let signer_address = signer.address();
74        let wallet_address = match wallet_type {
75            RelayerTxType::Eoa => signer_address,
76            RelayerTxType::Safe => derive::derive_safe_address(signer_address)?,
77            RelayerTxType::Proxy => derive::derive_proxy_address(signer_address)?,
78        };
79
80        let provider = Provider::<Http>::try_from(rpc_url)
81            .map_err(|e| RelayerError::Other(format!("Invalid RPC URL: {e}")))?;
82        let provider = SignerMiddleware::new(provider, signer.with_chain_id(chain_id));
83
84        Ok(Self {
85            provider,
86            signer_address,
87            wallet_address,
88            wallet_type,
89            chain_id,
90        })
91    }
92
93    /// Create a DirectExecutor for a Proxy wallet with an explicit proxy address
94    /// (instead of deriving it from the signer).
95    pub fn new_proxy_with_address(
96        rpc_url: &str,
97        signer: LocalWallet,
98        chain_id: u64,
99        proxy_address: Address,
100    ) -> Result<Self> {
101        let signer_address = signer.address();
102        let provider = Provider::<Http>::try_from(rpc_url)
103            .map_err(|e| RelayerError::Other(format!("Invalid RPC URL: {e}")))?;
104        let provider = SignerMiddleware::new(provider, signer.with_chain_id(chain_id));
105
106        Ok(Self {
107            provider,
108            signer_address,
109            wallet_address: proxy_address,
110            wallet_type: RelayerTxType::Proxy,
111            chain_id,
112        })
113    }
114
115    /// Get the wallet address (Safe or Proxy).
116    pub fn wallet_address(&self) -> Address {
117        self.wallet_address
118    }
119
120    /// Backward-compatible alias for `wallet_address()`.
121    pub fn safe_address(&self) -> Address {
122        self.wallet_address
123    }
124
125    pub fn signer_address(&self) -> Address {
126        self.signer_address
127    }
128
129    pub fn wallet_type(&self) -> RelayerTxType {
130        self.wallet_type
131    }
132
133    /// Get MATIC balance of the EOA (for gas).
134    pub async fn get_matic_balance(&self) -> Result<f64> {
135        let balance = self
136            .provider
137            .get_balance(self.signer_address, None)
138            .await
139            .map_err(|e| RelayerError::Other(format!("Failed to get balance: {e}")))?;
140        let matic = balance.as_u128() as f64 / 1e18;
141        Ok(matic)
142    }
143
144    /// Execute a transaction directly on-chain.
145    ///
146    /// Routes to Safe or Proxy execution based on `wallet_type`.
147    pub async fn execute(&self, tx: &Transaction) -> Result<DirectTxResult> {
148        match self.wallet_type {
149            RelayerTxType::Eoa => self.execute_eoa(tx).await,
150            RelayerTxType::Safe => self.execute_safe(tx).await,
151            RelayerTxType::Proxy => self.execute_proxy(tx).await,
152        }
153    }
154
155    // ── EOA execution ──────────────────────────────────────────────────
156
157    /// Execute a transaction directly from the EOA (signature_type=0).
158    ///
159    /// Simplest path: just send the calldata to the target address.
160    async fn execute_eoa(&self, tx: &Transaction) -> Result<DirectTxResult> {
161        let target: Address = tx
162            .to
163            .parse()
164            .map_err(|e: <Address as std::str::FromStr>::Err| {
165                RelayerError::InvalidAddress(e.to_string())
166            })?;
167        let calldata = hex::decode(tx.data.strip_prefix("0x").unwrap_or(&tx.data))
168            .map_err(|e| RelayerError::Abi(format!("Invalid calldata hex: {e}")))?;
169
170        tracing::debug!(target = ?target, "Executing direct EOA call");
171        self.send_raw_tx(target, calldata).await
172    }
173
174    // ── Safe execution ─────────────────────────────────────────────────
175
176    /// Execute a transaction directly through the Gnosis Safe.
177    async fn execute_safe(&self, tx: &Transaction) -> Result<DirectTxResult> {
178        let target: Address = tx
179            .to
180            .parse()
181            .map_err(|e: <Address as std::str::FromStr>::Err| {
182                RelayerError::InvalidAddress(e.to_string())
183            })?;
184        let inner_calldata = hex::decode(tx.data.strip_prefix("0x").unwrap_or(&tx.data))
185            .map_err(|e| RelayerError::Abi(format!("Invalid calldata hex: {e}")))?;
186
187        // 1. Read Safe nonce
188        let safe_nonce = self.read_safe_nonce().await?;
189        tracing::debug!(safe_nonce, "Safe nonce");
190
191        // 2. Get the Safe tx hash from the contract itself (authoritative)
192        let safe_tx_hash = self
193            .get_transaction_hash_onchain(target, &inner_calldata, safe_nonce)
194            .await?;
195        tracing::debug!(hash = ?safe_tx_hash, "Safe tx hash from contract");
196
197        // 3. ECDSA-sign the raw hash (NO eth_sign prefix — raw sign_hash)
198        let signature = self
199            .provider
200            .signer()
201            .sign_hash(safe_tx_hash)
202            .map_err(|e| RelayerError::Signing(e.to_string()))?;
203
204        // 4. Pack signature: r(32) + s(32) + v(1), v = 27 or 28
205        let mut packed_sig = Vec::with_capacity(65);
206        let mut r_bytes = [0u8; 32];
207        signature.r.to_big_endian(&mut r_bytes);
208        packed_sig.extend_from_slice(&r_bytes);
209        let mut s_bytes = [0u8; 32];
210        signature.s.to_big_endian(&mut s_bytes);
211        packed_sig.extend_from_slice(&s_bytes);
212        packed_sig.push(signature.v as u8);
213
214        // 5. Build execTransaction calldata
215        let exec_calldata =
216            self.encode_exec_transaction(target, &inner_calldata, &packed_sig);
217
218        // 6. Send transaction to Safe
219        self.send_raw_tx(self.wallet_address, exec_calldata).await
220    }
221
222    // ── Proxy execution ────────────────────────────────────────────────
223
224    /// Execute a transaction directly through a Proxy wallet.
225    ///
226    /// For proxy wallets (signature_type=1, e.g. magic.link), the EOA is the
227    /// owner of the proxy and can call the proxy's `proxy()` function directly.
228    async fn execute_proxy(&self, tx: &Transaction) -> Result<DirectTxResult> {
229        let target: Address = tx
230            .to
231            .parse()
232            .map_err(|e: <Address as std::str::FromStr>::Err| {
233                RelayerError::InvalidAddress(e.to_string())
234            })?;
235        let inner_calldata = hex::decode(tx.data.strip_prefix("0x").unwrap_or(&tx.data))
236            .map_err(|e| RelayerError::Abi(format!("Invalid calldata hex: {e}")))?;
237        let value = U256::from_dec_str(&tx.value)
238            .map_err(|e| RelayerError::Abi(format!("Invalid value: {e}")))?;
239
240        // Encode as proxy((uint8,address,uint256,bytes)[]) with a single call
241        let call_tuple = Token::Tuple(vec![
242            Token::Uint(U256::one()), // typeCode: 1 = Call (Polymarket proxy convention)
243            Token::Address(target),
244            Token::Uint(value),
245            Token::Bytes(inner_calldata),
246        ]);
247
248        let selector = &keccak256(b"proxy((uint8,address,uint256,bytes)[])")[..4];
249        let encoded = encode(&[Token::Array(vec![call_tuple])]);
250        let mut calldata = selector.to_vec();
251        calldata.extend_from_slice(&encoded);
252
253        tracing::debug!(
254            proxy_address = ?self.wallet_address,
255            target = ?target,
256            "Executing direct proxy call"
257        );
258
259        // Send directly to the proxy wallet
260        self.send_raw_tx(self.wallet_address, calldata).await
261    }
262
263    // ── Common send logic ──────────────────────────────────────────────
264
265    /// Send a raw transaction and wait for receipt.
266    async fn send_raw_tx(&self, to: Address, calldata: Vec<u8>) -> Result<DirectTxResult> {
267        let gas_price = self
268            .provider
269            .get_gas_price()
270            .await
271            .map_err(|e| RelayerError::Other(format!("Failed to get gas price: {e}")))?;
272
273        let tx_request = Eip1559TransactionRequest::new()
274            .to(to)
275            .data(calldata)
276            .gas(DEFAULT_GAS_LIMIT)
277            .max_fee_per_gas(gas_price * 3 / 2)
278            .max_priority_fee_per_gas(U256::from(30_000_000_000u64)); // 30 gwei
279
280        let pending = self
281            .provider
282            .send_transaction(tx_request, None)
283            .await
284            .map_err(|e| RelayerError::Other(format!("Failed to send tx: {e}")))?;
285
286        let tx_hash = format!("{:?}", pending.tx_hash());
287        tracing::info!(tx_hash = %tx_hash, "Direct tx sent");
288
289        let receipt: TransactionReceipt = pending
290            .await
291            .map_err(|e| RelayerError::Other(format!("Tx failed: {e}")))?
292            .ok_or_else(|| RelayerError::Other("No receipt".to_string()))?;
293
294        let gas_used = receipt.gas_used.map(|g| g.as_u64()).unwrap_or(0);
295        let effective_gas_price = receipt
296            .effective_gas_price
297            .map(|p| p.as_u128())
298            .unwrap_or(0);
299        let gas_cost_matic = gas_used as f64 * effective_gas_price as f64 / 1e18;
300        let block_number = receipt.block_number.map(|b| b.as_u64()).unwrap_or(0);
301        let success = receipt.status.map(|s| s.as_u64() == 1).unwrap_or(false);
302
303        if success {
304            tracing::info!(block = block_number, gas = gas_used, "Direct tx confirmed");
305        } else {
306            tracing::warn!(tx_hash = %tx_hash, "Direct tx reverted");
307        }
308
309        Ok(DirectTxResult {
310            tx_hash,
311            success,
312            gas_used,
313            gas_cost_matic,
314            block_number,
315        })
316    }
317
318    // ── Safe on-chain calls ────────────────────────────────────────────
319
320    /// Read the Safe nonce via eth_call to `nonce()`.
321    async fn read_safe_nonce(&self) -> Result<u64> {
322        let selector = &keccak256(b"nonce()")[..4];
323        let result = self.eth_call(self.wallet_address, selector).await.map_err(|e| {
324            RelayerError::Other(format!(
325                "Failed to read Safe nonce from {:?}: {}. \
326                 Check that the Safe is deployed and the RPC URL is reachable.",
327                self.wallet_address, e
328            ))
329        })?;
330        if result.is_empty() {
331            return Err(RelayerError::Other(format!(
332                "Empty nonce response from Safe {:?} — wallet may not be deployed",
333                self.wallet_address
334            )));
335        }
336        if result.len() < 32 {
337            return Err(RelayerError::Other(format!(
338                "Invalid nonce response ({} bytes, expected 32) from Safe {:?}",
339                result.len(),
340                self.wallet_address
341            )));
342        }
343        Ok(U256::from_big_endian(&result[..32]).as_u64())
344    }
345
346    /// Get the Safe tx hash via eth_call to `getTransactionHash(...)`.
347    async fn get_transaction_hash_onchain(
348        &self,
349        to: Address,
350        data: &[u8],
351        nonce: u64,
352    ) -> Result<H256> {
353        let selector = &keccak256(
354            b"getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)",
355        )[..4];
356
357        let encoded_args = encode(&[
358            Token::Address(to),
359            Token::Uint(U256::zero()),           // value
360            Token::Bytes(data.to_vec()),         // data
361            Token::Uint(U256::zero()),           // operation = Call
362            Token::Uint(U256::zero()),           // safeTxGas
363            Token::Uint(U256::zero()),           // baseGas
364            Token::Uint(U256::zero()),           // gasPrice
365            Token::Address(Address::zero()),     // gasToken
366            Token::Address(Address::zero()),     // refundReceiver
367            Token::Uint(U256::from(nonce)),      // _nonce
368        ]);
369
370        let mut calldata = selector.to_vec();
371        calldata.extend_from_slice(&encoded_args);
372
373        let result = self.eth_call(self.wallet_address, &calldata).await?;
374        if result.len() < 32 {
375            return Err(RelayerError::Other(
376                "Invalid getTransactionHash response".to_string(),
377            ));
378        }
379        Ok(H256::from_slice(&result[..32]))
380    }
381
382    /// Helper: eth_call to any contract address.
383    async fn eth_call(&self, to: Address, calldata: &[u8]) -> Result<Bytes> {
384        let selector_hex = if calldata.len() >= 4 {
385            format!("0x{}", hex::encode(&calldata[..4]))
386        } else {
387            "empty".to_string()
388        };
389        self.provider
390            .call(
391                &ethers::types::transaction::eip2718::TypedTransaction::Eip1559(
392                    Eip1559TransactionRequest::new()
393                        .to(to)
394                        .data(Bytes::from(calldata.to_vec())),
395                ),
396                None,
397            )
398            .await
399            .map_err(|e| RelayerError::Other(format!(
400                "eth_call to {:?} (selector {}) failed: {e}",
401                to, selector_hex
402            )))
403    }
404
405    /// Encode execTransaction calldata for Safe.
406    fn encode_exec_transaction(
407        &self,
408        to: Address,
409        inner_data: &[u8],
410        signature: &[u8],
411    ) -> Vec<u8> {
412        let selector = &keccak256(
413            b"execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
414        )[..4];
415
416        let encoded = encode(&[
417            Token::Address(to),
418            Token::Uint(U256::zero()),
419            Token::Bytes(inner_data.to_vec()),
420            Token::Uint(U256::zero()),           // operation
421            Token::Uint(U256::zero()),           // safeTxGas
422            Token::Uint(U256::zero()),           // baseGas
423            Token::Uint(U256::zero()),           // gasPrice
424            Token::Address(Address::zero()),     // gasToken
425            Token::Address(Address::zero()),     // refundReceiver
426            Token::Bytes(signature.to_vec()),
427        ]);
428
429        let mut calldata = selector.to_vec();
430        calldata.extend_from_slice(&encoded);
431        calldata
432    }
433}