use crate::{
commands::defi::create_h160_param, errors::CliError, print_error, print_info, print_success,
prompt_password,
};
use base64::{engine::general_purpose, Engine as _};
use clap::{Args, Subcommand};
use neo3::{
builder::{AccountSigner, ScriptBuilder, Signer, TransactionBuilder},
codec::NeoSerializable,
neo_clients::APITrait,
neo_contract::PolicyContract,
neo_protocol::AccountTrait,
neo_types::{ContractManifest, NefFile},
prelude::*,
};
use primitive_types::H160;
use std::{path::PathBuf, str::FromStr};
#[derive(Args, Debug)]
pub struct ContractArgs {
#[command(subcommand)]
pub command: ContractCommands,
}
#[derive(Subcommand, Debug)]
pub enum ContractCommands {
Deploy {
#[arg(short, long)]
nef: PathBuf,
#[arg(short, long)]
manifest: PathBuf,
#[arg(short, long)]
account: Option<String>,
},
Update {
#[arg(short, long)]
script_hash: String,
#[arg(short, long)]
nef: PathBuf,
#[arg(short, long)]
manifest: PathBuf,
#[arg(short, long)]
account: Option<String>,
},
Invoke {
#[arg(short, long)]
script_hash: String,
#[arg(short, long)]
method: String,
#[arg(short, long)]
params: Option<String>,
#[arg(short, long)]
account: Option<String>,
#[arg(short, long, default_value = "false")]
test_invoke: bool,
},
ListNativeContracts,
Policy,
}
pub async fn handle_contract_command(
args: ContractArgs,
state: &mut crate::commands::wallet::CliState,
) -> Result<(), CliError> {
match args.command {
ContractCommands::Deploy { nef, manifest, account } => {
deploy_contract(nef, manifest, account, state).await
},
ContractCommands::Update { script_hash, nef, manifest, account } => {
update_contract(script_hash, nef, manifest, account, state).await
},
ContractCommands::Invoke { script_hash, method, params, account, test_invoke } => {
invoke_contract(script_hash, method, params, account, test_invoke, state).await
},
ContractCommands::ListNativeContracts => list_native_contracts(state).await,
ContractCommands::Policy => show_policy(state).await,
}
}
async fn deploy_contract(
nef_path: PathBuf,
manifest_path: PathBuf,
account: Option<String>,
state: &mut crate::commands::wallet::CliState,
) -> Result<(), CliError> {
if state.wallet.is_none() {
print_error("No wallet is currently open");
return Err(CliError::Wallet("No wallet is currently open".to_string()));
}
if state.rpc_client.is_none() {
print_error("No RPC client is connected. Please connect to a node first.");
return Err(CliError::Network("No RPC client is connected".to_string()));
}
if !nef_path.exists() {
print_error(&format!("NEF file not found: {:?}", nef_path));
return Err(CliError::Input(format!("NEF file not found: {:?}", nef_path)));
}
if !manifest_path.exists() {
print_error(&format!("Manifest file not found: {:?}", manifest_path));
return Err(CliError::Input(format!("Manifest file not found: {:?}", manifest_path)));
}
print_info("Deploying smart contract...");
let nef_bytes = std::fs::read(&nef_path).map_err(|e| CliError::Io(e))?;
let manifest_json = std::fs::read_to_string(&manifest_path).map_err(|e| CliError::Io(e))?;
let _nef = NefFile::deserialize(&nef_bytes)
.map_err(|e| CliError::Input(format!("Failed to parse NEF file: {}", e)))?;
let _manifest: ContractManifest = serde_json::from_str(&manifest_json)
.map_err(|e| CliError::Input(format!("Failed to parse manifest file: {}", e)))?;
let wallet = state.wallet.as_ref().unwrap();
let account_address = match account {
Some(addr) => addr,
None => {
let accounts = wallet.get_accounts();
if accounts.is_empty() {
print_error("No accounts in wallet");
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
accounts[0].get_address().to_string()
},
};
let account_obj = wallet
.get_accounts()
.into_iter()
.find(|a| a.get_address() == account_address)
.cloned()
.ok_or_else(|| CliError::Wallet(format!("Account not found: {}", account_address)))?;
let password = prompt_password("Enter wallet password")?;
let rpc_client = state.rpc_client.as_ref().unwrap();
let params =
vec![ContractParameter::byte_array(nef_bytes), ContractParameter::string(manifest_json)];
let invocation_result = rpc_client
.invoke_function(
&H160::from_hex("fffdc93764dbaddd97c48f252a53ea4643faa3fd").unwrap(), "deploy".to_string(),
params.clone(),
Some(vec![Signer::from(
AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?,
)]),
)
.await
.map_err(|e| CliError::Network(format!("Failed to test invoke deploy: {}", e)))?;
let system_fee = invocation_result.gas_consumed;
print_info(&format!("Estimated system fee: {} GAS", system_fee));
let block_count = rpc_client
.get_block_count()
.await
.map_err(|e| CliError::Network(format!("Failed to get block count: {}", e)))?;
let valid_until_block = block_count + 100;
let signer = AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?;
let signers = vec![Signer::AccountSigner(signer)];
let mut tx_builder: TransactionBuilder<'_, neo3::neo_clients::HttpProvider> =
TransactionBuilder::new();
tx_builder.version(0);
tx_builder
.nonce((rand::random::<u32>() % 1000000) as u32)
.map_err(|e| CliError::from(e))?;
tx_builder.valid_until_block(valid_until_block).map_err(|e| CliError::from(e))?;
tx_builder.set_signers(signers).map_err(|e| CliError::from(e))?;
let method = "deploy".to_string();
let script = ScriptBuilder::new()
.contract_call(
&H160::from_hex("fffdc93764dbaddd97c48f252a53ea4643faa3fd").unwrap(),
&method,
¶ms,
None,
)
.map_err(|e| CliError::Builder(e.to_string()))?
.to_bytes();
tx_builder.set_script(Some(script));
tx_builder.set_additional_network_fee(100000000);
let mut tx = tx_builder
.build()
.await
.map_err(|e| CliError::Transaction(format!("Failed to build transaction: {}", e)))?;
print_info("Signing transaction with account's private key...");
let mut account_clone = account_obj.clone();
account_clone
.decrypt_private_key(&password)
.map_err(|e| CliError::Wallet(format!("Failed to decrypt private key: {}", e)))?;
let key_pair = account_clone
.key_pair()
.as_ref()
.ok_or_else(|| CliError::Wallet("No key pair available after decryption".to_string()))?
.clone();
let tx_hash = tx
.get_hash_data()
.await
.map_err(|e| CliError::Transaction(format!("Failed to get transaction hash: {}", e)))?;
let witness = neo3::builder::Witness::create(tx_hash, &key_pair)
.map_err(|e| CliError::Transaction(format!("Failed to create witness: {}", e)))?;
tx.add_witness(witness);
let mut encoder = neo3::codec::Encoder::new();
tx.encode(&mut encoder);
let tx_bytes = encoder.to_bytes();
let tx_json = serde_json::json!({
"jsonrpc": "2.0",
"method": "sendrawtransaction",
"params": [general_purpose::STANDARD.encode(&tx_bytes)],
"id": 1
})
.to_string();
let result = rpc_client
.send_raw_transaction(tx_json)
.await
.map_err(|e| CliError::Network(format!("Failed to send transaction: {}", e)))?;
print_success("Contract deployment transaction sent successfully");
println!("Transaction hash: {}", result.hash);
println!("Note: The contract hash can be obtained from the transaction when it is confirmed on the blockchain.");
Ok(())
}
async fn update_contract(
script_hash: String,
nef_path: PathBuf,
manifest_path: PathBuf,
account: Option<String>,
state: &mut crate::commands::wallet::CliState,
) -> Result<(), CliError> {
if state.wallet.is_none() {
print_error("No wallet is currently open");
return Err(CliError::Wallet("No wallet is currently open".to_string()));
}
if state.rpc_client.is_none() {
print_error("No RPC client is connected. Please connect to a node first.");
return Err(CliError::Network("No RPC client is connected".to_string()));
}
if !nef_path.exists() {
print_error(&format!("NEF file not found: {:?}", nef_path));
return Err(CliError::Input(format!("NEF file not found: {:?}", nef_path)));
}
if !manifest_path.exists() {
print_error(&format!("Manifest file not found: {:?}", manifest_path));
return Err(CliError::Input(format!("Manifest file not found: {:?}", manifest_path)));
}
print_info(&format!("Updating contract: {}", script_hash));
let nef_bytes = std::fs::read(&nef_path).map_err(|e| CliError::Io(e))?;
let manifest_json = std::fs::read_to_string(&manifest_path).map_err(|e| CliError::Io(e))?;
let _nef = NefFile::deserialize(&nef_bytes)
.map_err(|e| CliError::Input(format!("Failed to parse NEF file: {}", e)))?;
let _manifest: ContractManifest = serde_json::from_str(&manifest_json)
.map_err(|e| CliError::Input(format!("Failed to parse manifest file: {}", e)))?;
let wallet = state.wallet.as_ref().unwrap();
let account_address = match account {
Some(addr) => addr,
None => {
let accounts = wallet.get_accounts();
if accounts.is_empty() {
print_error("No accounts in wallet");
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
accounts[0].get_address().to_string()
},
};
let account_obj = wallet
.get_accounts()
.into_iter()
.find(|a| a.get_address() == account_address)
.cloned()
.ok_or_else(|| CliError::Wallet(format!("Account not found: {}", account_address)))?;
let password = prompt_password("Enter wallet password")?;
let contract_hash = H160::from_str(&script_hash)
.map_err(|_| CliError::Input("Invalid script hash format".to_string()))?;
let rpc_client = state.rpc_client.as_ref().unwrap();
let params = vec![
ContractParameter::h160(&contract_hash),
ContractParameter::byte_array(nef_bytes),
ContractParameter::string(manifest_json),
];
let invocation_result = rpc_client
.invoke_function(
&contract_hash,
"update".to_string(),
params.clone(),
Some(vec![Signer::from(
AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?,
)]),
)
.await
.map_err(|e| CliError::Network(format!("Failed to test invoke update: {}", e)))?;
let system_fee = invocation_result.gas_consumed;
print_info(&format!("Estimated system fee: {} GAS", system_fee));
let block_count = rpc_client
.get_block_count()
.await
.map_err(|e| CliError::Network(format!("Failed to get block count: {}", e)))?;
let valid_until_block = block_count + 100;
let signer = AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?;
let signers = vec![Signer::AccountSigner(signer)];
let mut tx_builder: TransactionBuilder<'_, neo3::neo_clients::HttpProvider> =
TransactionBuilder::new();
tx_builder.version(0);
tx_builder
.nonce((rand::random::<u32>() % 1000000) as u32)
.map_err(|e| CliError::from(e))?;
tx_builder.valid_until_block(valid_until_block).map_err(|e| CliError::from(e))?;
tx_builder.set_signers(signers).map_err(|e| CliError::from(e))?;
let method = "update".to_string();
let script = ScriptBuilder::new()
.contract_call(&contract_hash, &method, ¶ms, None)
.map_err(|e| CliError::Builder(e.to_string()))?
.to_bytes();
tx_builder.set_script(Some(script));
tx_builder.set_additional_network_fee(100000000);
let mut tx = tx_builder
.build()
.await
.map_err(|e| CliError::Transaction(format!("Failed to build transaction: {}", e)))?;
print_info("Signing transaction with account's private key...");
let mut account_clone = account_obj.clone();
account_clone
.decrypt_private_key(&password)
.map_err(|e| CliError::Wallet(format!("Failed to decrypt private key: {}", e)))?;
let key_pair = account_clone
.key_pair()
.as_ref()
.ok_or_else(|| CliError::Wallet("No key pair available after decryption".to_string()))?
.clone();
let tx_hash = tx
.get_hash_data()
.await
.map_err(|e| CliError::Transaction(format!("Failed to get transaction hash: {}", e)))?;
let witness = neo3::builder::Witness::create(tx_hash, &key_pair)
.map_err(|e| CliError::Transaction(format!("Failed to create witness: {}", e)))?;
tx.add_witness(witness);
let mut encoder = neo3::codec::Encoder::new();
tx.encode(&mut encoder);
let tx_bytes = encoder.to_bytes();
let tx_json = serde_json::json!({
"jsonrpc": "2.0",
"method": "sendrawtransaction",
"params": [general_purpose::STANDARD.encode(&tx_bytes)],
"id": 1
})
.to_string();
let result = rpc_client
.send_raw_transaction(tx_json)
.await
.map_err(|e| CliError::Network(format!("Failed to send transaction: {}", e)))?;
print_success("Contract updated successfully");
println!("Transaction hash: {}", result.hash);
Ok(())
}
async fn invoke_contract(
script_hash: String,
method: String,
params: Option<String>,
account: Option<String>,
test_invoke: bool,
state: &mut crate::commands::wallet::CliState,
) -> Result<(), CliError> {
if state.rpc_client.is_none() {
print_error("No RPC client is connected. Please connect to a node first.");
return Err(CliError::Network("No RPC client is connected".to_string()));
}
let parameters = match params {
Some(p) => {
let params_json: Vec<serde_json::Value> = serde_json::from_str(&p)
.map_err(|e| CliError::Input(format!("Invalid JSON parameters: {}", e)))?;
params_json
.into_iter()
.map(|v| contract_parameter_from_json(v))
.collect::<Result<Vec<_>, _>>()?
},
None => Vec::new(),
};
let contract_hash = H160::from_str(&script_hash)
.map_err(|_| CliError::Input("Invalid script hash format".to_string()))?;
let rpc_client = state.rpc_client.as_ref().unwrap();
if test_invoke {
print_info(&format!("Test invoking method '{}' on contract {}", method, script_hash));
let result = rpc_client
.invoke_function(&contract_hash, method.clone(), parameters, None)
.await
.map_err(|e| CliError::Network(format!("Failed to invoke function: {}", e)))?;
println!("Invocation result:");
println!(" State: {:?}", result.state);
println!(" Gas consumed: {}", result.gas_consumed);
println!(" Stack:");
for (i, item) in result.stack.iter().enumerate() {
println!(" {}: {:?}", i, item);
}
} else {
if state.wallet.is_none() {
print_error("No wallet is currently open");
return Err(CliError::Wallet("No wallet is currently open".to_string()));
}
print_info(&format!("Invoking method '{}' on contract {}", method, script_hash));
let wallet = state.wallet.as_ref().unwrap();
let account_address = match account {
Some(addr) => addr,
None => {
let accounts = wallet.get_accounts();
if accounts.is_empty() {
print_error("No accounts in wallet");
return Err(CliError::Wallet("No accounts in wallet".to_string()));
}
accounts[0].get_address().to_string()
},
};
let account_obj = wallet
.get_accounts()
.into_iter()
.find(|a| a.get_address() == account_address)
.cloned()
.ok_or_else(|| CliError::Wallet(format!("Account not found: {}", account_address)))?;
let password = prompt_password("Enter wallet password")?;
let invocation_result = rpc_client
.invoke_function(
&contract_hash,
method.clone(),
parameters.clone(),
Some(vec![Signer::from(
AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?,
)]),
)
.await
.map_err(|e| CliError::Network(format!("Failed to test invoke: {}", e)))?;
let system_fee = invocation_result.gas_consumed;
print_info(&format!("Estimated system fee: {} GAS", system_fee));
let block_count = rpc_client
.get_block_count()
.await
.map_err(|e| CliError::Network(format!("Failed to get block count: {}", e)))?;
let valid_until_block = block_count + 100;
let signer = AccountSigner::called_by_entry(&account_obj)
.map_err(|e| CliError::TransactionBuilder(e.to_string()))?;
let signers = vec![Signer::AccountSigner(signer)];
let mut tx_builder: TransactionBuilder<'_, neo3::neo_clients::HttpProvider> =
TransactionBuilder::new();
tx_builder.version(0);
tx_builder
.nonce((rand::random::<u32>() % 1000000) as u32)
.map_err(|e| CliError::from(e))?;
tx_builder.valid_until_block(valid_until_block).map_err(|e| CliError::from(e))?;
tx_builder.set_signers(signers).map_err(|e| CliError::from(e))?;
let script = ScriptBuilder::new()
.contract_call(&contract_hash, &method, ¶meters, None)
.map_err(|e| CliError::Builder(e.to_string()))?
.to_bytes();
tx_builder.set_script(Some(script));
tx_builder.set_additional_network_fee(100000000);
let mut tx = tx_builder
.build()
.await
.map_err(|e| CliError::Transaction(format!("Failed to build transaction: {}", e)))?;
print_info("Signing transaction with account's private key...");
let mut account_clone = account_obj.clone();
account_clone
.decrypt_private_key(&password)
.map_err(|e| CliError::Wallet(format!("Failed to decrypt private key: {}", e)))?;
let key_pair = account_clone
.key_pair()
.as_ref()
.ok_or_else(|| CliError::Wallet("No key pair available after decryption".to_string()))?
.clone();
let tx_hash = tx
.get_hash_data()
.await
.map_err(|e| CliError::Transaction(format!("Failed to get transaction hash: {}", e)))?;
let witness = neo3::builder::Witness::create(tx_hash, &key_pair)
.map_err(|e| CliError::Transaction(format!("Failed to create witness: {}", e)))?;
tx.add_witness(witness);
let mut encoder = neo3::codec::Encoder::new();
tx.encode(&mut encoder);
let tx_bytes = encoder.to_bytes();
let tx_json = serde_json::json!({
"jsonrpc": "2.0",
"method": "sendrawtransaction",
"params": [general_purpose::STANDARD.encode(&tx_bytes)],
"id": 1
})
.to_string();
let result = rpc_client
.send_raw_transaction(tx_json)
.await
.map_err(|e| CliError::Network(format!("Failed to send transaction: {}", e)))?;
print_success("Contract method invoked successfully");
println!("Transaction hash: {}", result.hash);
}
Ok(())
}
async fn list_native_contracts(
state: &mut crate::commands::wallet::CliState,
) -> Result<(), CliError> {
if state.rpc_client.is_none() {
print_error("No RPC client is connected. Please connect to a node first.");
return Err(CliError::Network("No RPC client is connected".to_string()));
}
print_info("Native contracts:");
let rpc_client = state.rpc_client.as_ref().unwrap();
let native_contracts = rpc_client
.get_native_contracts()
.await
.map_err(|e| CliError::Network(format!("Failed to get native contracts: {}", e)))?;
for (i, contract) in native_contracts.iter().enumerate() {
println!(
"{}. {} ({})",
i + 1,
contract.manifest().name.as_ref().unwrap_or(&"Unknown".to_string()),
contract.hash()
);
println!(" Supported Standards: {:?}", contract.manifest().supported_standards);
println!();
}
print_success("Native contracts retrieved successfully");
Ok(())
}
async fn show_policy(state: &mut crate::commands::wallet::CliState) -> Result<(), CliError> {
if state.rpc_client.is_none() {
print_error("No RPC client is connected. Please connect to a node first.");
return Err(CliError::Network("No RPC client is connected".to_string()));
}
let policy = PolicyContract::new(state.rpc_client.as_ref());
print_info("Fetching policy values...");
let fee_per_byte = policy
.get_fee_per_byte()
.await
.map_err(|e| CliError::Network(format!("Failed to get fee per byte: {}", e)))?;
let exec_fee_factor = policy
.get_exec_fee_factor()
.await
.map_err(|e| CliError::Network(format!("Failed to get exec fee factor: {}", e)))?;
let storage_price = policy
.get_storage_price()
.await
.map_err(|e| CliError::Network(format!("Failed to get storage price: {}", e)))?;
let pico_factor = match policy.get_exec_pico_fee_factor().await {
Ok(val) => val.to_string(),
Err(_) => "Not supported (Neo 3.9+)".to_string(),
};
let milliseconds_per_block = match policy.get_milliseconds_per_block().await {
Ok(val) => format!("{} ms", val),
Err(_) => "Not supported".to_string(),
};
println!("Policy Contract State:");
println!(" Fee Per Byte: {}", fee_per_byte);
println!(" Exec Fee Factor: {}", exec_fee_factor);
println!(" Storage Price: {}", storage_price);
println!(" Exec Pico Fee Factor: {}", pico_factor);
println!(" Milliseconds/Block: {}", milliseconds_per_block);
Ok(())
}
fn contract_parameter_from_json(value: serde_json::Value) -> Result<ContractParameter, CliError> {
match value {
serde_json::Value::Null => Ok(ContractParameter::any()),
serde_json::Value::Bool(b) => Ok(ContractParameter::bool(b)),
serde_json::Value::Number(n) => {
if n.is_i64() {
Ok(ContractParameter::integer(n.as_i64().unwrap()))
} else if n.is_f64() {
Ok(ContractParameter::string(n.to_string()))
} else {
Err(CliError::Input("Invalid number type".to_string()))
}
},
serde_json::Value::String(s) => {
if let Some(hex_str) = s.strip_prefix("0x") {
match hex::decode(hex_str) {
Ok(bytes) => Ok(ContractParameter::byte_array(bytes)),
Err(_) => Ok(ContractParameter::string(s)),
}
} else if let Some(hash_str) = s.strip_prefix("@") {
match H160::from_str(hash_str) {
Ok(hash) => create_h160_param(&format!("{:x}", hash)),
Err(_) => Ok(ContractParameter::string(s)),
}
} else {
Ok(ContractParameter::string(s))
}
},
serde_json::Value::Array(arr) => {
let mut params = Vec::new();
for item in arr {
params.push(contract_parameter_from_json(item)?);
}
Ok(ContractParameter::array(params))
},
serde_json::Value::Object(_) => {
Err(CliError::Input("Object parameters not supported".to_string()))
},
}
}