Skip to content

Commit

Permalink
refactor utils
Browse files Browse the repository at this point in the history
  • Loading branch information
lovelaced committed Sep 3, 2024
1 parent 348ba9e commit 01d20af
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 211 deletions.
189 changes: 55 additions & 134 deletions src/commands/mint_collection.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
use crate::error::Result;
use crate::utils::{json_utils};
use colored::*;
use spinners::{Spinner, Spinners};
use subxt::utils::{AccountId32, MultiAddress};
use crate::commands::assethub;
use crate::commands::mint_nft;
use crate::client::get_client;
use pallet_nfts::{ItemSettings, CollectionSettings};
use std::marker::PhantomData;
use reqwest::Client;
use serde_json::Value;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::error::Error;
use std::path::{Path};

// Function to convert CollectionSettings to the required BitFlags1 type
fn to_collection_bitflags(
Expand All @@ -24,131 +20,13 @@ fn to_collection_bitflags(
assethub::runtime_types::pallet_nfts::types::BitFlags1(bits, PhantomData)
}

// Function to pin data to IPFS
async fn pin_to_ipfs(data: &[u8]) -> Result<String> {
let pinata_jwt = crate::config::load_pinata_jwt_from_config()?;
let pinata_gateway = "https://api.pinata.cloud";

let client = Client::new();

if let Some(jwt) = pinata_jwt {
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(data.to_vec()).file_name("data"));

let response = client
.post(&format!("{}/pinning/pinFileToIPFS", pinata_gateway))
.bearer_auth(jwt) // Use JWT for authorization
.multipart(form)
.send()
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;

if !response.status().is_success() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to pin to IPFS via Pinata: {:?}", response.text().await),
)));
}

let pin_response: serde_json::Value = response.json().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
let ipfs_hash = pin_response["IpfsHash"].as_str().ok_or("Failed to parse IPFS hash from Pinata response")?;

Ok(format!("ipfs://{}", ipfs_hash))
} else {
let response = client
.post("https://ipfs.io/ipfs")
.body(data.to_vec())
.send()
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;

let ipfs_hash = response.text().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok(format!("ipfs://{}", ipfs_hash))
}
}

// Function to load JSON from a file
fn load_json_from_file(path: &Path) -> Result<Value> {
let file = File::open(path).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
serde_json::from_reader(file).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}

// Function to match JSON files with images based on the "image" field or filename
fn link_json_with_images(json_folder: &Path, image_folder: &Path) -> Result<HashMap<String, (Value, PathBuf)>> {
let mut linked_data = HashMap::new();

for entry in json_folder.read_dir().map_err(|e| Box::new(e) as Box<dyn std::error::Error>)? {
let entry = entry.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
if entry.path().is_file() {
let json_data = load_json_from_file(&entry.path())?;
let image_name = json_data.get("image")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
// If "image" field is not present, match by filename
let binding = entry.path();
let json_filename = binding.file_stem().unwrap_or_else(|| OsStr::new("")).to_string_lossy();
format!("{}.jpg", json_filename)
});

let image_path = image_folder.join(&image_name);
if !image_path.exists() {
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Image file not found: {:?}", image_path))));
}

linked_data.insert(image_name, (json_data, image_path));
}
}

Ok(linked_data)
}

