seedelf_cli/
koios.rs

1use crate::address;
2use crate::constants::ADA_HANDLE_POLICY_ID;
3use crate::register::Register;
4use hex;
5use reqwest::{Client, Error, Response};
6use serde::Deserialize;
7use serde_json::Value;
8
9/// Represents the latest blockchain tip information from Koios.
10#[derive(Deserialize, Debug)]
11pub struct BlockchainTip {
12    pub hash: String,
13    pub epoch_no: u64,
14    pub abs_slot: u64,
15    pub epoch_slot: u64,
16    pub block_no: u64,
17    pub block_time: u64,
18}
19
20/// Fetches the latest blockchain tip from the Koios API.
21///
22/// Queries the Koios API to retrieve the most recent block's details
23/// for the specified network.
24///
25/// # Arguments
26///
27/// * `network_flag` - A boolean flag indicating the network:
28///     - `true` for Preprod/Testnet.
29///     - `false` for Mainnet.
30///
31/// # Returns
32///
33/// * `Ok(Vec<BlockchainTip>)` - A vector containing the latest blockchain tip data.
34/// * `Err(Error)` - If the API request or JSON parsing fails.
35pub async fn tip(network_flag: bool) -> Result<Vec<BlockchainTip>, Error> {
36    let network: &str = if network_flag { "preprod" } else { "api" };
37    let url: String = format!("https://{}.koios.rest/api/v1/tip", network);
38
39    // Make the GET request and parse the JSON response
40    let response: Vec<BlockchainTip> = reqwest::get(&url)
41        .await?
42        .json::<Vec<BlockchainTip>>()
43        .await?;
44
45    Ok(response)
46}
47
48#[derive(Debug, Deserialize, Clone, Default)]
49pub struct Asset {
50    pub decimals: u8,
51    pub quantity: String,
52    pub policy_id: String,
53    pub asset_name: String,
54    pub fingerprint: String,
55}
56
57#[derive(Debug, Deserialize, Clone, Default)]
58pub struct InlineDatum {
59    pub bytes: String,
60    pub value: Value, // Flexible for arbitrary JSON
61}
62
63#[derive(Debug, Deserialize, Clone, Default)]
64pub struct UtxoResponse {
65    pub tx_hash: String,
66    pub tx_index: u64,
67    pub address: String,
68    pub value: String,
69    pub stake_address: Option<String>,
70    pub payment_cred: String,
71    pub epoch_no: u64,
72    pub block_height: u64,
73    pub block_time: u64,
74    pub datum_hash: Option<String>,
75    pub inline_datum: Option<InlineDatum>,
76    pub reference_script: Option<Value>, // Flexible for arbitrary scripts
77    pub asset_list: Option<Vec<Asset>>,
78    pub is_spent: bool,
79}
80
81/// Fetches the UTXOs associated with a given payment credential from the Koios API.
82///
83/// This function collects all UTXOs (Unspent Transaction Outputs) related to the specified
84/// payment credential by paginating through the Koios API results.
85///
86/// # Arguments
87///
88/// * `payment_credential` - A string slice representing the payment credential to search for.
89/// * `network_flag` - A boolean flag specifying the network:
90///     - `true` for Preprod/Testnet.
91///     - `false` for Mainnet.
92///
93/// # Returns
94///
95/// * `Ok(Vec<UtxoResponse>)` - A vector containing all UTXOs associated with the payment credential.
96/// * `Err(Error)` - If the API request or JSON parsing fails.
97///
98/// # Behavior
99///
100/// The function paginates through the UTXO results, starting with an offset of zero
101/// and incrementing by 1000 until no further results are returned.
102pub async fn credential_utxos(
103    payment_credential: &str,
104    network_flag: bool,
105) -> Result<Vec<UtxoResponse>, Error> {
106    let network: &str = if network_flag { "preprod" } else { "api" };
107    // this is searching the wallet contract. We have to collect the entire utxo set to search it.
108    let url: String = format!("https://{}.koios.rest/api/v1/credential_utxos", network);
109    let client: Client = reqwest::Client::new();
110
111    // Prepare the request payload
112    let payload: Value = serde_json::json!({
113        "_payment_credentials": [payment_credential],
114        "_extended": true
115    });
116
117    let mut all_utxos: Vec<UtxoResponse> = Vec::new();
118    let mut offset: i32 = 0;
119
120    loop {
121        // Make the POST request
122        let response: Response = client
123            .post(url.clone())
124            .header("accept", "application/json")
125            .header("content-type", "application/json")
126            .query(&[("offset", offset.to_string())])
127            .json(&payload)
128            .send()
129            .await?;
130
131        let mut utxos: Vec<UtxoResponse> = response.json().await?;
132        // Break the loop if no more results
133        if utxos.is_empty() {
134            break;
135        }
136
137        // Append the retrieved UTXOs to the main list
138        all_utxos.append(&mut utxos);
139
140        // Increment the offset by 1000 (page size)
141        offset += 1000;
142    }
143
144    Ok(all_utxos)
145}
146
147/// Fetches the UTXOs associated with a specific address from the Koios API.
148///
149/// This function retrieves up to 1000 UTXOs for the given address. The `_extended` flag
150/// is enabled in the payload to include detailed UTXO information.
151///
152/// # Arguments
153///
154/// * `address` - A string slice representing the Cardano address to query.
155/// * `network_flag` - A boolean flag specifying the network:
156///     - `true` for Preprod/Testnet.
157///     - `false` for Mainnet.
158///
159/// # Returns
160///
161/// * `Ok(Vec<UtxoResponse>)` - A vector containing the UTXOs associated with the given address.
162/// * `Err(Error)` - If the API request or JSON parsing fails.
163///
164/// # Notes
165///
166/// The function assumes a maximum of 1000 UTXOs per address, as per CIP-30 wallets.
167/// If an address exceeds this limit, the wallet is likely mismanaged.
168pub async fn address_utxos(address: &str, network_flag: bool) -> Result<Vec<UtxoResponse>, Error> {
169    let network: &str = if network_flag { "preprod" } else { "api" };
170    // this will limit to 1000 utxos which is ok for an address as that is a cip30 wallet
171    // if you have 1000 utxos in that wallets that cannot pay for anything then something
172    // is wrong in that wallet
173    let url: String = format!("https://{}.koios.rest/api/v1/address_utxos", network);
174    let client: Client = reqwest::Client::new();
175
176    // Prepare the request payload
177    let payload: Value = serde_json::json!({
178        "_addresses": [address],
179        "_extended": true
180    });
181
182    // Make the POST request
183    let response: Response = client
184        .post(url)
185        .header("accept", "application/json")
186        .header("content-type", "application/json")
187        .json(&payload)
188        .send()
189        .await?;
190
191    let utxos: Vec<UtxoResponse> = response.json().await?;
192
193    Ok(utxos)
194}
195
196/// Extracts byte values from an `InlineDatum` with detailed logging.
197///
198/// This function attempts to extract two byte strings from the `fields` array inside the `InlineDatum`.
199/// If the extraction fails due to missing keys, incorrect types, or insufficient elements, an error
200/// message is logged to standard error.
201///
202/// # Arguments
203///
204/// * `inline_datum` - An optional reference to an `InlineDatum`. The `InlineDatum` is expected to contain
205///   a `value` key, which maps to an object with a `fields` array of at least two elements.
206///
207/// # Returns
208///
209/// * `Some(Register)` - A `Register` instance containing the two extracted byte strings.
210/// * `None` - If the extraction fails or `inline_datum` is `None`.
211///
212/// # Behavior
213///
214/// Logs errors to `stderr` using `eprintln!` when:
215/// - `inline_datum` is `None`.
216/// - The `value` key is missing or is not an object.
217/// - The `fields` key is missing or is not an array.
218/// - The `fields` array has fewer than two elements.
219pub fn extract_bytes_with_logging(inline_datum: &Option<InlineDatum>) -> Option<Register> {
220    if let Some(datum) = inline_datum {
221        if let Value::Object(ref value_map) = datum.value {
222            if let Some(Value::Array(fields)) = value_map.get("fields") {
223                if let (Some(first), Some(second)) = (fields.first(), fields.get(1)) {
224                    let first_bytes: String = first.get("bytes")?.as_str()?.to_string();
225                    let second_bytes: String = second.get("bytes")?.as_str()?.to_string();
226                    return Some(Register::new(first_bytes, second_bytes));
227                } else {
228                    eprintln!("Fields array has fewer than two elements.");
229                }
230            } else {
231                eprintln!("`fields` key is missing or not an array.");
232            }
233        } else {
234            eprintln!("`value` is not an object.");
235        }
236    } else {
237        eprintln!("Inline datum is None.");
238    }
239    None
240}
241
242/// Checks if a target policy ID exists in the asset list.
243///
244/// This function checks whether a specified `target_policy_id` exists
245/// within the provided `asset_list`. If the `asset_list` is `None`, the function
246/// returns `false`.
247///
248/// # Arguments
249///
250/// * `asset_list` - An optional reference to a vector of `Asset` items.
251/// * `target_policy_id` - A string slice representing the policy ID to search for.
252///
253/// # Returns
254///
255/// * `true` - If the target policy ID exists in the asset list.
256/// * `false` - If the target policy ID does not exist or the asset list is `None`.
257///
258/// # Behavior
259///
260/// - Safely handles `None` values for `asset_list` using `map_or`.
261/// - Uses `iter().any()` to efficiently search for a matching policy ID.
262pub fn contains_policy_id(asset_list: &Option<Vec<Asset>>, target_policy_id: &str) -> bool {
263    asset_list
264        .as_ref() // Convert Option<Vec<Asset>> to Option<&Vec<Asset>>
265        .is_some_and(|assets| {
266            assets
267                .iter()
268                .any(|asset| asset.policy_id == target_policy_id)
269        })
270}
271
272/// Evaluates a transaction using the Koios API.
273///
274/// This function sends a CBOR-encoded transaction to the Koios API for evaluation.
275/// The API uses Ogmios to validate and evaluate the transaction. The target network
276/// is determined by the `network_flag`.
277///
278/// # Arguments
279///
280/// * `tx_cbor` - A string containing the CBOR-encoded transaction.
281/// * `network_flag` - A boolean flag specifying the network:
282///     - `true` for Preprod/Testnet.
283///     - `false` for Mainnet.
284///
285/// # Returns
286///
287/// * `Ok(Value)` - A JSON response containing the evaluation result.
288/// * `Err(Error)` - If the API request fails or the JSON parsing fails.
289///
290/// # Behavior
291///
292/// The function constructs a JSON-RPC request payload and sends a POST request
293/// to the Koios Ogmios endpoint.
294pub async fn evaluate_transaction(tx_cbor: String, network_flag: bool) -> Result<Value, Error> {
295    let network: &str = if network_flag { "preprod" } else { "api" };
296
297    // Prepare the request payload
298    let payload: Value = serde_json::json!({
299        "jsonrpc": "2.0",
300        "method": "evaluateTransaction",
301        "params": {
302            "transaction": {
303                "cbor": tx_cbor
304            }
305        }
306    });
307
308    let url: String = format!("https://{}.koios.rest/api/v1/ogmios", network);
309    let client: Client = reqwest::Client::new();
310
311    // Make the POST request
312    let response: Response = client
313        .post(url)
314        .header("accept", "application/json")
315        .header("content-type", "application/json")
316        .json(&payload)
317        .send()
318        .await?;
319
320    response.json().await
321}
322
323/// Submits a transaction body to witness collateral using a specified API endpoint.
324///
325/// This function sends a CBOR-encoded transaction body to the collateral witnessing endpoint.
326/// The target network (Preprod or Mainnet) is determined by the `network_flag`.
327///
328/// # Arguments
329///
330/// * `tx_cbor` - A string containing the CBOR-encoded transaction body.
331/// * `network_flag` - A boolean flag specifying the network:
332///     - `true` for Preprod.
333///     - `false` for Mainnet.
334///
335/// # Returns
336///
337/// * `Ok(Value)` - A JSON response from the API, containing collateral witnessing results.
338/// * `Err(Error)` - If the API request fails or the response JSON parsing fails.
339///
340/// # Behavior
341///
342/// The function constructs a JSON payload containing the transaction body and sends
343/// it to the specified API endpoint using a POST request.
344pub async fn witness_collateral(tx_cbor: String, network_flag: bool) -> Result<Value, Error> {
345    let network: &str = if network_flag { "preprod" } else { "mainnet" };
346    let url: String = format!("https://www.giveme.my/{}/collateral/", network);
347    let client: Client = reqwest::Client::new();
348
349    let payload: Value = serde_json::json!({
350        "tx_body": tx_cbor,
351    });
352
353    // Make the POST request
354    let response: Response = client
355        .post(url)
356        .header("content-type", "application/json")
357        .json(&payload)
358        .send()
359        .await?;
360
361    response.json().await
362}
363
364/// Submits a CBOR-encoded transaction to the Koios API.
365///
366/// This function decodes the provided CBOR-encoded transaction from a hex string into binary
367/// data and sends it to the Koios API for submission. The target network (Preprod or Mainnet)
368/// is determined by the `network_flag`.
369///
370/// # Arguments
371///
372/// * `tx_cbor` - A string containing the hex-encoded CBOR transaction.
373/// * `network_flag` - A boolean flag specifying the network:
374///     - `true` for Preprod.
375///     - `false` for Mainnet.
376///
377/// # Returns
378///
379/// * `Ok(Value)` - A JSON response from the API indicating the result of the transaction submission.
380/// * `Err(Error)` - If the API request fails or the response JSON parsing fails.
381///
382/// # Behavior
383///
384/// - Decodes the transaction CBOR hex string into raw binary data.
385/// - Sends the binary data as the body of a POST request with `Content-Type: application/cbor`.
386pub async fn submit_tx(tx_cbor: String, network_flag: bool) -> Result<Value, Error> {
387    let network: &str = if network_flag { "preprod" } else { "api" };
388    let url: String = format!("https://{}.koios.rest/api/v1/submittx", network);
389    let client: Client = reqwest::Client::new();
390
391    // Decode the hex string into binary data
392    let data: Vec<u8> = hex::decode(&tx_cbor).unwrap();
393
394    let response: Response = client
395        .post(url)
396        .header("Content-Type", "application/cbor")
397        .body(data) // Send the raw binary data as the body of the request
398        .send()
399        .await?;
400
401    response.json().await
402}
403
404pub async fn ada_handle_address(
405    asset_name: String,
406    network_flag: bool,
407    cip68_flag: bool,
408    variant: u64,
409) -> Result<String, String> {
410    let network: &str = if network_flag { "preprod" } else { "api" };
411    let token_name: String = if cip68_flag {
412        "000de140".to_string() + &hex::encode(asset_name.clone())
413    } else {
414        hex::encode(asset_name.clone())
415    };
416    let url: String = format!(
417        "https://{}.koios.rest/api/v1/asset_nft_address?_asset_policy={}&_asset_name={}",
418        network,
419        ADA_HANDLE_POLICY_ID,
420        // some have the 222 prefix
421        token_name
422    );
423    let client: Client = reqwest::Client::new();
424
425    let response: Response = match client
426        .get(url)
427        .header("Content-Type", "application/json")
428        .send()
429        .await
430    {
431        Ok(resp) => resp,
432        Err(err) => return Err(format!("HTTP request failed: {}", err)),
433    };
434
435    let outcome: Value = response.json().await.unwrap();
436    let vec_outcome = serde_json::from_value::<Vec<serde_json::Value>>(outcome)
437        .expect("Failed to parse outcome as Vec<Value>");
438
439    // Borrow from the longer-lived variable
440    let payment_address = match vec_outcome
441        .first()
442        .and_then(|obj| obj.get("payment_address"))
443        .and_then(|val| val.as_str())
444    {
445        Some(address) => address,
446        None => {
447            if cip68_flag {
448                return Err("Payment address not found".to_string());
449            } else {
450                return Box::pin(ada_handle_address(
451                    asset_name,
452                    network_flag,
453                    !cip68_flag,
454                    variant,
455                ))
456                .await;
457            }
458        }
459    };
460
461    let wallet_addr: String = address::wallet_contract(network_flag, variant)
462        .to_bech32()
463        .unwrap();
464
465    if payment_address == wallet_addr {
466        Err("ADA Handle Is In Wallet Address".to_string())
467    } else {
468        Ok(payment_address.to_string())
469    }
470}
471
472pub async fn utxo_info(utxo: &str, network_flag: bool) -> Result<Vec<UtxoResponse>, Error> {
473    let network: &str = if network_flag { "preprod" } else { "api" };
474    // this will limit to 1000 utxos which is ok for an address as that is a cip30 wallet
475    // if you have 1000 utxos in that wallets that cannot pay for anything then something
476    // is wrong in that wallet
477    let url: String = format!("https://{}.koios.rest/api/v1/utxo_info", network);
478    let client: Client = reqwest::Client::new();
479
480    // Prepare the request payload
481    let payload: Value = serde_json::json!({
482        "_utxo_refs": [utxo],
483        "_extended": true
484    });
485
486    // Make the POST request
487    let response: Response = client
488        .post(url)
489        .header("accept", "application/json")
490        .header("content-type", "application/json")
491        .json(&payload)
492        .send()
493        .await?;
494
495    let utxos: Vec<UtxoResponse> = response.json().await?;
496
497    Ok(utxos)
498}
499
500// make it so it only works for nfts
501pub async fn nft_utxo(
502    policy_id: String,
503    token_name: String,
504    network_flag: bool,
505) -> Result<Vec<UtxoResponse>, Error> {
506    let network: &str = if network_flag { "preprod" } else { "api" };
507    let url: String = format!("https://{}.koios.rest/api/v1/asset_utxos", network);
508    let client: Client = reqwest::Client::new();
509
510    // Prepare the request payload
511    let payload: Value = serde_json::json!({
512        "_asset_list": [[policy_id, token_name]],
513        "_extended": true
514    });
515
516    // Make the POST request
517    let response: Response = client
518        .post(url)
519        .header("accept", "application/json")
520        .header("content-type", "application/json")
521        .json(&payload)
522        .send()
523        .await?;
524
525    let utxos: Vec<UtxoResponse> = response.json().await?;
526
527    if utxos.len() > 1 {
528        return Ok(vec![]);
529    }
530
531    Ok(utxos)
532}
533
534#[derive(Debug, Deserialize, Clone, Default)]
535pub struct ResolvedDatum {
536    pub datum_hash: Option<String>,
537    pub creation_tx_hash: String,
538    pub value: Value,
539    pub bytes: Option<String>,
540}
541
542pub async fn datum_from_datum_hash(
543    datum_hash: String,
544    network_flag: bool,
545) -> Result<Vec<ResolvedDatum>, Error> {
546    let network: &str = if network_flag { "preprod" } else { "api" };
547    let url: String = format!("https://{}.koios.rest/api/v1/datum_info", network);
548    let client: Client = reqwest::Client::new();
549
550    // Prepare the request payload
551    let payload: Value = serde_json::json!({
552        "_datum_hashes": [datum_hash],
553    });
554
555    // Make the POST request
556    let response: Response = client
557        .post(url)
558        .header("accept", "application/json")
559        .header("content-type", "application/json")
560        .json(&payload)
561        .send()
562        .await?;
563
564    let datums: Vec<ResolvedDatum> = response.json().await?;
565    Ok(datums)
566}
567
568#[derive(Debug, Deserialize, Clone, Default)]
569pub struct History {
570    pub tx_hash: String,
571    pub epoch_no: u64,
572    pub block_height: Option<u64>,
573    pub block_time: u64,
574}
575
576pub async fn asset_history(
577    policy_id: String,
578    token_name: String,
579    network_flag: bool,
580    limit: u64,
581) -> Result<Vec<String>, Error> {
582    let network: &str = if network_flag { "preprod" } else { "api" };
583    let url: String = format!(
584        "https://{}.koios.rest/api/v1/asset_txs?_asset_policy={}&_asset_name={}&_after_block_height=50000&_history=true&limit={}",
585        network, policy_id, token_name, limit
586    );
587    let client: Client = reqwest::Client::new();
588
589    // Make the POST request
590    let response: Response = client
591        .get(url)
592        .header("content-type", "application/json")
593        .send()
594        .await?;
595
596    let data: Vec<History> = response.json().await.unwrap();
597    let tx_hashes: Vec<String> = data.iter().map(|h| h.tx_hash.clone()).collect();
598    Ok(tx_hashes)
599}