#![allow(dead_code)]
use ccl::Bridge;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
pub const DEVKIT_URL: &str = "http://localhost:10000/local-cluster/api";
pub const INTENT_MNEMONIC: &str =
"test walk nut penalty hip pave soap entry language right filter choice";
pub const INTENT_SENDER: &str = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp";
pub const MINT_RECEIVER: &str = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz";
pub const SCRIPT_ADDR: &str = "addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w";
pub const SCRIPT_DATUM_HASH: &str =
"9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b";
pub const SCRIPT_TX_HASH: &str =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
pub const GOV_ACTION_PLACEHOLDER: &str =
"12745f09b138d4d0a11a560b4591ebb830cf12336347606d2edbbf1893d395c6";
pub const POOL_PLACEHOLDER: &str = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
pub const ACCOUNT_POOL_ID: &str = "pool1xtrj35uxrctye2egew8sqezgzwwg796ql7uw02572gedcpgmwck";
pub fn plutus_exec_units() -> Value {
json!([{"mem": 2000000, "steps": 500000000}])
}
pub fn devkit_available() -> bool {
ureq::get(&format!("{}/admin/devnet", DEVKIT_URL))
.timeout(Duration::from_secs(3))
.call()
.map(|r| r.status() == 200)
.unwrap_or(false)
}
pub fn devkit_reset() {
for attempt in 1..=3 {
let _ = ureq::post(&format!("{}/admin/devnet/reset", DEVKIT_URL))
.timeout(Duration::from_secs(60))
.call();
if devkit_wait_healthy(Duration::from_secs(60)) {
return;
}
println!(
"devkit reset attempt {}/3: devnet still down, re-posting reset",
attempt
);
}
println!("devkit reset: devnet did not serve chain data after 3 attempts");
}
fn devkit_wait_healthy(budget: Duration) -> bool {
let deadline = std::time::Instant::now() + budget;
while std::time::Instant::now() < deadline {
thread::sleep(Duration::from_secs(3));
let chain_data_ok = ureq::get(&format!("{}/epochs/parameters", DEVKIT_URL))
.timeout(Duration::from_secs(5))
.call()
.map(|r| r.status() == 200)
.unwrap_or(false);
if chain_data_ok && devkit_submit_path_alive() {
return true;
}
}
false
}
fn devkit_submit_path_alive() -> bool {
match ureq::post(&format!("{}/tx/submit", DEVKIT_URL))
.timeout(Duration::from_secs(5))
.set("Content-Type", "application/cbor")
.send_bytes(&[0x00])
{
Ok(_) => true, Err(ureq::Error::Status(_, resp)) => !resp
.into_string()
.unwrap_or_default()
.contains("Connection refused"),
Err(_) => false,
}
}
pub fn devkit_topup(address: &str, ada_amount: u64) {
let body = json!({"address": address, "adaAmount": ada_amount}).to_string();
for attempt in 1..=8 {
let resp = ureq::post(&format!("{}/addresses/topup", DEVKIT_URL))
.timeout(Duration::from_secs(30))
.set("Content-Type", "application/json")
.send_string(&body);
match resp {
Ok(r) => {
let text = r.into_string().unwrap_or_default();
if !text.contains("\"status\":false") {
return;
}
}
Err(e) if attempt == 8 => panic!("topup failed after retries: {}", e),
Err(_) => {}
}
thread::sleep(Duration::from_secs(4));
}
panic!("topup failed after retries");
}
pub fn devkit_get_utxos(address: &str) -> Value {
let resp = ureq::get(&format!("{}/addresses/{}/utxos", DEVKIT_URL, address))
.timeout(Duration::from_secs(30))
.call()
.expect("Failed to get utxos");
resp.into_json::<Value>().expect("Invalid utxo JSON")
}
pub fn devkit_get_protocol_params() -> Value {
let resp = ureq::get(&format!("{}/epochs/parameters", DEVKIT_URL))
.timeout(Duration::from_secs(30))
.call()
.expect("Failed to get protocol params");
resp.into_json::<Value>().expect("Invalid PP JSON")
}
pub fn devnet_pp() -> Value {
let mut pp = devkit_get_protocol_params();
pp["drep_deposit"] = json!("500000000");
pp["gov_action_deposit"] = json!("1000000000");
pp["pool_deposit"] = json!("500000000");
pp
}
pub fn devkit_submit_tx(tx_cbor_hex: &str) -> String {
devkit_try_submit(tx_cbor_hex).expect("Failed to submit tx")
}
pub fn devkit_try_submit(tx_cbor_hex: &str) -> Result<String, String> {
let mut last_err = String::new();
for _ in 0..8 {
match devkit_try_submit_once(tx_cbor_hex) {
Err(e) if e.contains("Connection refused") => last_err = e,
other => return other,
}
thread::sleep(Duration::from_secs(4));
}
Err(last_err)
}
fn devkit_try_submit_once(tx_cbor_hex: &str) -> Result<String, String> {
let tx_bytes = hex_decode(tx_cbor_hex).map_err(|e| e.to_string())?;
match ureq::post(&format!("{}/tx/submit", DEVKIT_URL))
.timeout(Duration::from_secs(30))
.set("Content-Type", "application/cbor")
.send_bytes(&tx_bytes)
{
Ok(resp) => Ok(resp
.into_string()
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string()),
Err(ureq::Error::Status(code, resp)) => Err(format!(
"submit failed ({}): {}",
code,
resp.into_string().unwrap_or_default()
)),
Err(e) => Err(e.to_string()),
}
}
pub fn parse_expected_treasury(submit_err: &str) -> Option<String> {
let idx = submit_err.find("expected: Coin ")?;
let rest = &submit_err[idx + "expected: Coin ".len()..];
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
None
} else {
Some(digits)
}
}
pub fn wait_for_block() {
thread::sleep(Duration::from_secs(3));
}
pub fn skip_if_no_devkit() -> bool {
if !devkit_available() {
eprintln!("SKIP: Yaci DevKit not available on port 10000");
return true;
}
false
}
pub fn get_testnet_account(bridge: &Bridge) -> (String, String, String) {
let result = bridge
.account()
.create(ccl::Network::Testnet)
.expect("create account");
let json: Value = serde_json::from_str(&result).expect("parse account");
let addr = json["base_address"].as_str().unwrap().to_string();
let mnemonic = json["mnemonic"].as_str().unwrap().to_string();
let stake = json["stake_address"].as_str().unwrap_or("").to_string();
(addr, mnemonic, stake)
}
pub fn fund_sender(bridge: &Bridge, ada: u64) -> (String, String) {
let (addr, mnemonic, _) = get_testnet_account(bridge);
devkit_topup(&addr, ada);
wait_for_block();
(addr, mnemonic)
}
pub fn total_lovelace(utxos: &Value) -> u64 {
let arr = match utxos.as_array() {
Some(a) => a,
None => return 0,
};
let mut total: u64 = 0;
for u in arr {
if let Some(amounts) = u["amount"].as_array() {
for a in amounts {
if a["unit"].as_str() == Some("lovelace") {
if let Some(q) = a["quantity"].as_str() {
total += q.parse::<u64>().unwrap_or(0);
} else if let Some(q) = a["quantity"].as_u64() {
total += q;
} else if let Some(q) = a["quantity"].as_f64() {
total += q as u64;
}
}
}
}
}
total
}
pub fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-fixtures/quicktx-intents")
}
pub fn read_fixture(rel: &str) -> String {
let path = fixtures_dir().join(rel);
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read fixture {}: {}", rel, e))
}
pub fn sign_submit(
bridge: &Bridge,
yaml: &str,
utxos: &Value,
pp: &Value,
exec_units: Option<&Value>,
keys: &[&str],
) -> String {
let result = bridge
.quicktx()
.build(yaml, utxos, pp, exec_units)
.expect("build");
let signed = bridge
.account()
.sign_tx_with_keys(INTENT_MNEMONIC, ccl::Network::Testnet, 0, 0, &result.tx_cbor, keys)
.expect("sign");
match devkit_try_submit(&signed) {
Ok(hash) => hash,
Err(e) => panic!("submit: {}", e),
}
}
pub fn build_sign_submit(
bridge: &Bridge,
fixture: &str,
exec_units: Option<&Value>,
keys: &[&str],
) -> String {
devkit_reset();
wait_for_block();
devkit_topup(INTENT_SENDER, 6000);
wait_for_block();
let utxos = devkit_get_utxos(INTENT_SENDER);
let pp = devnet_pp();
sign_submit(bridge, &read_fixture(fixture), &utxos, &pp, exec_units, keys)
}
pub fn setup_then_submit(
bridge: &Bridge,
setup_fixture: &str,
setup_keys: &[&str],
fixture: &str,
keys: &[&str],
) {
devkit_reset();
wait_for_block();
devkit_topup(INTENT_SENDER, 6000);
wait_for_block();
let pp = devnet_pp();
let u = devkit_get_utxos(INTENT_SENDER);
sign_submit(bridge, &read_fixture(setup_fixture), &u, &pp, None, setup_keys);
wait_for_block();
let u2 = devkit_get_utxos(INTENT_SENDER);
sign_submit(bridge, &read_fixture(fixture), &u2, &pp, None, keys);
}
pub fn assert_minted_asset_at(address: &str) {
wait_for_block();
let utxos = devkit_get_utxos(address);
if let Some(arr) = utxos.as_array() {
for u in arr {
if let Some(amounts) = u["amount"].as_array() {
for a in amounts {
if let Some(unit) = a["unit"].as_str() {
if !unit.is_empty() && unit != "lovelace" {
return; }
}
}
}
}
}
panic!("expected a minted asset at {}, found none", address);
}
pub fn assert_utxo_consumed(address: &str, tx_hash: &str) {
wait_for_block();
let utxos = devkit_get_utxos(address);
if let Some(arr) = utxos.as_array() {
for u in arr {
if u["tx_hash"].as_str() == Some(tx_hash) {
panic!("UTXO {} at {} was not consumed", tx_hash, address);
}
}
}
}
pub fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd length".to_string());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
pub fn devkit_current_epoch() -> i64 {
let pp = devkit_get_protocol_params();
if let Some(e) = pp["epoch"].as_i64() {
return e;
}
if let Some(s) = pp["epoch"].as_str() {
if let Ok(e) = s.parse() {
return e;
}
}
let latest: Value = ureq::get(&format!("{}/epochs/latest", DEVKIT_URL))
.timeout(Duration::from_secs(30))
.call()
.expect("get /epochs/latest")
.into_json()
.expect("parse /epochs/latest");
latest["epoch"].as_i64().expect("epoch in /epochs/latest")
}
pub const INTENT_SENDER2: &str = "addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0";
pub fn balance_at(address: &str) -> u64 {
total_lovelace(&devkit_get_utxos(address))
}
pub fn pp_lovelace(pp: &Value, key: &str) -> u64 {
if let Some(n) = pp[key].as_u64() {
return n;
}
pp[key]
.as_str()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| panic!("no lovelace value for pp[{}] ({:?})", key, pp[key]))
}
pub fn sign_submit_fee(
bridge: &Bridge,
yaml: &str,
utxos: &Value,
pp: &Value,
exec_units: Option<&Value>,
keys: &[&str],
) -> u64 {
let result = bridge
.quicktx()
.build(yaml, utxos, pp, exec_units)
.expect("build");
let signed = bridge
.account()
.sign_tx_with_keys(INTENT_MNEMONIC, ccl::Network::Testnet, 0, 0, &result.tx_cbor, keys)
.expect("sign");
match devkit_try_submit(&signed) {
Ok(_) => result.fee.parse().expect("parse fee"),
Err(e) => panic!("submit: {}", e),
}
}
pub fn reset_and_fund(ada: u64) -> Value {
devkit_reset();
wait_for_block();
devkit_topup(INTENT_SENDER, ada);
wait_for_block();
devnet_pp()
}