pub async fn mint_collection(json_folder: Option<&str>, image_folder: Option<&str>) -> Result<()> {
let api = get_client().await?;
println!("{}", "🚀 Connection with parachain established.".green().bold());

let account_signer = crate::config::load_account_from_config()?;
let admin: MultiAddress<AccountId32, ()> = account_signer.public_key().into();

let mut pinned_data = Vec::new();

// Handle optional JSON and image folders
if let (Some(json_folder_str), Some(image_folder_str)) = (json_folder, image_folder) {
let json_folder_path = Path::new(json_folder_str);
let image_folder_path = Path::new(image_folder_str);

// Link JSON files with their corresponding images
let linked_data = link_json_with_images(json_folder_path, image_folder_path)?;

for (image_name, (mut json_data, image_path)) in linked_data {
let mut sp = Spinner::new(Spinners::Dots12, format!("🖼️ Processing image: {}", image_name).yellow().bold().to_string());

// If the JSON already contains an image link, use it
if let Some(image) = json_data.get("image").and_then(Value::as_str) {
if !image.is_empty() {
sp.stop_and_persist("🔗", format!("Using existing image link in JSON for {}.", image_name).green().bold().to_string());
}
}

if json_data.get("image").and_then(Value::as_str).map(|s| s.is_empty()).unwrap_or(true) {
// Pin image to IPFS
let image_bytes = std::fs::read(&image_path).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
let ipfs_image_link = pin_to_ipfs(&image_bytes).await?;
json_data["image"] = Value::String(ipfs_image_link.clone());

sp.stop_and_persist("✅", format!("Image pinned to IPFS and link added to JSON for {}.", image_name).green().bold().to_string());
}

// Pin updated JSON to IPFS
let mut sp = Spinner::new(Spinners::Dots12, format!("📦 Pinning JSON metadata for {} to IPFS...", image_name).yellow().bold().to_string());
let json_bytes = serde_json::to_vec(&json_data).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
let ipfs_json_link = pin_to_ipfs(&json_bytes).await?;
sp.stop_and_persist("✅", format!("JSON metadata for {} pinned to IPFS.", image_name).green().bold().to_string());

pinned_data.push((image_name, ipfs_json_link));
}
}

let config = assethub::runtime_types::pallet_nfts::types::CollectionConfig {
settings: to_collection_bitflags(CollectionSettings::all_enabled()),
max_supply: None,
Expand All @@ -157,26 +35,20 @@ pub async fn mint_collection(json_folder: Option<&str>, image_folder: Option<&st
price: None,
start_block: None,
end_block: None,
default_item_settings: crate::commands::mint_nft::to_item_bitflags(
ItemSettings::all_enabled(),
),
default_item_settings: mint_nft::to_item_bitflags(ItemSettings::all_enabled()),
__ignore: Default::default(),
},
__ignore: Default::default(),
};

let payload = assethub::tx().nfts().create(admin, config);
let payload = assethub::tx().nfts().create(admin.clone(), config);

let mut sp = Spinner::new(Spinners::Dots12, "⏳ Preparing transaction...".yellow().bold().to_string());
sp.stop_and_persist("🚀", "Sending transaction to the network...".yellow().bold().to_string());

let extrinsic_result = api
.tx()
.sign_and_submit_then_watch_default(&payload, &account_signer)
.await?;
let extrinsic_result = api.tx().sign_and_submit_then_watch_default(&payload, &account_signer).await?;

let mut sp = Spinner::new(Spinners::Dots12, "⏳ Finalizing transaction...".yellow().bold().to_string());

let extrinsic_result = extrinsic_result.wait_for_finalized_success().await?;

sp.stop_and_persist("✅", "Collection creation finalized!".green().bold().to_string());
Expand All @@ -193,6 +65,55 @@ pub async fn mint_collection(json_folder: Option<&str>, image_folder: Option<&st
"📦 Collection ID".cyan().bold(),
collection.to_string().bright_white()
);

// Step 2: Mint NFTs into the collection if JSON and image paths were provided
if let Some(json_folder_str) = json_folder {
let json_folder_path = Path::new(json_folder_str);
let image_folder_path = image_folder.map(Path::new);

let entries: Vec<_> = json_folder_path
.read_dir()?
.collect::<std::result::Result<Vec<_>, _>>()?;

for (nft_id, entry) in entries.iter().enumerate() {
let json_path = entry.path();

let image_path = if let Some(image_folder_path) = image_folder_path {
json_utils::find_image_for_json(&json_path).ok().or_else(|| {
let file_stem = json_path.file_stem()?.to_str()?;
let image_path = image_folder_path.join(format!("{}.jpg", file_stem));
if image_path.exists() {
Some(image_path)
} else {
let image_path = image_folder_path.join(format!("{}.jpeg", file_stem));
if image_path.exists() {
Some(image_path)
} else {
let image_path = image_folder_path.join(format!("{}.png", file_stem));
image_path.exists().then_some(image_path)
}
}
})
} else {
None
};

if image_folder_path.is_some() && image_path.is_none() {
return Err("Error: No image found or provided for the NFT.".into());
}

let image_path_str = match image_path {
Some(p) => p.to_str()
.map(|s| s.to_owned())
.ok_or_else(|| "Failed to convert image path to string".to_string())
.map(Some),
None => Ok(None),
}?;

let nft_id = nft_id as u32;
mint_nft::mint_nft(collection, nft_id, Some(json_path.to_str().unwrap()), image_path_str.as_deref()).await?;
}
}
}
} else {
println!("{}", "❌ Collection ID not found in events.".red().bold());
Expand Down
87 changes: 10 additions & 77 deletions src/commands/mint_nft.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use crate::error::Result;
use crate::utils::{ipfs_utils, json_utils};
use colored::*;
use spinners::{Spinner, Spinners};
use subxt::utils::{AccountId32, MultiAddress};
use crate::commands::assethub;
use crate::client::get_client;
use pallet_nfts::ItemSettings;
use std::marker::PhantomData;
use reqwest::Client;
use serde_json::Value;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::error::Error;
use std::path::Path;
use std::path::PathBuf;

// Function to convert ItemSettings to the required BitFlags2 type
pub fn to_item_bitflags(
Expand All @@ -22,85 +21,22 @@ pub fn to_item_bitflags(
assethub::runtime_types::pallet_nfts::types::BitFlags2(bits, PhantomData)
}

async fn pin_to_ipfs(data: &[u8]) -> Result<String> {
let pinata_jwt = crate::config::load_pinata_jwt_from_config()?;
let pinata_gateway = "https://api.pinata.cloud";

let client = Client::new();

if let Some(jwt) = pinata_jwt {
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(data.to_vec()).file_name("data"));

let response = client
.post(&format!("{}/pinning/pinFileToIPFS", pinata_gateway))
.bearer_auth(jwt) // Use JWT for authorization
.multipart(form)
.send()
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;

if !response.status().is_success() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to pin to IPFS via Pinata: {:?}", response.text().await),
)));
}

