use futures::prelude::*;
use gear_core::{
ids::{ActorId, CodeId, prelude::*},
rpc::ReplyInfo,
};
use gear_core_errors::{ReplyCode, SuccessReplyReason};
use gsdk::{AccountKeyring, Api, Error, Result, gear};
use parity_scale_codec::Encode;
use std::{process::Command, str::FromStr, time::Instant};
use subxt::{
ext::subxt_rpcs::{Error as SubxtRpcError, UserError, rpc_params},
utils::{AccountId32, H256},
};
use tokio::time::{Duration, timeout};
use utils::dev_node;
mod utils;
#[tokio::test]
async fn pallet_errors_formatting() -> Result<()> {
let (_node, api) = dev_node().await;
let err = api
.unsigned()
.calculate_upload_gas(
AccountId32([0u8; 32]),
vec![],
vec![],
0,
true,
)
.await
.expect_err("Must return error");
let Error::SubxtRpc(err) = err else {
panic!("unexpected error variant: {err:?}")
};
let expected_err = SubxtRpcError::User(UserError {
code: 8000,
message: "Runtime error".into(),
data: Some(
serde_json::value::to_raw_value(
"Extrinsic `gear.upload_program` failed: 'ProgramConstructionFailed'",
)
.unwrap(),
),
});
assert_eq!(err.to_string(), expected_err.to_string());
Ok(())
}
#[tokio::test]
async fn test_calculate_upload_gas() -> Result<()> {
let (_node, api) = dev_node().await;
api.calculate_upload_gas(demo_messenger::WASM_BINARY.to_vec(), vec![], 0, true)
.await?;
Ok(())
}
#[tokio::test]
async fn test_calculate_create_gas() -> Result<()> {
let (_node, api) = dev_node().await;
api.upload_code(demo_messenger::WASM_BINARY.to_vec())
.await?;
let code_id = CodeId::generate(demo_messenger::WASM_BINARY);
let gas_info = api.calculate_create_gas(code_id, vec![], 0, true).await?;
api.create_program_bytes(code_id, vec![], vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_read_wasm_custom_section() -> Result<()> {
let (_node, api) = dev_node().await;
let wat_code = r#"
(module
(import "env" "memory" (memory 0))
(export "init" (func $init))
(func $init)
(@custom "sails:idl" "hello idl")
)
"#;
let wasm = wat::parse_str(wat_code).unwrap();
let upload = api.upload_code(wasm).await?;
let upload_block_hash = upload.block_hash;
let code_id = upload.value;
let present = api.read_wasm_custom_section(code_id, "sails:idl").await?;
assert_eq!(
present.as_ref().map(|bytes| bytes.0.as_slice()),
Some(b"hello idl".as_ref())
);
let present_at_upload = api
.read_wasm_custom_section_at(code_id, "sails:idl", upload_block_hash)
.await?;
assert_eq!(
present_at_upload.as_ref().map(|bytes| bytes.0.as_slice()),
Some(b"hello idl".as_ref())
);
let missing_section = api
.read_wasm_custom_section(code_id, "no:such:section")
.await?;
assert!(missing_section.is_none());
let unknown_code = api
.read_wasm_custom_section(CodeId::from([0u8; 32]), "sails:idl")
.await?;
assert!(unknown_code.is_none());
Ok(())
}
#[tokio::test]
async fn test_calculate_handle_gas() -> Result<()> {
let (_node, api) = dev_node().await;
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_messenger::WASM_BINARY), &salt);
api.upload_program_bytes(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
api.active_program(pid).await.is_ok(),
"Program not exists on chain."
);
let gas_info = api.calculate_handle_gas(pid, vec![], 0, true).await?;
api.send_message_bytes(pid, vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_calculate_reply_gas() -> Result<()> {
let (_node, api) = dev_node().await;
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_waiter::WASM_BINARY), &salt);
let payload = demo_waiter::Command::SendUpTo(AccountKeyring::Alice.to_account_id().into(), 10);
api.upload_program_bytes(
demo_waiter::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
api.active_program(pid).await.is_ok(),
"Program not exists on chain"
);
api.send_message(pid, payload, 100_000_000_000, 0).await?;
let mailbox = api.mailbox_messages(10).await?;
assert_eq!(mailbox.len(), 1);
let message_id = mailbox[0].0.id();
let gas_info = api.calculate_reply_gas(message_id, vec![], 0, true).await?;
api.send_reply_bytes(message_id, vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_subscribe_program_state_changes() -> Result<()> {
let (_node, api) = dev_node().await;
let mut subscription = api.subscribe_program_state_changes(None).await?;
let salt = b"state-change".to_vec();
let program_id = api
.upload_program_bytes(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?
.value
.1;
let expected_id = H256::from(program_id.into_bytes());
let change = timeout(Duration::from_secs(30), async {
loop {
let event = subscription.next().await;
println!("Got event: {event:?}");
match event {
Some(Ok(event)) if event.program_ids.contains(&expected_id) => break Ok(event),
Some(Ok(_)) => continue,
Some(Err(err)) => break Err(err),
None => break Err(Error::EventNotFound),
}
}
})
.await
.expect("timed out waiting for program state change")?;
assert!(change.program_ids.contains(&expected_id));
Ok(())
}
#[tokio::test]
async fn test_runtime_wasm_blob_version() -> Result<()> {
let git_commit_hash = {
match Command::new("git")
.args(["rev-parse", "--short=11", "HEAD"])
.output()
{
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_owned(),
Ok(o) => panic!("Git command failed with status: {}", o.status),
Err(err) => panic!("cargo:warning=Failed to execute git command: {err}"),
}
};
let (_node, api) = dev_node().await;
api.blocks()
.subscribe_finalized()
.await?
.then(|block| async { api.runtime_wasm_blob_version_at(block?.hash()).await })
.take(4)
.inspect_ok(|version| {
assert!(
version.ends_with(&git_commit_hash),
"Version `{}` must end with commit hash `{}`",
version,
git_commit_hash
)
})
.try_fold(
api.runtime_wasm_blob_version().await?,
|version_a, version_b| {
assert_eq!(version_a, version_b);
future::ready(Ok(version_b))
},
)
.await?;
Ok(())
}
#[tokio::test]
async fn test_runtime_wasm_blob_version_history() -> Result<()> {
let api = Api::new("wss://archive-rpc.vara.network:443").await?;
let no_method_block_hash =
H256::from_str("0xa84349fc30b8f2d02cc31d49fe8d4a45b6de5a3ac1f1ad975b8920b0628dd6b9")
.unwrap();
let err = api
.runtime_wasm_blob_version_at(no_method_block_hash)
.await
.unwrap_err();
let Error::SubxtRpc(wasm_blob_version_err) = err else {
panic!("unexpected error variant: {err:?}")
};
let err = SubxtRpcError::User(UserError {
code: 9000,
message: "Unable to find WASM blob version in WASM blob".into(),
data: None,
});
assert_eq!(wasm_blob_version_err.to_string(), err.to_string());
Ok(())
}
#[tokio::test]
async fn test_original_code_storage() -> Result<()> {
let (_node, api) = dev_node().await;
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_messenger::WASM_BINARY), &salt);
api.upload_program_bytes(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
let program = api.active_program(pid).await?;
let code = api
.original_code(program.code_id.into_bytes().into())
.await?;
assert_eq!(
code,
demo_messenger::WASM_BINARY.to_vec(),
"Program code mismatched"
);
Ok(())
}
#[ignore]
#[tokio::test]
async fn test_program_counters() -> Result<()> {
let uri = String::from("wss://testnet.vara.network:443");
let instant = Instant::now();
let (block_hash, block_number, count_program, count_active_program, count_memory_page) =
query_program_counters(&uri, None).await?;
println!("elapsed = {:?}", instant.elapsed());
println!(
"testnet block_hash = {block_hash}, block_number = {block_number}, count_program = {count_program}, count_active_program = {count_active_program}, count_memory_page = {count_memory_page}"
);
Ok(())
}
#[tokio::test]
async fn test_calculate_reply_for_handle() -> Result<()> {
use demo_fungible_token::{FTAction, FTEvent, InitConfig, WASM_BINARY};
let (_node, api) = dev_node().await;
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(WASM_BINARY), &salt);
let payload = InitConfig::test_sequence();
api.upload_program(WASM_BINARY.to_vec(), salt, payload, 100_000_000_000, 0)
.await?;
assert!(
api.active_program(pid).await.is_ok(),
"Program not exists on chain."
);
let message_in = FTAction::TotalSupply;
let message_out = FTEvent::TotalSupply(0);
let reply_info = api
.calculate_reply_for_handle(pid, message_in.encode(), 100_000_000_000, 0)
.await?;
let raw_reply_info: serde_json::Value = api
.rpc()
.request(
"gear_calculateReplyForHandle",
rpc_params![
H256::from_slice(api.account_id().as_ref()),
H256(pid.into_bytes()),
hex::encode(message_in.encode()),
100_000_000_000u64,
0
],
)
.await?;
let reply_result = api
.calculate_reply_for_handle_result(pid, message_in.encode(), 100_000_000_000, 0)
.await?;
assert_eq!(
reply_info,
ReplyInfo {
payload: message_out.encode(),
value: 0,
code: ReplyCode::Success(SuccessReplyReason::Manual)
}
);
assert_eq!(
raw_reply_info,
serde_json::json!({
"payload": format!("0x{}", hex::encode(message_out.encode())),
"value": 0,
"code": {
"Success": "Manual"
}
})
);
assert_eq!(reply_result.reply, reply_info);
assert!(reply_result.messages.is_empty());
Ok(())
}
#[tokio::test]
async fn test_calculate_reply_for_handle_does_not_change_state() -> Result<()> {
let (_node, api) = dev_node().await;
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_vec::WASM_BINARY), &salt);
api.upload_program_bytes(
demo_vec::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
api.active_program(pid).await.is_ok(),
"Program not exists on chain."
);
let initial_state = api.read_state_bytes(pid, vec![]).await?;
let reply_info = api
.calculate_reply_for_handle(pid, 42i32.encode(), 100_000_000_000, 0)
.await?;
assert_eq!(
reply_info,
ReplyInfo {
payload: 42i32.encode(),
value: 0,
code: ReplyCode::Success(SuccessReplyReason::Manual)
}
);
let calculated_state = api.read_state_bytes(pid, vec![]).await?;
assert_eq!(initial_state, calculated_state);
api.send_message(pid, 42i32, 100_000_000_000, 0).await?;
let updated_state = api.read_state_bytes(pid, vec![]).await?;
assert_ne!(initial_state, updated_state);
Ok(())
}
async fn query_program_counters(
uri: &str,
block_hash: Option<H256>,
) -> Result<(H256, u32, u64, u64, u64)> {
use gsdk::gear::runtime_types::gear_core::program::Program;
use parity_scale_codec::Decode;
let api = Api::new(uri).await?.signed_as_alice();
let (block_hash, block_number) = match block_hash {
Some(hash) => {
let block = api.blocks().at(hash).await?;
assert_eq!(hash, block.hash(), "block hash mismatched");
(hash, block.number())
}
None => {
let latest_block = api.blocks().at_latest().await?;
(latest_block.hash(), latest_block.number())
}
};
let storage = api.storage_at(Some(block_hash)).await?;
let addr = gear::storage().gear_program().program_storage_iter();
let mut iter = storage.iter(addr).await?;
let mut count_memory_page = 0u64;
let mut count_program = 0u64;
let mut count_active_program = 0u64;
while let Some(pair) = iter.next().await {
let pair = pair?;
let (key, program) = (pair.key_bytes, pair.value);
count_program += 1;
let program_id = ActorId::decode(&mut key.as_ref())?;
if let Program::Active(_) = program {
count_active_program += 1;
count_memory_page += api.program_pages(program_id).await?.len() as u64;
}
}
Ok((
block_hash,
block_number,
count_program,
count_active_program,
count_memory_page,
))
}