use std::borrow::Cow;
use alloy_primitives::{Address, B256, Bytes, U256, keccak256};
use alloy_signer::Signer;
use alloy_sol_types::{SolCall, SolStruct, sol};
use crate::{
PolyrelError,
types::{Config, OperationType, SignatureParams, SubmitRequest, WalletType},
};
const FIELD_TX_FEE: &str = "tx_fee";
const FIELD_GAS_PRICE: &str = "gas_price";
const FIELD_GAS_LIMIT: &str = "gas_limit";
const FIELD_NONCE: &str = "nonce";
sol! {
#[derive(Debug)]
struct SafeTx {
address to;
uint256 value;
bytes data;
uint8 operation;
uint256 safeTxGas;
uint256 baseGas;
uint256 gasPrice;
address gasToken;
address refundReceiver;
uint256 nonce;
}
interface IERC20 {
function approve(address spender, uint256 value) external returns (bool);
function transfer(address to, uint256 value) external returns (bool);
}
interface IERC1155 {
function setApprovalForAll(address operator, bool approved) external;
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
) external;
}
interface IMultiSend {
function multiSend(bytes transactions) external;
}
interface IConditionalTokens {
function splitPosition(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata partition,
uint256 amount
) external;
function mergePositions(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata partition,
uint256 amount
) external;
function redeemPositions(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata indexSets
) external;
}
interface INegRiskAdapter {
function redeemPositions(
bytes32 conditionId,
uint256[] amounts
) external;
}
}
sol! {
struct CreateProxy {
address paymentToken;
uint256 payment;
address paymentReceiver;
}
}
sol! {
struct ProxyCall {
uint8 typeCode;
address to;
uint256 value;
bytes data;
}
interface IProxyWalletFactory {
function proxy(ProxyCall[] calls) external payable returns (bytes[]);
}
}
pub type Call = (Address, Bytes);
const PROXY_CALL_TYPE_CALL: u8 = 1;
pub struct NonEmptyProxyCalls(Vec<(Address, Bytes)>);
impl NonEmptyProxyCalls {
pub fn new(calls: Vec<(Address, Bytes)>) -> Option<Self> {
if calls.is_empty() {
return None;
}
Some(Self(calls))
}
}
pub fn encode_proxy_calls(transactions: NonEmptyProxyCalls) -> Bytes {
let calls: Vec<ProxyCall> = transactions
.0
.iter()
.map(|(to, data)| ProxyCall {
typeCode: PROXY_CALL_TYPE_CALL,
to: *to,
value: U256::ZERO,
data: data.to_vec().into(),
})
.collect();
Bytes::from(IProxyWalletFactory::proxyCall { calls }.abi_encode())
}
pub fn usdc_approve_exchange(config: &Config, amount: U256) -> Call {
let calldata = IERC20::approveCall { spender: config.ctf_exchange(), value: amount };
(config.usdc_e(), Bytes::from(calldata.abi_encode()))
}
pub fn usdc_approve_neg_risk_exchange(config: &Config, amount: U256) -> Call {
let calldata = IERC20::approveCall { spender: config.neg_risk_ctf_exchange(), value: amount };
(config.usdc_e(), Bytes::from(calldata.abi_encode()))
}
pub fn usdc_transfer(config: &Config, to: Address, amount: U256) -> Call {
let calldata = IERC20::transferCall { to, value: amount };
(config.usdc_e(), Bytes::from(calldata.abi_encode()))
}
pub fn ctf_approve_exchange(config: &Config) -> Call {
let calldata =
IERC1155::setApprovalForAllCall { operator: config.ctf_exchange(), approved: true };
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn ctf_approve_neg_risk_exchange(config: &Config) -> Call {
let calldata = IERC1155::setApprovalForAllCall {
operator: config.neg_risk_ctf_exchange(),
approved: true,
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn usdc_approve_conditional_tokens(config: &Config, amount: U256) -> Call {
let calldata =
IERC20::approveCall { spender: config.conditional_tokens(), value: amount };
(config.usdc_e(), Bytes::from(calldata.abi_encode()))
}
pub fn usdc_approve_neg_risk_adapter(config: &Config, amount: U256) -> Call {
let calldata =
IERC20::approveCall { spender: config.neg_risk_adapter(), value: amount };
(config.usdc_e(), Bytes::from(calldata.abi_encode()))
}
pub fn ctf_approve_neg_risk_adapter(config: &Config) -> Call {
let calldata = IERC1155::setApprovalForAllCall {
operator: config.neg_risk_adapter(),
approved: true,
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn ctf_transfer(
config: &Config,
from: Address,
to: Address,
token_id: U256,
amount: U256,
) -> Call {
let calldata = IERC1155::safeTransferFromCall {
from,
to,
id: token_id,
value: amount,
data: Bytes::new(),
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn ctf_split_position(
config: &Config,
condition_id: B256,
partition: Vec<U256>,
amount: U256,
) -> Call {
let calldata = IConditionalTokens::splitPositionCall {
collateralToken: config.usdc_e(),
parentCollectionId: B256::ZERO,
conditionId: condition_id,
partition,
amount,
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn ctf_merge_positions(
config: &Config,
condition_id: B256,
partition: Vec<U256>,
amount: U256,
) -> Call {
let calldata = IConditionalTokens::mergePositionsCall {
collateralToken: config.usdc_e(),
parentCollectionId: B256::ZERO,
conditionId: condition_id,
partition,
amount,
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn ctf_redeem_positions(config: &Config, condition_id: B256, index_sets: Vec<U256>) -> Call {
let calldata = IConditionalTokens::redeemPositionsCall {
collateralToken: config.usdc_e(),
parentCollectionId: B256::ZERO,
conditionId: condition_id,
indexSets: index_sets,
};
(
config.conditional_tokens(),
Bytes::from(calldata.abi_encode()),
)
}
pub fn neg_risk_redeem_positions(condition_id: B256, amounts: Vec<U256>) -> Bytes {
Bytes::from(
INegRiskAdapter::redeemPositionsCall { conditionId: condition_id, amounts }.abi_encode(),
)
}
#[derive(Debug, Clone)]
pub struct SafeTransaction {
pub to: Address,
pub value: U256,
pub data: Vec<u8>,
pub operation: OperationType,
}
pub struct NonEmptyTransactions(Vec<SafeTransaction>);
impl NonEmptyTransactions {
pub fn new(transactions: Vec<SafeTransaction>) -> Result<Self, PolyrelError> {
if transactions.is_empty() {
return Err(PolyrelError::EmptyBatch);
}
Ok(Self(transactions))
}
pub fn into_inner(self) -> Vec<SafeTransaction> {
self.0
}
}
pub fn aggregate_transactions(
transactions: NonEmptyTransactions,
multisend_address: Address,
) -> SafeTransaction {
let txns = transactions.into_inner();
if txns.len() == 1 {
return txns.into_iter().next().expect("non-empty");
}
let encoded = encode_multisend_payload(&txns);
let calldata = IMultiSend::multiSendCall { transactions: encoded.into() }.abi_encode();
SafeTransaction {
to: multisend_address,
value: U256::ZERO,
data: calldata,
operation: OperationType::DelegateCall,
}
}
fn encode_multisend_payload(transactions: &[SafeTransaction]) -> Vec<u8> {
let mut encoded = Vec::new();
for tx in transactions {
encoded.push(tx.operation.as_u8());
encoded.extend_from_slice(tx.to.as_slice());
encoded.extend_from_slice(&tx.value.to_be_bytes::<32>());
encoded.extend_from_slice(&U256::from(tx.data.len()).to_be_bytes::<32>());
encoded.extend_from_slice(&tx.data);
}
encoded
}
pub fn derive_safe_address(owner: Address, safe_factory: Address, init_code_hash: B256) -> Address {
let encoded = {
let mut buf = [0u8; 32];
buf[12..].copy_from_slice(owner.as_slice());
buf
};
let salt = keccak256(encoded);
create2_address(safe_factory, salt, init_code_hash)
}
pub fn derive_proxy_address(
owner: Address,
proxy_factory: Address,
init_code_hash: B256,
) -> Address {
let salt = keccak256(owner.as_slice());
create2_address(proxy_factory, salt, init_code_hash)
}
fn create2_address(deployer: Address, salt: B256, init_code_hash: B256) -> Address {
let mut buf = [0u8; 1 + 20 + 32 + 32];
buf[0] = 0xff;
buf[1..21].copy_from_slice(deployer.as_slice());
buf[21..53].copy_from_slice(salt.as_slice());
buf[53..85].copy_from_slice(init_code_hash.as_slice());
let hash = keccak256(buf);
Address::from_slice(&hash[12..])
}
pub fn safe_tx_hash(
chain_id: u64,
safe_address: Address,
tx: &SafeTransaction,
nonce: U256,
) -> B256 {
let domain = alloy_sol_types::Eip712Domain {
chain_id: Some(U256::from(chain_id)),
verifying_contract: Some(safe_address),
..Default::default()
};
let safe_tx = SafeTx {
to: tx.to,
value: tx.value,
data: tx.data.clone().into(),
operation: tx.operation.as_u8(),
safeTxGas: U256::ZERO,
baseGas: U256::ZERO,
gasPrice: U256::ZERO,
gasToken: Address::ZERO,
refundReceiver: Address::ZERO,
nonce,
};
safe_tx.eip712_signing_hash(&domain)
}
pub(crate) async fn sign_safe_transaction<S: Signer + Sync>(
signer: &S,
config: &Config,
tx: SafeTransaction,
nonce: U256,
) -> Result<SubmitRequest, PolyrelError> {
let safe_address = derive_safe_address(
signer.address(),
config.safe_factory(),
config.safe_init_code_hash(),
);
let signing_hash = safe_tx_hash(config.chain_id(), safe_address, &tx, nonce);
let signature = signer
.sign_message(signing_hash.as_slice())
.await
.map_err(|e| PolyrelError::Signing(Cow::Owned(e.to_string())))?;
let packed = pack_safe_signature(&alloy_primitives::hex::encode(signature.as_bytes()))?;
Ok(SubmitRequest::builder()
.wallet_type(WalletType::Safe)
.from(signer.address().to_string().into())
.to(tx.to.to_string().into())
.maybe_proxy_wallet(Some(format!("{safe_address:#x}").into()))
.data(format!("0x{}", alloy_primitives::hex::encode(&tx.data)).into())
.maybe_nonce(Some(nonce.to_string().into()))
.signature(packed.into())
.signature_params(SignatureParams::safe(tx.operation.as_u8()))
.build())
}
pub(crate) async fn sign_safe_create_request<S: Signer + Sync>(
signer: &S,
config: &Config,
) -> Result<SubmitRequest, PolyrelError> {
let safe_address = derive_safe_address(
signer.address(),
config.safe_factory(),
config.safe_init_code_hash(),
);
let domain = alloy_sol_types::Eip712Domain {
name: Some(crate::SAFE_FACTORY_NAME.into()),
chain_id: Some(U256::from(config.chain_id())),
verifying_contract: Some(config.safe_factory()),
..Default::default()
};
let msg = CreateProxy {
paymentToken: Address::ZERO,
payment: U256::ZERO,
paymentReceiver: Address::ZERO,
};
let hash = msg.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&hash)
.await
.map_err(|e| PolyrelError::Signing(Cow::Owned(e.to_string())))?;
Ok(SubmitRequest::builder()
.wallet_type(WalletType::SafeCreate)
.from(signer.address().to_string().into())
.to(config.safe_factory().to_string().into())
.maybe_proxy_wallet(Some(format!("{safe_address:#x}").into()))
.data("0x".into())
.signature(format!("0x{}", alloy_primitives::hex::encode(signature.as_bytes())).into())
.signature_params(SignatureParams::safe_create())
.build())
}
pub struct ProxyTransactionArgs {
pub data: Bytes,
pub nonce: Cow<'static, str>,
pub gas_price: Cow<'static, str>,
pub gas_limit: Cow<'static, str>,
pub relay_address: Address,
}
pub async fn sign_proxy_transaction<S: Signer + Sync>(
signer: &S,
config: &Config,
args: ProxyTransactionArgs,
) -> Result<SubmitRequest, PolyrelError> {
let gas_limit = parse_u256(&args.gas_limit, FIELD_GAS_LIMIT)?;
if gas_limit.is_zero() {
return Err(PolyrelError::InvalidNumericField {
field: FIELD_GAS_LIMIT,
value: Cow::Borrowed("0"),
});
}
let factory = config.proxy_wallet_factory();
let proxy_address =
derive_proxy_address(signer.address(), factory, config.proxy_init_code_hash());
let struct_hash = proxy_struct_hash(
signer.address(),
factory,
&args.data,
"0",
&args.gas_price,
&args.gas_limit,
&args.nonce,
config.relay_hub(),
args.relay_address,
)?;
let signature = signer
.sign_message(struct_hash.as_slice())
.await
.map_err(|e| PolyrelError::Signing(Cow::Owned(e.to_string())))?;
Ok(SubmitRequest::builder()
.wallet_type(WalletType::Proxy)
.from(signer.address().to_string().into())
.to(factory.to_string().into())
.maybe_proxy_wallet(Some(proxy_address.to_string().into()))
.data(format!("0x{}", alloy_primitives::hex::encode(&args.data)).into())
.maybe_nonce(Some(args.nonce))
.signature(format!("0x{}", alloy_primitives::hex::encode(signature.as_bytes())).into())
.signature_params(SignatureParams::proxy(
args.gas_price,
args.gas_limit,
config.relay_hub(),
args.relay_address,
))
.build())
}
pub fn pack_safe_signature(sig_hex: &str) -> Result<String, PolyrelError> {
let raw = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
let mut bytes = alloy_primitives::hex::decode(raw)
.map_err(|_| PolyrelError::InvalidSignature(Cow::Borrowed("hex decode failed")))?;
const EXPECTED_LEN: usize = 65;
if bytes.len() != EXPECTED_LEN {
return Err(PolyrelError::InvalidSignature(Cow::Borrowed(
"signature must be 65 bytes",
)));
}
bytes[64] = match bytes[64] {
0 | 1 => bytes[64] + 31,
27 | 28 => bytes[64] + 4,
_ => {
return Err(PolyrelError::InvalidSignature(Cow::Borrowed(
"invalid v value in signature",
)));
},
};
Ok(format!("0x{}", alloy_primitives::hex::encode(bytes)))
}
fn parse_u256(value: &str, field: &'static str) -> Result<U256, PolyrelError> {
value.parse::<U256>().map_err(|_| PolyrelError::InvalidNumericField {
field,
value: Cow::Owned(value.to_owned()),
})
}
#[allow(clippy::too_many_arguments)]
fn proxy_struct_hash(
from: Address,
to: Address,
data: &[u8],
tx_fee: &str,
gas_price: &str,
gas_limit: &str,
nonce: &str,
relay_hub: Address,
relay_address: Address,
) -> Result<B256, PolyrelError> {
let prefix = b"rlx:";
let tx_fee_u256 = parse_u256(tx_fee, FIELD_TX_FEE)?;
let gas_price_u256 = parse_u256(gas_price, FIELD_GAS_PRICE)?;
let gas_limit_u256 = parse_u256(gas_limit, FIELD_GAS_LIMIT)?;
let nonce_u256 = parse_u256(nonce, FIELD_NONCE)?;
let mut buf = Vec::new();
buf.extend_from_slice(prefix);
buf.extend_from_slice(from.as_slice());
buf.extend_from_slice(to.as_slice());
buf.extend_from_slice(data);
buf.extend_from_slice(&tx_fee_u256.to_be_bytes::<32>());
buf.extend_from_slice(&gas_price_u256.to_be_bytes::<32>());
buf.extend_from_slice(&gas_limit_u256.to_be_bytes::<32>());
buf.extend_from_slice(&nonce_u256.to_be_bytes::<32>());
buf.extend_from_slice(relay_hub.as_slice());
buf.extend_from_slice(relay_address.as_slice());
Ok(keccak256(&buf))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_adjusts_v0_to_31() {
let mut sig = vec![0xaa; 64];
sig.push(0);
let hex = alloy_primitives::hex::encode(&sig);
let packed = pack_safe_signature(&hex).unwrap();
let bytes = alloy_primitives::hex::decode(packed.strip_prefix("0x").unwrap()).unwrap();
assert_eq!(bytes[64], 31);
}
#[test]
fn pack_adjusts_v1_to_32() {
let mut sig = vec![0xbb; 64];
sig.push(1);
let hex = alloy_primitives::hex::encode(&sig);
let packed = pack_safe_signature(&hex).unwrap();
let bytes = alloy_primitives::hex::decode(packed.strip_prefix("0x").unwrap()).unwrap();
assert_eq!(bytes[64], 32);
}
#[test]
fn pack_adjusts_v27_to_31() {
let mut sig = vec![0xcc; 64];
sig.push(27);
let hex = alloy_primitives::hex::encode(&sig);
let packed = pack_safe_signature(&hex).unwrap();
let bytes = alloy_primitives::hex::decode(packed.strip_prefix("0x").unwrap()).unwrap();
assert_eq!(bytes[64], 31);
}
#[test]
fn pack_adjusts_v28_to_32() {
let mut sig = vec![0xdd; 64];
sig.push(28);
let hex = alloy_primitives::hex::encode(&sig);
let packed = pack_safe_signature(&hex).unwrap();
let bytes = alloy_primitives::hex::decode(packed.strip_prefix("0x").unwrap()).unwrap();
assert_eq!(bytes[64], 32);
}
#[test]
fn pack_rejects_invalid_v() {
let mut sig = vec![0xee; 64];
sig.push(5);
let hex = alloy_primitives::hex::encode(&sig);
let result = pack_safe_signature(&hex);
assert!(result.is_err());
}
#[test]
fn pack_rejects_wrong_length() {
let hex = alloy_primitives::hex::encode(vec![0u8; 60]);
let result = pack_safe_signature(&hex);
assert!(result.is_err());
}
#[test]
fn pack_handles_0x_prefix() {
let mut sig = vec![0xaa; 64];
sig.push(0);
let hex = format!("0x{}", alloy_primitives::hex::encode(&sig));
let packed = pack_safe_signature(&hex);
assert!(packed.is_ok());
}
#[test]
fn proxy_struct_hash_rejects_invalid_nonce() {
let result = proxy_struct_hash(
Address::ZERO,
Address::ZERO,
&[],
"0",
"0",
"1000000",
"not_a_number",
Address::ZERO,
Address::ZERO,
);
assert!(result.is_err());
}
#[test]
fn proxy_struct_hash_rejects_invalid_gas_limit() {
let result = proxy_struct_hash(
Address::ZERO,
Address::ZERO,
&[],
"0",
"0",
"abc",
"0",
Address::ZERO,
Address::ZERO,
);
assert!(result.is_err());
}
#[test]
fn proxy_struct_hash_accepts_valid_inputs() {
let result = proxy_struct_hash(
Address::ZERO,
Address::ZERO,
&[0xde, 0xad],
"0",
"0",
"10000000",
"42",
Address::ZERO,
Address::ZERO,
);
assert!(result.is_ok());
}
#[test]
fn non_empty_transactions_rejects_empty() {
let result = NonEmptyTransactions::new(vec![]);
assert!(result.is_err());
}
#[test]
fn non_empty_transactions_accepts_one() {
let tx = SafeTransaction {
to: Address::ZERO,
value: U256::ZERO,
data: vec![],
operation: OperationType::Call,
};
let result = NonEmptyTransactions::new(vec![tx]);
assert!(result.is_ok());
}
#[test]
fn non_empty_proxy_calls_rejects_empty() {
let result = NonEmptyProxyCalls::new(vec![]);
assert!(result.is_none());
}
#[test]
fn non_empty_proxy_calls_accepts_one() {
let call = (Address::ZERO, Bytes::new());
let result = NonEmptyProxyCalls::new(vec![call]);
assert!(result.is_some());
}
#[test]
fn derive_safe_address_is_deterministic() {
let owner = Address::ZERO;
let factory = crate::SAFE_FACTORY;
let hash = crate::SAFE_INIT_CODE_HASH.into();
let a = derive_safe_address(owner, factory, hash);
let b = derive_safe_address(owner, factory, hash);
assert_eq!(a, b);
assert_ne!(a, Address::ZERO);
}
#[test]
fn derive_proxy_address_is_deterministic() {
let owner = Address::ZERO;
let factory = crate::PROXY_WALLET_FACTORY;
let hash = crate::PROXY_INIT_CODE_HASH.into();
let a = derive_proxy_address(owner, factory, hash);
let b = derive_proxy_address(owner, factory, hash);
assert_eq!(a, b);
assert_ne!(a, Address::ZERO);
}
#[test]
fn different_init_code_hash_yields_different_address() {
let owner = Address::ZERO;
let factory = crate::SAFE_FACTORY;
let default_hash: B256 = crate::SAFE_INIT_CODE_HASH.into();
let custom_hash = B256::ZERO;
let default_addr = derive_safe_address(owner, factory, default_hash);
let custom_addr = derive_safe_address(owner, factory, custom_hash);
assert_ne!(default_addr, custom_addr);
}
#[test]
fn usdc_approve_calldata_starts_with_approve_selector() {
let config = crate::types::Config::builder().build().unwrap();
let approve_selector: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
let (_, data) = usdc_approve_exchange(&config, U256::from(100));
assert_eq!(&data[..4], &approve_selector);
}
#[test]
fn ctf_approve_calldata_starts_with_set_approval_selector() {
let config = crate::types::Config::builder().build().unwrap();
let set_approval_selector: [u8; 4] = [0xa2, 0x2c, 0xb4, 0x65];
let (_, data) = ctf_approve_exchange(&config);
assert_eq!(&data[..4], &set_approval_selector);
}
#[test]
fn usdc_approve_conditional_tokens_encodes_correct_spender_and_amount() {
let config = crate::types::Config::builder().build().unwrap();
let amount = U256::from(100);
let (target, data) = usdc_approve_conditional_tokens(&config, amount);
assert_eq!(target, config.usdc_e());
assert_eq!(&data[..4], &[0x09, 0x5e, 0xa7, 0xb3]);
let spender = Address::from_slice(&data[16..36]);
assert_eq!(spender, config.conditional_tokens());
let encoded_amount = U256::from_be_slice(&data[36..68]);
assert_eq!(encoded_amount, amount);
}
#[test]
fn usdc_approve_neg_risk_adapter_encodes_correct_spender_and_amount() {
let config = crate::types::Config::builder().build().unwrap();
let amount = U256::from(42);
let (target, data) = usdc_approve_neg_risk_adapter(&config, amount);
assert_eq!(target, config.usdc_e());
assert_eq!(&data[..4], &[0x09, 0x5e, 0xa7, 0xb3]);
let spender = Address::from_slice(&data[16..36]);
assert_eq!(spender, config.neg_risk_adapter());
let encoded_amount = U256::from_be_slice(&data[36..68]);
assert_eq!(encoded_amount, amount);
}
#[test]
fn ctf_approve_neg_risk_adapter_encodes_correct_operator_and_approved() {
let config = crate::types::Config::builder().build().unwrap();
let (target, data) = ctf_approve_neg_risk_adapter(&config);
assert_eq!(target, config.conditional_tokens());
assert_eq!(&data[..4], &[0xa2, 0x2c, 0xb4, 0x65]);
let operator = Address::from_slice(&data[16..36]);
assert_eq!(operator, config.neg_risk_adapter());
assert_eq!(data[67], 1);
}
#[test]
fn aggregate_single_returns_directly() {
let tx = SafeTransaction {
to: Address::ZERO,
value: U256::ZERO,
data: vec![0xde, 0xad],
operation: OperationType::Call,
};
let batch = NonEmptyTransactions::new(vec![tx]).unwrap();
let result = aggregate_transactions(batch, crate::SAFE_MULTISEND);
assert_eq!(result.operation, OperationType::Call);
assert_eq!(result.data, vec![0xde, 0xad]);
}
#[test]
fn aggregate_multiple_produces_delegate_call() {
let tx1 = SafeTransaction {
to: Address::ZERO,
value: U256::ZERO,
data: vec![0x01],
operation: OperationType::Call,
};
let tx2 = SafeTransaction {
to: Address::ZERO,
value: U256::ZERO,
data: vec![0x02],
operation: OperationType::Call,
};
let batch = NonEmptyTransactions::new(vec![tx1, tx2]).unwrap();
let result = aggregate_transactions(batch, crate::SAFE_MULTISEND);
assert_eq!(result.operation, OperationType::DelegateCall);
assert_eq!(result.to, crate::SAFE_MULTISEND);
}
#[tokio::test]
async fn sign_proxy_transaction_rejects_zero_gas_limit() {
let signer = alloy_signer_local::PrivateKeySigner::random();
let config = crate::types::Config::builder().build().unwrap();
let args = ProxyTransactionArgs {
data: Bytes::from(vec![0xde, 0xad]),
nonce: "1".into(),
gas_price: "0".into(),
gas_limit: "0".into(),
relay_address: Address::ZERO,
};
let result = sign_proxy_transaction(&signer, &config, args).await;
assert!(matches!(
result,
Err(PolyrelError::InvalidNumericField { field: FIELD_GAS_LIMIT, .. })
));
}
#[tokio::test]
async fn sign_safe_create_request_produces_correct_fields() {
let signer = alloy_signer_local::PrivateKeySigner::random();
let config = crate::types::Config::builder().build().unwrap();
let request = sign_safe_create_request(&signer, &config).await.unwrap();
let expected_safe = derive_safe_address(
signer.address(),
config.safe_factory(),
config.safe_init_code_hash(),
);
assert_eq!(request.wallet_type, WalletType::SafeCreate);
assert_eq!(request.data, "0x");
assert_eq!(request.to, config.safe_factory().to_string());
assert_eq!(request.from, signer.address().to_string());
assert_eq!(
request.proxy_wallet.as_deref(),
Some(format!("{expected_safe:#x}").as_str())
);
assert!(request.signature.starts_with("0x"));
assert!(request.nonce.is_none());
let zero = Address::ZERO.to_string();
let params = &request.signature_params;
assert_eq!(params.payment_token.as_deref(), Some(zero.as_str()));
assert_eq!(params.payment.as_deref(), Some("0"));
assert_eq!(params.payment_receiver.as_deref(), Some(zero.as_str()));
assert!(params.gas_price.is_none());
}
}