let pin_response: serde_json::Value = response.json().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
let ipfs_hash = pin_response["IpfsHash"].as_str().ok_or("Failed to parse IPFS hash from Pinata response")?;

Ok(format!("ipfs://{}", ipfs_hash))
} else {
let response = client
.post("https://ipfs.io/ipfs")
.body(data.to_vec())
.send()
.await
.map_err(|e| Box::new(e) as Box<dyn Error>)?;

let ipfs_hash = response.text().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
Ok(format!("ipfs://{}", ipfs_hash))
}
}

fn load_json_from_file(path: &Path) -> Result<Value> {
let file = File::open(path).map_err(|e| Box::new(e) as Box<dyn Error>)?;
serde_json::from_reader(file).map_err(|e| Box::new(e) as Box<dyn Error>)
}

fn find_image_for_json(json_path: &Path) -> Result<PathBuf> {
let parent_dir = json_path.parent().ok_or("Failed to find parent directory of JSON file")?;
let json_stem = json_path.file_stem().ok_or("Failed to extract file stem from JSON file name")?.to_string_lossy();

for extension in &["jpg", "jpeg", "png"] {
let image_path = parent_dir.join(format!("{}.{}", json_stem, extension));
if image_path.exists() {
return Ok(image_path);
}
}

Err("No matching image found for the provided JSON file.".into())
}

