use neo_devpack_solidity::runtime::execution::ExecutionContext;
use neo_devpack_solidity::runtime::RuntimeConfig;
use sha2::{Digest, Sha256};
fn syscall_id(name: &str) -> [u8; 4] {
let d = Sha256::digest(name.as_bytes());
[d[0], d[1], d[2], d[3]]
}
fn push_data(script: &mut Vec<u8>, data: &[u8]) {
assert!(
data.len() <= u8::MAX as usize,
"push_data only supports PUSHDATA1 lengths"
);
script.push(0x0C); script.push(data.len() as u8);
script.extend_from_slice(data);
}
fn push_int(script: &mut Vec<u8>, val: u8) {
match val {
0 => script.push(0x10), 1 => script.push(0x11), 2 => script.push(0x12), 3 => script.push(0x13), 4 => script.push(0x14), 5 => script.push(0x15), n if n <= 16 => script.push(0x10 + n), _ => panic!("push_int only supports 0..16"),
}
}
const POLICY_HASH_LE: [u8; 20] =
*b"\x7b\xc6\x81\xc0\xa1\xf7\x1d\x54\x34\x57\xb6\x8b\xba\x8d\x5f\x9f\xdd\x4e\x5e\xcc";
const ORACLE_HASH_LE: [u8; 20] =
*b"\x58\x87\x17\x11\x7e\x0a\xa8\x10\x72\xaf\xab\x71\xd2\xdd\x89\xfe\x7c\x4b\x92\xfe";
const ROLE_MANAGEMENT_HASH_LE: [u8; 20] =
*b"\xe2\x95\xe3\x91\x54\x4c\x17\x8a\xd9\x4f\x03\xec\x4d\xcd\xff\x78\x53\x4e\xcf\x49";
const LEDGER_HASH_LE: [u8; 20] =
*b"\xbe\xf2\x04\x31\x40\x36\x2a\x77\xc1\x50\x99\xc7\xe6\x4c\x12\xf7\x00\xb6\x65\xda";
const NOTARY_HASH_LE: [u8; 20] =
*b"\x3b\xec\x35\x31\x11\x9b\xba\xd7\x6d\xd0\x44\x92\x0b\x0d\xe6\xc3\x19\x4f\xe1\xc1";
const TREASURY_HASH_LE: [u8; 20] =
*b"\xc1\x3a\x56\xc9\x83\x53\xa7\xea\x6a\x32\x4d\x9a\x83\x5d\x1b\x5b\xf2\x26\x63\x15";
fn build_native_call(hash: &[u8; 20], method: &str, param_opcodes: &[u8]) -> Vec<u8> {
let call_id = syscall_id("System.Contract.Call");
let mut code = Vec::new();
code.extend_from_slice(param_opcodes);
code.push(0x1F); push_data(&mut code, method.as_bytes());
push_data(&mut code, hash);
code.push(0x41); code.extend_from_slice(&call_id);
code.push(0x40); code
}
fn empty_params() -> Vec<u8> {
vec![0x10, 0xC0] }
fn single_int_param(val: u64) -> Vec<u8> {
let mut ops = Vec::new();
let bytes = val.to_le_bytes();
push_data(&mut ops, &bytes);
ops.push(0x11); ops.push(0xC0); ops
}
fn single_bytes_param(data: &[u8]) -> Vec<u8> {
let mut ops = Vec::new();
push_data(&mut ops, data);
ops.push(0x11); ops.push(0xC0); ops
}
fn run_code(code: &[u8]) -> Vec<u8> {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
ctx.initialize(code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
ctx.return_data().to_vec()
}
fn run_on(ctx: &mut ExecutionContext, code: &[u8]) -> Vec<u8> {
ctx.initialize(code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
ctx.return_data().to_vec()
}
#[test]
fn policy_get_fee_per_byte_returns_default() {
let params = empty_params();
let code = build_native_call(&POLICY_HASH_LE, "getFeePerByte", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 1000, "default fee per byte should be 1000");
}
#[test]
fn policy_set_and_get_fee_per_byte() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let set_params = single_int_param(2000);
let set_code = build_native_call(&POLICY_HASH_LE, "setFeePerByte", &set_params);
run_on(&mut ctx, &set_code);
let get_params = empty_params();
let get_code = build_native_call(&POLICY_HASH_LE, "getFeePerByte", &get_params);
let result = run_on(&mut ctx, &get_code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 2000, "fee per byte should be 2000 after set");
}
#[test]
fn policy_get_exec_fee_factor_returns_default() {
let params = empty_params();
let code = build_native_call(&POLICY_HASH_LE, "getExecFeeFactor", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 30, "default exec fee factor should be 30");
}
#[test]
fn policy_get_storage_price_returns_default() {
let params = empty_params();
let code = build_native_call(&POLICY_HASH_LE, "getStoragePrice", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 100_000, "default storage price should be 100000");
}
#[test]
fn policy_block_and_unblock_account() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let account = [0xAA; 20];
let block_params = single_bytes_param(&account);
let block_code = build_native_call(&POLICY_HASH_LE, "blockAccount", &block_params);
let result = run_on(&mut ctx, &block_code);
assert_eq!(result, vec![1], "blockAccount should return true");
let check_params = single_bytes_param(&account);
let check_code = build_native_call(&POLICY_HASH_LE, "isBlocked", &check_params);
let result = run_on(&mut ctx, &check_code);
assert_eq!(result, vec![1], "isBlocked should return true");
let unblock_params = single_bytes_param(&account);
let unblock_code = build_native_call(&POLICY_HASH_LE, "unblockAccount", &unblock_params);
let result = run_on(&mut ctx, &unblock_code);
assert_eq!(result, vec![1], "unblockAccount should return true");
let result = run_on(&mut ctx, &check_code);
assert_eq!(
result,
vec![0],
"isBlocked should return false after unblock"
);
}
#[test]
fn oracle_get_price_returns_default() {
let params = empty_params();
let code = build_native_call(&ORACLE_HASH_LE, "getPrice", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 50_000_000, "default oracle price should be 50000000");
}
#[test]
fn oracle_set_and_get_price() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let set_params = single_int_param(100_000_000);
let set_code = build_native_call(&ORACLE_HASH_LE, "setPrice", &set_params);
run_on(&mut ctx, &set_code);
let get_params = empty_params();
let get_code = build_native_call(&ORACLE_HASH_LE, "getPrice", &get_params);
let result = run_on(&mut ctx, &get_code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(
val, 100_000_000,
"oracle price should be 100000000 after set"
);
}
#[test]
fn oracle_request_returns_incrementing_ids() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let call_id = syscall_id("System.Contract.Call");
for expected_id in 0u32..3 {
let mut code = Vec::new();
push_data(&mut code, &50_000_000u64.to_le_bytes()); push_data(&mut code, &[]); push_data(&mut code, b"callback");
push_data(&mut code, &[0u8; 20]); push_data(&mut code, b"$.price");
push_data(&mut code, b"https://example.com/api");
push_int(&mut code, 6); code.push(0xC0); code.push(0x1F); push_data(&mut code, b"request");
push_data(&mut code, &ORACLE_HASH_LE);
code.push(0x41);
code.extend_from_slice(&call_id);
code.push(0x40);
let result = run_on(&mut ctx, &code);
let id = u32::from_le_bytes({
let mut buf = [0u8; 4];
for (i, b) in result.iter().take(4).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(id, expected_id, "oracle request id should increment");
}
}
#[test]
fn role_management_designate_and_query() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let call_id = syscall_id("System.Contract.Call");
let key1 = [0x01u8; 33];
let key2 = [0x02u8; 33];
let mut code = Vec::new();
push_data(&mut code, &key2);
push_data(&mut code, &key1);
push_int(&mut code, 2);
code.push(0xC0); push_int(&mut code, 4);
push_int(&mut code, 2);
code.push(0xC0); code.push(0x1F); push_data(&mut code, b"designateAsRole");
push_data(&mut code, &ROLE_MANAGEMENT_HASH_LE);
code.push(0x41);
code.extend_from_slice(&call_id);
code.push(0x40);
run_on(&mut ctx, &code);
let query_params = single_int_param(4);
let query_code = build_native_call(
&ROLE_MANAGEMENT_HASH_LE,
"getDesignatedByRole",
&query_params,
);
let result = run_on(&mut ctx, &query_code);
assert!(
!result.is_empty(),
"getDesignatedByRole should return non-empty result"
);
}
#[test]
fn role_management_empty_role_returns_empty() {
let params = single_int_param(8); let code = build_native_call(&ROLE_MANAGEMENT_HASH_LE, "getDesignatedByRole", ¶ms);
let result = run_code(&code);
let result_str = String::from_utf8_lossy(&result);
assert!(
result.is_empty() || result_str.contains("[]"),
"undesignated role should return empty array, got {result_str:?}"
);
}
#[test]
fn ledger_current_index_returns_default_height() {
let params = empty_params();
let code = build_native_call(&LEDGER_HASH_LE, "currentIndex", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert!(
val < 1_000_000,
"currentIndex should be a reasonable height, got {val}"
);
}
#[test]
fn ledger_current_hash_returns_32_bytes() {
let params = empty_params();
let code = build_native_call(&LEDGER_HASH_LE, "currentHash", ¶ms);
let result = run_code(&code);
assert_eq!(result.len(), 32, "currentHash should return 32 bytes");
}
#[test]
fn ledger_get_block_returns_synthetic_block() {
let params = single_int_param(0);
let code = build_native_call(&LEDGER_HASH_LE, "getBlock", ¶ms);
let result = run_code(&code);
assert!(
!result.is_empty(),
"getBlock(0) should return a non-empty synthetic block"
);
}
#[test]
fn ledger_get_block_beyond_height_returns_null() {
let params = single_int_param(999_999_999);
let code = build_native_call(&LEDGER_HASH_LE, "getBlock", ¶ms);
let result = run_code(&code);
assert!(
result.is_empty(),
"getBlock beyond height should return null/empty, got {} bytes",
result.len()
);
}
#[test]
fn ledger_get_transaction_vm_state_returns_halt() {
let params = empty_params();
let code = build_native_call(&LEDGER_HASH_LE, "getTransactionVMState", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 1, "getTransactionVMState should return HALT (1)");
}
#[test]
fn notary_verify_returns_true() {
let params = empty_params();
let code = build_native_call(&NOTARY_HASH_LE, "verify", ¶ms);
let result = run_code(&code);
assert_eq!(result, vec![1], "Notary.verify should return true");
}
#[test]
fn notary_balance_of_unknown_account_returns_zero() {
let account = [0xBB; 20];
let params = single_bytes_param(&account);
let code = build_native_call(&NOTARY_HASH_LE, "balanceOf", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 0, "balanceOf unknown account should be 0");
}
#[test]
fn notary_deposit_and_query_lifecycle() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let call_id = syscall_id("System.Contract.Call");
let account = [0xCC; 20];
let mut deposit_code = Vec::new();
push_data(&mut deposit_code, &5000u64.to_le_bytes());
push_data(&mut deposit_code, &1000u64.to_le_bytes());
push_data(&mut deposit_code, &account);
push_int(&mut deposit_code, 3);
deposit_code.push(0xC0); deposit_code.push(0x1F);
push_data(&mut deposit_code, b"onNEP17Payment");
push_data(&mut deposit_code, &NOTARY_HASH_LE);
deposit_code.push(0x41);
deposit_code.extend_from_slice(&call_id);
deposit_code.push(0x40);
run_on(&mut ctx, &deposit_code);
let balance_params = single_bytes_param(&account);
let balance_code = build_native_call(&NOTARY_HASH_LE, "balanceOf", &balance_params);
let result = run_on(&mut ctx, &balance_code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 1000, "balanceOf should be 1000 after deposit");
let exp_params = single_bytes_param(&account);
let exp_code = build_native_call(&NOTARY_HASH_LE, "expirationOf", &exp_params);
let result = run_on(&mut ctx, &exp_code);
let till = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(till, 5000, "expirationOf should be 5000");
let withdraw_params = single_bytes_param(&account);
let withdraw_code = build_native_call(&NOTARY_HASH_LE, "withdraw", &withdraw_params);
let result = run_on(&mut ctx, &withdraw_code);
assert_eq!(result, vec![1], "withdraw should return true");
let result = run_on(&mut ctx, &balance_code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 0, "balanceOf should be 0 after withdraw");
}
#[test]
fn notary_get_max_not_valid_before_delta_default() {
let params = empty_params();
let code = build_native_call(&NOTARY_HASH_LE, "getMaxNotValidBeforeDelta", ¶ms);
let result = run_code(&code);
let val = u64::from_le_bytes({
let mut buf = [0u8; 8];
for (i, b) in result.iter().take(8).enumerate() {
buf[i] = *b;
}
buf
});
assert_eq!(val, 140, "default maxNotValidBeforeDelta should be 140");
}
#[test]
fn treasury_verify_returns_true() {
let params = empty_params();
let code = build_native_call(&TREASURY_HASH_LE, "verify", ¶ms);
let result = run_code(&code);
assert_eq!(result, vec![1], "Treasury.verify should return true");
}
#[test]
fn treasury_nep17_payment_tracks_balance() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let call_id = syscall_id("System.Contract.Call");
let depositor = [0xDD; 20];
let mut code = Vec::new();
push_data(&mut code, &[]); push_data(&mut code, &500u64.to_le_bytes());
push_data(&mut code, &depositor);
push_int(&mut code, 3);
code.push(0xC0); code.push(0x1F);
push_data(&mut code, b"onNEP17Payment");
push_data(&mut code, &TREASURY_HASH_LE);
code.push(0x41);
code.extend_from_slice(&call_id);
code.push(0x40);
run_on(&mut ctx, &code);
}
#[test]
fn treasury_nep11_payment_tracks_tokens() {
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("ctx");
let call_id = syscall_id("System.Contract.Call");
let depositor = [0xEE; 20];
let token_id = b"token-001";
let mut code = Vec::new();
push_data(&mut code, token_id);
push_data(&mut code, &1u64.to_le_bytes());
push_data(&mut code, &depositor);
push_int(&mut code, 3);
code.push(0xC0); code.push(0x1F);
push_data(&mut code, b"onNEP11Payment");
push_data(&mut code, &TREASURY_HASH_LE);
code.push(0x41);
code.extend_from_slice(&call_id);
code.push(0x40);
run_on(&mut ctx, &code);
}