use std::time::Duration;
use alloy_primitives::{keccak256, Address, B256, U256};
use base64::prelude::*;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Sha256;
use super::constants::{RELAYER_HOST, SAFE_MULTISEND, CHAIN_ID};
use super::onboarding::derive_safe_address;
use super::signer::TradingSigner;
use super::types::BuilderCredentials;
use crate::error::{Error, Result};
type HmacSha256 = Hmac<Sha256>;
const ZERO_ADDR: &str = "0x0000000000000000000000000000000000000000";
#[derive(Debug, Clone)]
pub struct SafeSubTx {
pub to: Address,
pub value: U256,
pub data: Vec<u8>,
pub operation: u8, }
fn safe_tx_type_hash() -> B256 {
keccak256(
b"SafeTx(address to,uint256 value,bytes data,uint8 operation,\
uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,\
address refundReceiver,uint256 nonce)",
)
}
fn domain_type_hash() -> B256 {
keccak256(b"EIP712Domain(uint256 chainId,address verifyingContract)")
}
fn encode_u256(n: U256) -> [u8; 32] {
n.to_be_bytes::<32>()
}
fn encode_address(a: Address) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[12..].copy_from_slice(a.as_slice());
buf
}
fn domain_separator(chain_id: u64, verifying: Address) -> B256 {
let mut enc = Vec::with_capacity(96);
enc.extend_from_slice(domain_type_hash().as_slice());
enc.extend_from_slice(&encode_u256(U256::from(chain_id)));
enc.extend_from_slice(&encode_address(verifying));
keccak256(&enc)
}
fn safe_tx_struct_hash(
to: Address, value: U256, data: &[u8], operation: u8,
safe_tx_gas: U256, base_gas: U256, gas_price: U256,
gas_token: Address, refund_receiver: Address, nonce: U256,
) -> B256 {
let data_hash = keccak256(data);
let mut enc = Vec::with_capacity(384);
enc.extend_from_slice(safe_tx_type_hash().as_slice());
enc.extend_from_slice(&encode_address(to));
enc.extend_from_slice(&encode_u256(value));
enc.extend_from_slice(data_hash.as_slice());
enc.extend_from_slice(&encode_u256(U256::from(operation)));
enc.extend_from_slice(&encode_u256(safe_tx_gas));
enc.extend_from_slice(&encode_u256(base_gas));
enc.extend_from_slice(&encode_u256(gas_price));
enc.extend_from_slice(&encode_address(gas_token));
enc.extend_from_slice(&encode_address(refund_receiver));
enc.extend_from_slice(&encode_u256(nonce));
keccak256(&enc)
}
fn eip712_digest(chain_id: u64, safe: Address, struct_hash: B256) -> B256 {
let mut enc = Vec::with_capacity(66);
enc.extend_from_slice(&[0x19, 0x01]);
enc.extend_from_slice(domain_separator(chain_id, safe).as_slice());
enc.extend_from_slice(struct_hash.as_slice());
keccak256(&enc)
}
fn encode_multisend_calldata(txns: &[SafeSubTx]) -> Vec<u8> {
let selector = [0x8d, 0x80, 0xff, 0x0a];
let mut inner = Vec::new();
for tx in txns {
inner.push(tx.operation);
inner.extend_from_slice(tx.to.as_slice());
inner.extend_from_slice(&encode_u256(tx.value));
inner.extend_from_slice(&encode_u256(U256::from(tx.data.len() as u64)));
inner.extend_from_slice(&tx.data);
}
let pad = (32 - inner.len() % 32) % 32;
let mut out = Vec::with_capacity(4 + 64 + inner.len() + pad);
out.extend_from_slice(&selector);
out.extend_from_slice(&encode_u256(U256::from(32)));
out.extend_from_slice(&encode_u256(U256::from(inner.len() as u64)));
out.extend_from_slice(&inner);
out.extend_from_slice(&vec![0u8; pad]);
out
}
fn build_hmac_headers(
creds: &BuilderCredentials, method: &str, path: &str, body: &str,
) -> Result<Vec<(String, String)>> {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let msg = format!("{}{}{}{}", ts, method, path, body);
let secret_norm = creds.secret.replace('-', "+").replace('_', "/");
let key = BASE64_STANDARD.decode(&secret_norm)
.map_err(|e| Error::Trading(format!("bad builder secret: {}", e)))?;
let mut mac = HmacSha256::new_from_slice(&key)
.map_err(|e| Error::Trading(format!("hmac init: {}", e)))?;
mac.update(msg.as_bytes());
let sig_bytes = mac.finalize().into_bytes();
let sig = BASE64_STANDARD.encode(&sig_bytes).replace('+', "-").replace('/', "_");
Ok(vec![
("POLY_BUILDER_API_KEY".into(), creds.key.clone()),
("POLY_BUILDER_PASSPHRASE".into(), creds.passphrase.clone()),
("POLY_BUILDER_SIGNATURE".into(), sig),
("POLY_BUILDER_TIMESTAMP".into(), ts.to_string()),
])
}
fn pack_sig(sig_bytes: &[u8]) -> Result<String> {
if sig_bytes.len() != 65 {
return Err(Error::Trading(format!("expected 65-byte sig, got {}", sig_bytes.len())));
}
let mut v = sig_bytes[64];
v = match v {
0 | 1 => v + 31,
27 | 28 => v + 4,
_ => return Err(Error::Trading(format!("invalid sig v: {}", v))),
};
let mut out = Vec::with_capacity(65);
out.extend_from_slice(&sig_bytes[..64]);
out.push(v);
Ok(format!("0x{}", hex::encode(&out)))
}
#[derive(Deserialize)]
struct NonceResp {
nonce: String,
}
#[derive(Deserialize)]
struct SubmitResp {
#[serde(rename = "transactionID")]
transaction_id: String,
}
#[derive(Deserialize)]
struct TxStatus {
state: Option<String>,
#[serde(rename = "transactionHash")]
transaction_hash: Option<String>,
}
pub struct RelayClient {
relayer_url: String,
chain_id: u64,
creds: BuilderCredentials,
http: reqwest::Client,
}
impl RelayClient {
pub fn new(creds: BuilderCredentials) -> Self {
Self {
relayer_url: RELAYER_HOST.trim_end_matches('/').to_string(),
chain_id: CHAIN_ID as u64,
creds,
http: reqwest::Client::new(),
}
}
pub async fn execute_safe(
&self,
signer: &dyn TradingSigner,
txns: Vec<SafeSubTx>,
) -> Result<String> {
let eoa_address = signer.address();
if txns.is_empty() {
return Err(Error::Trading("no transactions to execute".into()));
}
let safe_addr = derive_safe_address(eoa_address);
let resp = self
.http
.get(format!("{}/nonce", self.relayer_url))
.query(&[("address", format!("{:?}", eoa_address)), ("type", "SAFE".into())])
.send()
.await
.map_err(|e| Error::Trading(format!("relayer /nonce: {}", e)))?;
if !resp.status().is_success() {
return Err(Error::Trading(format!("relayer /nonce status {}", resp.status())));
}
let nonce_resp: NonceResp = resp.json().await
.map_err(|e| Error::Trading(format!("relayer /nonce parse: {}", e)))?;
let nonce: u64 = nonce_resp.nonce.parse().unwrap_or(0);
let (to_addr, data_bytes, operation) = if txns.len() == 1 {
let t = &txns[0];
(t.to, t.data.clone(), t.operation)
} else {
let multisend_addr: Address = SAFE_MULTISEND.parse().unwrap();
(multisend_addr, encode_multisend_calldata(&txns), 1u8)
};
let zero: Address = ZERO_ADDR.parse().unwrap();
let struct_hash = safe_tx_struct_hash(
to_addr, U256::ZERO, &data_bytes, operation,
U256::ZERO, U256::ZERO, U256::ZERO,
zero, zero, U256::from(nonce),
);
let digest = eip712_digest(self.chain_id, safe_addr, struct_hash);
let signed = signer.sign_message(digest.as_slice()).await?;
let packed_sig = pack_sig(&signed)?;
let req = json!({
"from": format!("{:?}", eoa_address),
"to": format!("{:?}", to_addr),
"proxyWallet": format!("{:?}", safe_addr),
"data": format!("0x{}", hex::encode(&data_bytes)),
"nonce": nonce.to_string(),
"signature": packed_sig,
"signatureParams": {
"gasPrice": "0",
"operation": operation.to_string(),
"safeTxnGas": "0",
"baseGas": "0",
"gasToken": ZERO_ADDR,
"refundReceiver": ZERO_ADDR,
},
"type": "SAFE",
"metadata": "",
});
let body = serde_json::to_string(&req).unwrap();
let headers = build_hmac_headers(&self.creds, "POST", "/submit", &body)?;
let mut req_builder = self.http
.post(format!("{}/submit", self.relayer_url))
.header("Content-Type", "application/json");
for (k, v) in headers {
req_builder = req_builder.header(k, v);
}
let resp = req_builder.body(body).send().await
.map_err(|e| Error::Trading(format!("relayer /submit: {}", e)))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(Error::Trading(format!("relayer /submit status {}: {}", status, text)));
}
let submit: SubmitResp = resp.json().await
.map_err(|e| Error::Trading(format!("relayer /submit parse: {}", e)))?;
for _ in 0..30 {
tokio::time::sleep(Duration::from_secs(2)).await;
let resp = self
.http
.get(format!("{}/transaction", self.relayer_url))
.query(&[("id", &submit.transaction_id)])
.send()
.await;
let Ok(resp) = resp else { continue };
if !resp.status().is_success() { continue }
let rows: serde_json::Value = match resp.json().await {
Ok(v) => v,
Err(_) => continue,
};
let row = if rows.is_array() {
rows.get(0).cloned()
} else {
Some(rows)
};
let Some(row) = row else { continue };
let row: TxStatus = match serde_json::from_value(row) {
Ok(v) => v,
Err(_) => continue,
};
let state = row.state.as_deref().unwrap_or("");
if state == "STATE_MINED" || state == "STATE_CONFIRMED" {
return row.transaction_hash.ok_or_else(|| Error::Trading("no tx hash".into()));
}
if state == "STATE_FAILED" || state == "STATE_REVERTED" {
return Err(Error::Trading(format!(
"tx {} failed: state={} hash={:?}",
submit.transaction_id, state, row.transaction_hash
)));
}
}
Err(Error::Trading(format!("tx {} timed out", submit.transaction_id)))
}
}