pub async fn mint_nft(collection_id: u32, nft_id: u32, json_path: Option<&str>, image_path: Option<&str>) -> Result<()> {
let api = get_client().await?;
println!("{}", "🚀 Connection with parachain established.".green().bold());

let account_signer = crate::config::load_account_from_config()?;
let account: MultiAddress<AccountId32, ()> = account_signer.public_key().into();

// Initialize variables for JSON and image processing
let mut json_data: Option<Value> = None;
let mut image_link = String::new();

if let Some(json_path_str) = json_path {
let json_path = Path::new(json_path_str);
json_data = Some(load_json_from_file(json_path)?);
json_data = Some(json_utils::load_json_from_file(json_path)?);

let mut sp = Spinner::new(Spinners::Dots12, "🖼️ Processing image...".yellow().bold().to_string());

// If the JSON already contains an image link, use it
if let Some(image) = json_data.as_ref().unwrap().get("image").and_then(Value::as_str) {
if !image.is_empty() {
image_link = image.to_string();
Expand All @@ -112,11 +48,11 @@ pub async fn mint_nft(collection_id: u32, nft_id: u32, json_path: Option<&str>,
let image_path_buf = if let Some(image_path_str) = image_path {
PathBuf::from(image_path_str)
} else {
find_image_for_json(json_path)?
json_utils::find_image_for_json(json_path)?
};

let image_bytes = std::fs::read(&image_path_buf).map_err(|e| Box::new(e) as Box<dyn Error>)?;
image_link = pin_to_ipfs(&image_bytes).await?;
let image_bytes = std::fs::read(&image_path_buf)?;
image_link = ipfs_utils::pin_to_ipfs(&image_bytes).await?;
json_data.as_mut().unwrap()["image"] = Value::String(image_link.clone());

sp.stop_and_persist("✅", "Image pinned to IPFS and link added to JSON.".green().bold().to_string());
Expand All @@ -125,11 +61,10 @@ pub async fn mint_nft(collection_id: u32, nft_id: u32, json_path: Option<&str>,
return Err("Error: --json must be provided when using --image.".into());
}

// If JSON was provided, pin it to IPFS
let ipfs_json_link = if let Some(json_data) = json_data {
let mut sp = Spinner::new(Spinners::Dots12, "📦 Pinning JSON metadata to IPFS...".yellow().bold().to_string());
let json_bytes = serde_json::to_vec(&json_data).map_err(|e| Box::new(e) as Box<dyn Error>)?;
let link = pin_to_ipfs(&json_bytes).await?;
let json_bytes = serde_json::to_vec(&json_data)?;
let link = ipfs_utils::pin_to_ipfs(&json_bytes).await?;
sp.stop_and_persist("✅", "JSON metadata pinned to IPFS.".green().bold().to_string());
Some(link)
} else {
Expand All @@ -148,16 +83,14 @@ pub async fn mint_nft(collection_id: u32, nft_id: u32, json_path: Option<&str>,

let mut sp = Spinner::new(Spinners::Dots12, "⏳ Minting NFT...".yellow().bold().to_string());
sp.stop();
let mut sp = Spinner::new(Spinners::Dots12, "🚀 Sending transaction to the network...".yellow().bold().to_string());
sp = Spinner::new(Spinners::Dots12, "🚀 Sending transaction to the network...".yellow().bold().to_string());

let extrinsic_result = api
.tx()
.sign_and_submit_then_watch_default(&nft_creation_tx, &account_signer)
.await?;

let extrinsic_result = extrinsic_result.wait_for_finalized_success().await?;
sp.stop();
sp = Spinner::new(Spinners::Dots12, "⏳ Finalizing transaction...".yellow().bold().to_string());
sp.stop_and_persist("✅", "NFT minting finalized!".green().bold().to_string());

let extrinsic_hash = extrinsic_result.extrinsic_hash();
Expand Down
Loading

0 comments on commit 01d20af

Please sign in to comment.