use futures::StreamExt;
use gear_core::{
ids::{ActorId, CodeId, prelude::*},
rpc::ReplyInfo,
};
use gear_core_errors::{ReplyCode, SuccessReplyReason};
use gsdk::{Api, Error, Event, Result, gear};
use parity_scale_codec::Encode;
use std::{borrow::Cow, process::Command, str::FromStr, time::Instant};
use subxt::{
ext::subxt_rpcs::{Error as SubxtRpcError, UserError},
utils::H256,
};
use tokio::time::{Duration, timeout};
use utils::{alice_account_id, dev_node};
mod utils;
#[tokio::test]
async fn pallet_errors_formatting() -> Result<()> {
let node = dev_node();
let api = Api::new(node.ws().as_str()).await?;
let err = api
.calculate_upload_gas(
[0u8; 32].into(),
vec![],
vec![],
0,
true,
None,
)
.await
.expect_err("Must return error")
.unwrap_subxt_rpc();
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 = dev_node();
let api = Api::new(node.ws().as_str()).await?;
let alice: [u8; 32] = *alice_account_id().as_ref();
api.calculate_upload_gas(
alice.into(),
demo_messenger::WASM_BINARY.to_vec(),
vec![],
0,
true,
None,
)
.await?;
Ok(())
}
#[tokio::test]
async fn test_calculate_create_gas() -> Result<()> {
let node = dev_node();
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
signer
.calls()
.upload_code(demo_messenger::WASM_BINARY.to_vec())
.await?;
let code_id = CodeId::generate(demo_messenger::WASM_BINARY);
let gas_info = signer
.rpc()
.calculate_create_gas(None, code_id, vec![], 0, true, None)
.await?;
signer
.calls()
.create_program(code_id, vec![], vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_calculate_handle_gas() -> Result<()> {
let node = dev_node();
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_messenger::WASM_BINARY), &salt);
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
signer
.calls()
.upload_program(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
signer.api().gprog(pid).await.is_ok(),
"Program not exists on chain."
);
let gas_info = signer
.rpc()
.calculate_handle_gas(None, pid, vec![], 0, true, None)
.await?;
signer
.calls()
.send_message(pid, vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_calculate_reply_gas() -> Result<()> {
let node = dev_node();
let alice: [u8; 32] = *alice_account_id().as_ref();
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_waiter::WASM_BINARY), &salt);
let payload = demo_waiter::Command::SendUpTo(alice, 10);
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
signer
.calls()
.upload_program(
demo_waiter::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
signer.api().gprog(pid).await.is_ok(),
"Program not exists on chain"
);
signer
.calls()
.send_message(pid, payload.encode(), 100_000_000_000, 0)
.await?;
let mailbox = signer
.api()
.mailbox(Some(alice_account_id().clone()), 10)
.await?;
assert_eq!(mailbox.len(), 1);
let message_id = mailbox[0].0.id();
let gas_info = signer
.rpc()
.calculate_reply_gas(None, message_id, vec![], 0, true, None)
.await?;
signer
.calls()
.send_reply(message_id, vec![], gas_info.min_limit, 0)
.await?;
Ok(())
}
#[tokio::test]
async fn test_subscribe_program_state_changes() -> Result<()> {
let node = dev_node();
let api = Api::new(node.ws().as_str()).await?;
let mut subscription = api.subscribe_program_state_changes(None).await?;
let signer = api.clone().signer("//Alice", None)?;
let salt = b"state-change".to_vec();
let tx = signer
.calls()
.upload_program(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
let program_id = api
.events_of(&tx)
.await?
.into_iter()
.find_map(|event| {
if let Event::Gear(gear::gear::Event::ProgramChanged { id, .. }) = event {
Some(id)
} else {
None
}
})
.expect("program change event not found");
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 = || -> Cow<str> {
if let Ok(hash) = std::env::var("SUBSTRATE_CLI_GIT_COMMIT_HASH") {
Cow::from(hash.trim().to_owned())
} else {
match Command::new("git")
.args(["rev-parse", "--short=11", "HEAD"])
.output()
{
Ok(o) if o.status.success() => {
let sha = String::from_utf8_lossy(&o.stdout).trim().to_owned();
Cow::from(sha)
}
Ok(o) => {
println!("cargo:warning=Git command failed with status: {}", o.status);
Cow::from("unknown")
}
Err(err) => {
println!("cargo:warning=Failed to execute git command: {err}");
Cow::from("unknown")
}
}
}
};
let git_commit_hash = git_commit_hash();
assert_ne!(git_commit_hash, "unknown");
let node = dev_node();
let api = Api::new(node.ws().as_str()).await?;
let mut finalized_blocks = api.subscribe_finalized_blocks().await?;
let wasm_blob_version_1 = api.runtime_wasm_blob_version(None).await?;
assert!(
wasm_blob_version_1.ends_with(git_commit_hash.as_ref()),
"The WASM blob version {wasm_blob_version_1} does not end with the git commit hash {git_commit_hash}",
);
let block_hash_1 = finalized_blocks.next_events().await?.unwrap().block_hash();
let wasm_blob_version_2 = api.runtime_wasm_blob_version(Some(block_hash_1)).await?;
assert_eq!(wasm_blob_version_1, wasm_blob_version_2);
let block_hash_2 = finalized_blocks.next_events().await?.unwrap().block_hash();
let wasm_blob_version_3 = api.runtime_wasm_blob_version(Some(block_hash_2)).await?;
assert_ne!(block_hash_1, block_hash_2);
assert_eq!(wasm_blob_version_2, wasm_blob_version_3);
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 wasm_blob_version_err = api
.runtime_wasm_blob_version(Some(no_method_block_hash))
.await
.unwrap_err()
.unwrap_subxt_rpc();
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 = dev_node();
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_messenger::WASM_BINARY), &salt);
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
signer
.calls()
.upload_program(
demo_messenger::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
let program = signer.api().gprog(pid).await?;
let rpc = signer.api().backend();
let block_hash = rpc.latest_finalized_block_ref().await?.hash();
let code = signer
.api()
.original_code_storage_at(program.code_id.into_bytes().into(), Some(block_hash))
.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 = dev_node();
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(WASM_BINARY), &salt);
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
let payload = InitConfig::test_sequence().encode();
signer
.calls()
.upload_program(WASM_BINARY.to_vec(), salt, payload, 100_000_000_000, 0)
.await?;
assert!(
signer.api().gprog(pid).await.is_ok(),
"Program not exists on chain."
);
let message_in = FTAction::TotalSupply;
let message_out = FTEvent::TotalSupply(0);
let reply_info = signer
.rpc()
.calculate_reply_for_handle(None, pid, message_in.encode(), 100_000_000_000, 0, None)
.await?;
assert_eq!(
reply_info,
ReplyInfo {
payload: message_out.encode(),
value: 0,
code: ReplyCode::Success(SuccessReplyReason::Manual)
}
);
Ok(())
}
#[tokio::test]
async fn test_calculate_reply_for_handle_does_not_change_state() -> Result<()> {
let node = dev_node();
let salt = vec![];
let pid = ActorId::generate_from_user(CodeId::generate(demo_vec::WASM_BINARY), &salt);
let signer = Api::new(node.ws().as_str())
.await?
.signer("//Alice", None)?;
signer
.calls()
.upload_program(
demo_vec::WASM_BINARY.to_vec(),
salt,
vec![],
100_000_000_000,
0,
)
.await?;
assert!(
signer.api().gprog(pid).await.is_ok(),
"Program not exists on chain."
);
let pid_h256 = H256::from_slice(pid.as_ref());
let initial_state = signer.api().read_state(pid_h256, vec![], None).await?;
let reply_info = signer
.rpc()
.calculate_reply_for_handle(None, pid, 42i32.encode(), 100_000_000_000, 0, None)
.await?;
assert_eq!(
reply_info,
ReplyInfo {
payload: 42i32.encode(),
value: 0,
code: ReplyCode::Success(SuccessReplyReason::Manual)
}
);
let calculated_state = signer.api().read_state(pid_h256, vec![], None).await?;
assert_eq!(initial_state, calculated_state);
signer
.calls()
.send_message(pid, 42i32.encode(), 100_000_000_000, 0)
.await?;
let updated_state = signer.api().read_state(pid_h256, vec![], None).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 signer = Api::new(uri).await?.signer("//Alice", None)?;
let client_block = signer.api().blocks();
let (block_hash, block_number) = match block_hash {
Some(hash) => {
let block = client_block.at(hash).await?;
assert_eq!(hash, block.hash(), "block hash mismatched");
(hash, block.number())
}
None => {
let latest_block = client_block.at_latest().await?;
(latest_block.hash(), latest_block.number())
}
};
let storage = signer.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 += signer.api().gpages(program_id, None).await?.len() as u64;
}
}
Ok((
block_hash,
block_number,
count_program,
count_active_program,
count_memory_page,
))
}