shadow_drive_cli/command/nft/
utils.rs

1use inquire::validator::Validation;
2use serde_json::json;
3use serde_json::Value;
4use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
5use std::str::FromStr;
6
7/// This function ensures the contents of a JSON file are compliant with the Off-Chain Shadow Standard
8/// which we define as a JSON with the non-null values for the following fields:
9///
10/// 1) `name`:  Name of the asset.
11/// 2) `symbol`: Symbol of the asset.
12/// 3) `description`: Description of the asset.
13/// 4) `image`: URI pointing to the asset's logo.
14/// 5) `external_url`: URI pointing to an external URL defining the asset — e.g. the game's main site.
15///
16/// The function simply checks whether these fields are non-null. Although we do not check for it,
17/// we recommend the following fields are included if relevant:
18///
19/// 6) `animation_url` (optional): URI pointing to the asset's animation.
20/// 7) `attributes` (optional): Array of attributes defining the characteristics of the asset.
21///    a) `trait_type`: The type of attribute.
22///    b) `value`: The value for that attribute.
23pub fn validate_json_compliance(json: &Value) -> bool {
24    let has_name = json.get("name").is_some();
25    let has_symbol = json.get("symbol").is_some();
26    let has_description = json.get("description").is_some();
27    let has_image = json.get("image").is_some();
28    let has_external_url = json.get("external_url").is_some();
29
30    has_name & has_symbol & has_description & has_image & has_external_url
31}
32
33pub(crate) async fn swap_sol_for_shdw_tx(
34    shades: u64,
35    user: Pubkey,
36) -> anyhow::Result<VersionedTransaction> {
37    // First we get the best route/quote
38    let Ok(quote) = Value::from_str(&quote_sol_to_shdw(shades).await?) else {
39        return Err(anyhow::Error::msg("Failed to parse jup.ag quote response as json"))
40    };
41
42    // Then request the transaction for this swap
43    let request_body = json!({
44        "route": dbg!(&quote[0]),
45        "userPublicKey": user.to_string(),
46        "wrapUnwrapSOL": true,
47        // "feeAccount": fee_account // leaving in very unlikely case we ever want to charge a fee
48    });
49    let client = reqwest::Client::new();
50    let response = client
51        .post("https://quote-api.jup.ag/v4/swap")
52        .header("Content-Type", "application/json")
53        .json(&request_body)
54        .send()
55        .await?;
56
57    // Parse response as json
58    let Ok(body) = serde_json::Value::from_str(&response.text().await?) else {
59        return Err(anyhow::Error::msg("Failed to parse jup.ag swap_tx response as json"))
60    };
61
62    // Deserialize response into VersionedTransaction
63    let Some(Some(tx_body))= body.get("swapTransaction").map(|b| b.as_str()) else {
64        return Err(anyhow::Error::msg("Unexpected response from jup.ag swap_tx endpoint"))
65    };
66    #[allow(deprecated)]
67    let Ok(Ok(transaction)) = base64::decode(tx_body).map(|bytes| bincode::deserialize(&bytes)) else {
68        return Err(anyhow::Error::msg("Invalid base64 encoding from jup.ag swap_tx endpoint"))
69    };
70
71    Ok(transaction)
72}
73
74pub(crate) const SHDW_MINT: &'static str = "SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y";
75pub(crate) const SOL_MINT: &'static str = "So11111111111111111111111111111111111111112";
76pub(crate) const SHDW_MINT_PUBKEY: Pubkey = Pubkey::new_from_array([
77    6, 121, 219, 1, 206, 42, 132, 247, 28, 19, 158, 124, 153, 66, 246, 218, 59, 51, 31, 222, 195,
78    49, 157, 2, 248, 153, 235, 167, 1, 52, 115, 126,
79]);
80
81async fn quote_sol_to_shdw(shades: u64) -> anyhow::Result<String> {
82    const SLIPPAGE_BPS: u16 = 5;
83
84    let url = format!(
85        "https://quote-api.jup.ag/v4/quote?inputMint={}&outputMint={}&amount={}&slippageBps={SLIPPAGE_BPS}&swapMode=ExactOut",
86        SOL_MINT, SHDW_MINT, shades
87    );
88
89    let response = reqwest::Client::new()
90        .get(&url)
91        .header("accept", "application/json")
92        .send()
93        .await?;
94
95    let body = response.text().await?;
96
97    Ok(body)
98}
99
100pub(crate) fn pubkey_validator(
101    input: &str,
102) -> Result<Validation, Box<dyn std::error::Error + Send + Sync>> {
103    // Check for valid pubkey
104    if Pubkey::from_str(input).is_ok() {
105        Ok(Validation::Valid)
106    } else {
107        Ok(Validation::Invalid("Invalid Pubkey".into()))
108    }
109}
110
111pub(crate) fn validate_and_convert_to_half_percent(input: &str) -> Result<u8, &'static str> {
112    // Removing possible percent sign from input
113    let input = input.trim().trim_end_matches('%');
114
115    // Try to parse input into a floating point number
116    let value = input.parse::<f64>();
117
118    match value {
119        Ok(v) => {
120            // Checking if value is positive and half or whole number
121            if v < 0.0 {
122                Err("Value must be positive.")
123            } else if (2.0 * v).fract() != 0.0 {
124                Err("Value must be a whole or half number.")
125            } else {
126                // Multiplying value by 2 to convert to half percentages and round to closest integer
127                Ok((2.0 * v).round() as u8)
128            }
129        }
130        Err(_) => Err("Invalid input, not a number."),
131    }
132}
133
134#[test]
135fn test_validate_and_convert_to_half_percent() {
136    assert_eq!(validate_and_convert_to_half_percent("1"), Ok(2));
137    assert_eq!(validate_and_convert_to_half_percent("1%"), Ok(2));
138    assert_eq!(validate_and_convert_to_half_percent("1.5"), Ok(3));
139    assert_eq!(validate_and_convert_to_half_percent("1.5%"), Ok(3));
140    assert_eq!(validate_and_convert_to_half_percent("2"), Ok(4));
141    assert_eq!(validate_and_convert_to_half_percent("2%"), Ok(4));
142    assert_eq!(validate_and_convert_to_half_percent("2.0"), Ok(4));
143    assert_eq!(validate_and_convert_to_half_percent("2.0%"), Ok(4));
144    assert_eq!(validate_and_convert_to_half_percent("2.5"), Ok(5));
145    assert_eq!(validate_and_convert_to_half_percent("2.5%"), Ok(5));
146
147    assert_eq!(
148        validate_and_convert_to_half_percent("2.4"),
149        Err("Value must be a whole or half number.")
150    );
151    assert_eq!(
152        validate_and_convert_to_half_percent("-1"),
153        Err("Value must be positive.")
154    );
155    assert_eq!(
156        validate_and_convert_to_half_percent("-1.5"),
157        Err("Value must be positive.")
158    );
159    assert_eq!(
160        validate_and_convert_to_half_percent("not a number"),
161        Err("Invalid input, not a number.")
162    );
163    assert_eq!(
164        validate_and_convert_to_half_percent(""),
165        Err("Invalid input, not a number.")
166    );
167}