Support conatrcts call args in rhai bindings
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled

This commit is contained in:
2025-05-10 00:42:21 +03:00
parent 654f91b849
commit f669bdb84f
12 changed files with 542 additions and 38 deletions

View File

@@ -4,8 +4,12 @@
//! It parses command line arguments and calls into the implementation in the cmd module.
use clap::{App, Arg};
use env_logger;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the logger
env_logger::init();
// Parse command line arguments
let matches = App::new("herodo")
.version("0.1.0")
@@ -27,4 +31,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Call the run function from the cmd module
sal::cmd::herodo::run(script_path)
}
}

View File

@@ -114,15 +114,35 @@ pub async fn call_write_function(
let call_data = function.encode_input(&args)
.map_err(|e| CryptoError::ContractError(format!("Failed to encode function call: {}", e)))?;
// Create the transaction request
// Create the transaction request with gas limit
let tx = TransactionRequest::new()
.to(contract.address)
.data(call_data);
.data(call_data)
.gas(U256::from(300000)); // Set a reasonable gas limit
// Send the transaction using the client directly
let pending_tx = client.send_transaction(tx, None)
.await
.map_err(|e| CryptoError::ContractError(format!("Failed to send transaction: {}", e)))?;
log::info!("Sending transaction to contract at {}", contract.address);
log::info!("Function: {}, Args: {:?}", function_name, args);
// Log detailed information about the transaction
log::debug!("Sending transaction to contract at {}", contract.address);
log::debug!("Function: {}, Args: {:?}", function_name, args);
log::debug!("From address: {}", wallet.address);
log::debug!("Gas limit: {:?}", tx.gas);
let pending_tx = match client.send_transaction(tx, None).await {
Ok(pending_tx) => {
log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
pending_tx
},
Err(e) => {
// Log the error for debugging
log::error!("Failed to send transaction: {}", e);
log::error!("ERROR DETAILS: {:?}", e);
return Err(CryptoError::ContractError(format!("Failed to send transaction: {}", e)));
}
};
// Return the transaction hash
Ok(pending_tx.tx_hash())

View File

@@ -0,0 +1,183 @@
//! Utility functions for smart contract interactions.
use ethers::abi::{Abi, Token, ParamType};
use ethers::types::{Address, U256};
use std::str::FromStr;
use rhai::{Dynamic, Array};
/// Convert Rhai Dynamic values to ethers Token types
pub fn convert_rhai_to_token(value: &Dynamic, expected_type: Option<&ParamType>) -> Result<Token, String> {
match value {
// Handle integers
v if v.is_int() => {
let i = v.as_int().unwrap();
if let Some(param_type) = expected_type {
match param_type {
ParamType::Uint(_) => Ok(Token::Uint(U256::from(i as u64))),
ParamType::Int(_) => {
// Convert to I256 - in a real implementation, we would handle this properly
// For now, we'll just use U256 for both types
Ok(Token::Uint(U256::from(i as u64)))
},
_ => Err(format!("Expected {}, got integer", param_type))
}
} else {
// Default to Uint256 if no type info
Ok(Token::Uint(U256::from(i as u64)))
}
},
// Handle strings and addresses
v if v.is_string() => {
let s = v.to_string();
if let Some(param_type) = expected_type {
match param_type {
ParamType::Address => {
match Address::from_str(&s) {
Ok(addr) => Ok(Token::Address(addr)),
Err(e) => Err(format!("Invalid address format: {}", e))
}
},
ParamType::String => Ok(Token::String(s)),
ParamType::Bytes => {
// Handle hex string conversion to bytes
if s.starts_with("0x") {
match ethers::utils::hex::decode(&s[2..]) {
Ok(bytes) => Ok(Token::Bytes(bytes)),
Err(e) => Err(format!("Invalid hex string: {}", e))
}
} else {
Ok(Token::Bytes(s.as_bytes().to_vec()))
}
},
_ => Err(format!("Expected {}, got string", param_type))
}
} else {
// Try to detect type from string format
if s.starts_with("0x") && s.len() == 42 {
// Likely an address
match Address::from_str(&s) {
Ok(addr) => Ok(Token::Address(addr)),
Err(_) => Ok(Token::String(s))
}
} else {
Ok(Token::String(s))
}
}
},
// Handle booleans
v if v.is_bool() => {
let b = v.as_bool().unwrap();
if let Some(param_type) = expected_type {
if matches!(param_type, ParamType::Bool) {
Ok(Token::Bool(b))
} else {
Err(format!("Expected {}, got boolean", param_type))
}
} else {
Ok(Token::Bool(b))
}
},
// Handle arrays
v if v.is_array() => {
let arr = v.clone().into_array().unwrap();
if let Some(ParamType::Array(inner_type)) = expected_type {
let mut tokens = Vec::new();
for item in arr.iter() {
match convert_rhai_to_token(item, Some(inner_type)) {
Ok(token) => tokens.push(token),
Err(e) => return Err(e)
}
}
Ok(Token::Array(tokens))
} else {
Err("Array type mismatch or no type information available".to_string())
}
},
// Handle other types or return error
_ => Err(format!("Unsupported Rhai type: {:?}", value))
}
}
/// Validate and convert arguments based on function ABI
pub fn prepare_function_arguments(
abi: &Abi,
function_name: &str,
args: &Array
) -> Result<Vec<Token>, String> {
// Get the function from the ABI
let function = abi.function(function_name)
.map_err(|e| format!("Function not found in ABI: {}", e))?;
// Check if number of arguments matches
if function.inputs.len() != args.len() {
return Err(format!(
"Wrong number of arguments for function '{}': expected {}, got {}",
function_name, function.inputs.len(), args.len()
));
}
// Convert each argument according to the expected type
let mut tokens = Vec::new();
for (i, (param, arg)) in function.inputs.iter().zip(args.iter()).enumerate() {
match convert_rhai_to_token(arg, Some(&param.kind)) {
Ok(token) => tokens.push(token),
Err(e) => return Err(format!("Error converting argument {}: {}", i, e))
}
}
Ok(tokens)
}
/// Convert ethers Token to Rhai Dynamic value
pub fn convert_token_to_rhai(tokens: &[Token]) -> Dynamic {
if tokens.is_empty() {
return Dynamic::UNIT;
}
// If there's only one return value, return it directly
if tokens.len() == 1 {
return token_to_dynamic(&tokens[0]);
}
// If there are multiple return values, return them as an array
let mut array = Array::new();
for token in tokens {
array.push(token_to_dynamic(token));
}
Dynamic::from(array)
}
/// Convert a single token to a Dynamic value
pub fn token_to_dynamic(token: &Token) -> Dynamic {
match token {
Token::Address(addr) => Dynamic::from(format!("{:?}", addr)),
Token::Bytes(bytes) => Dynamic::from(ethers::utils::hex::encode(bytes)),
Token::Int(i) => Dynamic::from(i.to_string()),
Token::Uint(u) => Dynamic::from(u.to_string()),
Token::Bool(b) => Dynamic::from(*b),
Token::String(s) => Dynamic::from(s.clone()),
Token::Array(arr) => {
let mut rhai_arr = Array::new();
for item in arr {
rhai_arr.push(token_to_dynamic(item));
}
Dynamic::from(rhai_arr)
},
Token::Tuple(tuple) => {
let mut rhai_arr = Array::new();
for item in tuple {
rhai_arr.push(token_to_dynamic(item));
}
Dynamic::from(rhai_arr)
},
// Handle other token types
_ => {
log::warn!("Unsupported token type: {:?}", token);
Dynamic::UNIT
}
}
}

View File

@@ -16,6 +16,7 @@ mod provider;
mod transaction;
mod storage;
mod contract;
mod contract_utils;
pub mod networks;
#[cfg(test)]
pub mod tests;
@@ -76,3 +77,11 @@ pub use contract::{
call_write_function,
estimate_gas,
};
// Re-export contract utility functions
pub use contract_utils::{
convert_rhai_to_token,
prepare_function_arguments,
convert_token_to_rhai,
token_to_dynamic,
};

View File

@@ -41,7 +41,7 @@ pub fn gnosis() -> NetworkConfig {
pub fn peaq() -> NetworkConfig {
NetworkConfig {
name: names::PEAQ.to_string(),
chain_id: 1701,
chain_id: 3338,
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
explorer_url: "https://peaq.subscan.io/".to_string(),
token_symbol: "PEAQ".to_string(),

View File

@@ -0,0 +1,47 @@
//! Tests for smart contract argument handling functionality.
use ethers::types::Address;
use std::str::FromStr;
use crate::hero_vault::ethereum::*;
#[test]
fn test_contract_creation() {
// Create a simple ABI
let abi_json = r#"[
{
"inputs": [],
"name": "getValue",
"outputs": [{"type": "uint256", "name": ""}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"type": "uint256", "name": "newValue"}],
"name": "setValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]"#;
// Parse the ABI
let abi = load_abi_from_json(abi_json).unwrap();
// Create a contract address
let address = Address::from_str("0x1234567890123456789012345678901234567890").unwrap();
// Create a network config
let network = networks::gnosis();
// Create a contract
let contract = Contract::new(address, abi, network);
// Verify the contract was created correctly
assert_eq!(contract.address, address);
assert_eq!(contract.network.name, "Gnosis");
// Verify the ABI contains the expected functions
assert!(contract.abi.function("getValue").is_ok());
assert!(contract.abi.function("setValue").is_ok());
}

View File

@@ -4,3 +4,4 @@ mod wallet_tests;
mod network_tests;
mod transaction_tests;
mod contract_tests;
mod contract_args_tests;

View File

@@ -11,7 +11,7 @@ fn test_network_config() {
let peaq = networks::peaq();
assert_eq!(peaq.name, "Peaq");
assert_eq!(peaq.chain_id, 1701);
assert_eq!(peaq.chain_id, 3338);
assert_eq!(peaq.token_symbol, "PEAQ");
let agung = networks::agung();

View File

@@ -9,10 +9,10 @@ use std::sync::Mutex;
use once_cell::sync::Lazy;
use tokio::runtime::Runtime;
use ethers::types::{Address, U256};
use ethers::abi::Token;
use std::str::FromStr;
use crate::hero_vault::{keypair, symmetric, ethereum};
use crate::hero_vault::ethereum::{prepare_function_arguments, convert_token_to_rhai};
// Global Tokio runtime for blocking async operations
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
@@ -750,8 +750,15 @@ fn load_contract_abi_from_file(network_name: &str, address: &str, file_path: &st
}
}
// Call a read-only function on a contract
fn call_contract_read(contract_json: &str, function_name: &str) -> Dynamic {
// Use the utility functions from the ethereum module
// Call a read-only function on a contract (no arguments version)
fn call_contract_read_no_args(contract_json: &str, function_name: &str) -> Dynamic {
call_contract_read(contract_json, function_name, rhai::Array::new())
}
// Call a read-only function on a contract with arguments
fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Array) -> Dynamic {
// Deserialize the contract
let contract: ethereum::Contract = match serde_json::from_str(contract_json) {
Ok(contract) => contract,
@@ -761,6 +768,15 @@ fn call_contract_read(contract_json: &str, function_name: &str) -> Dynamic {
}
};
// Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens,
Err(e) => {
log::error!("Error preparing arguments: {}", e);
return Dynamic::UNIT;
}
};
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
@@ -779,32 +795,11 @@ fn call_contract_read(contract_json: &str, function_name: &str) -> Dynamic {
}
};
// For simplicity, we're not handling arguments in this implementation
let tokens: Vec<Token> = Vec::new();
// Execute the call in a blocking manner
match rt.block_on(async {
ethereum::call_read_function(&contract, &provider, function_name, tokens).await
}) {
Ok(result) => {
// Convert the result to a Rhai value
if result.is_empty() {
Dynamic::UNIT
} else {
// For simplicity, we'll just return the first value as a string
match &result[0] {
Token::String(s) => Dynamic::from(s.clone()),
Token::Uint(u) => Dynamic::from(u.to_string()),
Token::Int(i) => Dynamic::from(i.to_string()),
Token::Bool(b) => Dynamic::from(*b),
Token::Address(a) => Dynamic::from(format!("{:?}", a)),
_ => {
log::error!("Unsupported return type");
Dynamic::UNIT
}
}
}
},
Ok(result) => convert_token_to_rhai(&result),
Err(e) => {
log::error!("Failed to call contract function: {}", e);
Dynamic::UNIT
@@ -812,8 +807,13 @@ fn call_contract_read(contract_json: &str, function_name: &str) -> Dynamic {
}
}
// Call a state-changing function on a contract
fn call_contract_write(contract_json: &str, function_name: &str) -> String {
// Call a state-changing function on a contract (no arguments version)
fn call_contract_write_no_args(contract_json: &str, function_name: &str) -> String {
call_contract_write(contract_json, function_name, rhai::Array::new())
}
// Call a state-changing function on a contract with arguments
fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Array) -> String {
// Deserialize the contract
let contract: ethereum::Contract = match serde_json::from_str(contract_json) {
Ok(contract) => contract,
@@ -823,6 +823,15 @@ fn call_contract_write(contract_json: &str, function_name: &str) -> String {
}
};
// Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens,
Err(e) => {
log::error!("Error preparing arguments: {}", e);
return String::new();
}
};
// Get the runtime
let rt = match RUNTIME.lock() {
Ok(rt) => rt,
@@ -851,15 +860,19 @@ fn call_contract_write(contract_json: &str, function_name: &str) -> String {
}
};
// For simplicity, we're not handling arguments in this implementation
let tokens: Vec<Token> = Vec::new();
// Execute the transaction in a blocking manner
match rt.block_on(async {
ethereum::call_write_function(&contract, &wallet, &provider, function_name, tokens).await
}) {
Ok(tx_hash) => format!("{:?}", tx_hash),
Err(e) => {
// Log the error details for debugging
log::debug!("\nERROR DETAILS: Transaction failed: {}", e);
log::debug!("Contract address: {}", contract.address);
log::debug!("Function: {}", function_name);
log::debug!("Arguments: {:?}", args);
log::debug!("Wallet address: {}", wallet.address);
log::debug!("Network: {}", contract.network.name);
log::error!("Transaction failed: {}", e);
String::new()
}
@@ -917,7 +930,13 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box<EvalAltResu
// Register smart contract functions
engine.register_fn("load_contract_abi", load_contract_abi);
engine.register_fn("load_contract_abi_from_file", load_contract_abi_from_file);
// Register the read function with different arities
engine.register_fn("call_contract_read", call_contract_read_no_args);
engine.register_fn("call_contract_read", call_contract_read);
// Register the write function with different arities
engine.register_fn("call_contract_write", call_contract_write_no_args);
engine.register_fn("call_contract_write", call_contract_write);
Ok(())