From 8ff6e8817478e21f4fbec2f65b9a274297a647cd Mon Sep 17 00:00:00 2001 From: raykast Date: Wed, 20 Sep 2023 20:08:38 -0700 Subject: [PATCH] WIP: Basic core CLI and drop upload command setup. --- .gitignore | 5 + Cargo.toml | 27 + src/cache.rs | 99 ++ src/cli.rs | 106 ++ src/commands/config.rs | 103 ++ src/commands/upload_drop.rs | 53 + src/config.rs | 234 ++++ src/main.rs | 59 + src/queries/queue-mint-to-drop.graphql | 8 + src/queries/schema.graphql | 1533 ++++++++++++++++++++++++ 10 files changed, 2227 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/cache.rs create mode 100644 src/cli.rs create mode 100644 src/commands/config.rs create mode 100644 src/commands/upload_drop.rs create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/queries/queue-mint-to-drop.graphql create mode 100644 src/queries/schema.graphql diff --git a/.gitignore b/.gitignore index 6985cf1..196e176 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..79b38b1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "holaplex-hub-cli" +description = "Command-line interface for Holaplex Hub" +authors = ["Holaplex "] +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "hub" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.75" +clap = { version = "4.4.4", features = ["cargo", "derive", "env", "wrap_help"] } +directories = "5.0.1" +graphql_client = { version = "0.13.0", features = ["reqwest"] } +itertools = "0.11.0" +log = "0.4.20" +reqwest = { version = "0.11.20", features = ["brotli", "deflate", "gzip", "json", "multipart"] } +ron = "0.8.1" +rpassword = "7.2.0" +rustyline = { version = "12.0.0", default-features = false } +serde = { version = "1.0.188", features = ["derive"] } +tokio = { version = "1.32.0", features = ["rt-multi-thread"] } +toml = { version = "0.8.0", features = ["preserve_order"] } +url = { version = "2.4.1", features = ["serde"] } +uuid = { version = "1.4.1", features = ["serde"] } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..a7ed383 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,99 @@ +use std::{ + fs::{self, File}, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +const SWAPFILE_NAME: &str = ".hub.cache~"; +const FILE_NAME: &str = ".hub.cache"; + +fn swap_file(path: impl AsRef) -> PathBuf { path.as_ref().join(SWAPFILE_NAME) } +fn cache_file(path: impl AsRef) -> PathBuf { path.as_ref().join(FILE_NAME) } + +#[derive(Debug, Default)] +#[repr(transparent)] +struct CacheItem(Option); + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for CacheItem { + #[inline] + fn deserialize(deserializer: D) -> std::result::Result + where D: serde::Deserializer<'de> { + Deserialize::deserialize(deserializer).map(Self) + } + + #[inline] + fn deserialize_in_place( + deserializer: D, + place: &mut Self, + ) -> std::result::Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + Deserialize::deserialize_in_place(deserializer, &mut place.0) + } +} + +impl Serialize for CacheItem { + #[inline] + fn serialize(&self, serializer: S) -> std::result::Result + where S: serde::Serializer { + Serialize::serialize( + if self.0.as_ref().map_or(false, |v| *v == T::default()) { + &None + } else { + &self.0 + }, + serializer, + ) + } +} + +impl CacheItem { + fn as_mut(&mut self) -> &mut T { + if let Some(ref mut v) = self.0 { + v + } else { + self.0 = Some(T::default()); + self.0.as_mut().unwrap() + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Cache { + asset_uploads: CacheItem, +} + +impl Cache { + pub fn load(path: impl AsRef) -> Result { + let path = cache_file(&path); + let file = match File::open(&path) { + Ok(f) => f, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Self::default()), + Err(e) => return Err(e).with_context(|| format!("Error opening cache file {path:?}")), + }; + + ron::de::from_reader(file).with_context(|| format!("Error parsing cache file {path:?}")) + } + + pub fn save(&self, path: impl AsRef) -> Result<()> { + let swap = swap_file(&path); + let path = cache_file(path); + let file = File::create(&swap) + .with_context(|| format!("Error creating cache swapfile {swap:?}"))?; + + ron::ser::to_writer(file, &self) + .with_context(|| format!("Error serializing cache to {swap:?}"))?; + + fs::rename(swap, &path).with_context(|| format!("Error writing to cache file {path:?}")) + } + + #[inline] + pub fn asset_uploads_mut(&mut self) -> &mut AssetUploads { self.asset_uploads.as_mut() } +} + +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AssetUploads {} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..5c77a82 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,106 @@ +//! Command-line options. `missing_docs_in_private_items` is enabled because +//! Clap reads doc comments for CLI help text. + +#![warn(clippy::missing_docs_in_private_items)] + +use std::{io::IsTerminal, path::PathBuf}; + +use uuid::Uuid; + +#[derive(clap::Parser)] +#[command(author, version, about)] +/// Top-level options for the hub command +pub struct Opts { + /// Use a different Hub config file than the default + /// + /// By default, hub first searches the current directory for a file named + /// .hub-config.toml, then falls back to the default location in the + /// user configuration directory. This location can be viewed by running + /// `hub config path`. + #[arg(short = 'C', long, global = true)] + pub config: Option, + + /// Name of the subcommand to run + #[command(subcommand)] + pub subcmd: Subcommand, +} + +/// Top-level subcommands for hub +#[derive(clap::Subcommand)] +pub enum Subcommand { + /// View and set configuration for the hub command + Config(Config), + /// Upload files to Hub + Upload(Upload), +} + +/// Options for hub config +#[derive(clap::Args)] +pub struct Config { + /// Name of the subcommand to run + #[command(subcommand)] + pub subcmd: ConfigSubcommand, +} + +/// Subcommands for hub config +#[derive(clap::Subcommand)] +pub enum ConfigSubcommand { + /// Print the location of the config file currently used by other hub + /// commands + Path, + /// Set the Hub GraphQL API endpoint + GraphqlEndpoint(ConfigGraphqlEndpoint), + /// Override or reset the Hub root endpoint + HubEndpoint(ConfigHubEndpoint), + /// Read a new Hub API token from STDIN + Token, +} + +/// Options for hub config graphql-endpoint +#[derive(clap::Args)] +pub struct ConfigGraphqlEndpoint { + /// Specify the GraphQL API endpoint, required if not using a terminal, + /// otherwise STDIN is used as the default + #[arg(required = !std::io::stdin().is_terminal())] + pub endpoint: Option, +} + +/// Options for hub config hub-endpoint +#[derive(clap::Args)] +pub struct ConfigHubEndpoint { + /// Reset the endpoint override and infer it from the GraphQL API endpoint + #[arg(short, long)] + pub reset: bool, + + /// Override the root Hub endpoint, required if not using a terminal, + /// otherwise STDIN is used as the default + #[arg(required = !std::io::stdin().is_terminal(), conflicts_with("reset"))] + pub endpoint: Option, +} + +/// Options for hub upload +#[derive(clap::Args)] +pub struct Upload { + /// Name of the subcommand to run + #[command(subcommand)] + pub subcmd: UploadSubcommand, +} + +/// Subcommands for hub upload +#[derive(clap::Subcommand)] +pub enum UploadSubcommand { + /// Populate files for an open drop + Drop(UploadDrop), +} + +/// Options for hub upload drop +#[derive(clap::Args)] +pub struct UploadDrop { + /// UUID of the drop to upload to + #[arg(short = 'd', long = "drop")] + pub drop_id: Uuid, + + /// Path to the structured asset directory + #[arg(short = 'p', long = "assets")] + pub asset_dir: PathBuf, +} diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000..99179dc --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,103 @@ +use std::{ + fmt, + io::{self, IsTerminal, Read}, +}; + +use anyhow::{Context, Result}; + +use crate::{ + cli::{Config as Opts, ConfigGraphqlEndpoint, ConfigHubEndpoint, ConfigSubcommand}, + config::{Config, ConfigLocation}, +}; + +fn mutate_config( + config_location: &ConfigLocation, + mutate: impl FnOnce(&mut Config) -> Result<()>, +) -> Result<()> { + let config = config_location.load()?; + let mut next_config = config.clone(); + mutate(&mut next_config)?; + + if next_config != config { + next_config.save(config_location)?; + } + + Ok(()) +} + +pub fn run(config: &ConfigLocation, opts: Opts) -> Result<()> { + let Opts { subcmd } = opts; + + match subcmd { + ConfigSubcommand::Path => { + let canon = config.path().canonicalize().ok(); + println!("{}", canon.as_deref().unwrap_or(config.path()).display()); + Ok(()) + }, + ConfigSubcommand::GraphqlEndpoint(e) => mutate_config(config, |c| graphql_endpoint(c, e)), + ConfigSubcommand::HubEndpoint(e) => mutate_config(config, |c| hub_endpoint(c, e)), + ConfigSubcommand::Token => mutate_config(config, token), + } +} + +fn read_insecure(prompt: &str, bad_parse: impl fmt::Display) -> Result +where T::Err: fmt::Display { + let mut ed = rustyline::Editor::<(), _>::new().context("Error opening STDIN for reading")?; + + loop { + let line = ed.readline(prompt).context("Error reading from STDIN")?; + + match line.parse() { + Ok(u) => break Ok(u), + Err(e) => println!("{bad_parse}: {e}"), + } + } +} + +fn graphql_endpoint(config: &mut Config, endpoint: ConfigGraphqlEndpoint) -> Result<()> { + let ConfigGraphqlEndpoint { endpoint } = endpoint; + + let endpoint = if let Some(e) = endpoint { + e.parse().context("Invalid endpoint URL")? + } else { + read_insecure("Enter new GraphQL endpoint: ", "Invalid URL")? + }; + + config.set_graphql_endpoint(endpoint); + + Ok(()) +} + +fn hub_endpoint(config: &mut Config, endpoint: ConfigHubEndpoint) -> Result<()> { + let ConfigHubEndpoint { reset, endpoint } = endpoint; + + let endpoint = if reset { + None + } else if let Some(e) = endpoint { + Some(e.parse().context("Invalid endpoint URL")?) + } else { + Some(read_insecure("Enter new Hub endpoint: ", "Invalid URL")?) + }; + + config.set_hub_endpoint(endpoint); + + Ok(()) +} + +fn token(config: &mut Config) -> Result<()> { + let token = if io::stdin().is_terminal() { + rpassword::prompt_password("Enter new Hub API token: ") + .context("Error reading from terminal")? + } else { + let mut s = String::new(); + + io::stdin() + .read_to_string(&mut s) + .context("Error reading from STDIN")?; + s + }; + + config.set_token(token); + + Ok(()) +} diff --git a/src/commands/upload_drop.rs b/src/commands/upload_drop.rs new file mode 100644 index 0000000..6075c93 --- /dev/null +++ b/src/commands/upload_drop.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use graphql_client::GraphQLQuery; + +use crate::{cache::Cache, cli::UploadDrop, config::Config, runtime}; + +#[allow(clippy::upper_case_acronyms)] +type UUID = uuid::Uuid; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/queries/schema.graphql", + query_path = "src/queries/queue-mint-to-drop.graphql", + response_derives = "Debug" +)] +struct QueueMintToDrop; + +pub fn run(config: &Config, args: UploadDrop) -> Result<()> { + let UploadDrop { drop_id, asset_dir } = args; + let mut cache = Cache::load(&asset_dir)?; + + let uploads = cache.asset_uploads_mut(); + + runtime()?.block_on(async move { + let client = config.graphql_client()?; + + let res = config + .post_graphql::(&client, queue_mint_to_drop::Variables { + in_: queue_mint_to_drop::QueueMintToDropInput { + drop: drop_id, + metadata_json: queue_mint_to_drop::MetadataJsonInput { + name: "hi".into(), + symbol: "HI".into(), + description: "help".into(), + image: "data:text/plain;help".into(), + animation_url: None, + collection: None, + attributes: vec![], + external_url: None, + properties: None, + }, + }, + }) + .await; + + println!("{res:?}"); + + Result::<_>::Ok(()) + })?; + + println!("{:?}", config.upload_endpoint()); + + cache.save(asset_dir) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c75795f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,234 @@ +use std::{ + borrow::Cow, + fs, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use anyhow::{anyhow, Context, Result}; +use directories::ProjectDirs; +use itertools::Itertools; +use log::warn; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use url::Url; + +pub const LOCAL_NAME: &str = ".hub-config.toml"; + +#[inline] +pub fn project_dirs() -> Result { + ProjectDirs::from("com", "Holaplex", "Hub") + .context("Error finding default Hub config directory") +} + +#[inline] +pub fn config_path(dirs: &ProjectDirs) -> PathBuf { dirs.config_dir().join("config.toml") } + +fn swap_path(mut path: PathBuf) -> Option { + let mut name = path.file_name()?.to_string_lossy().into_owned(); + name.push('~'); + path.set_file_name(name); + Some(path) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ConfigMode { + ExplicitWrite, + ExplicitRead, + Implicit, +} + +pub struct ConfigLocation { + path: PathBuf, + mode: ConfigMode, +} + +impl ConfigLocation { + pub fn new(path: Option, writable: bool) -> Result { + let mode = match (&path, writable) { + (Some(_), true) => ConfigMode::ExplicitWrite, + (Some(_), false) => ConfigMode::ExplicitRead, + (None, _) => ConfigMode::Implicit, + }; + let path = path.map_or_else( + || { + let local = PathBuf::from(LOCAL_NAME); + // TODO: this should probably be expanded to search upward recursively + let local_exists = local.try_exists().unwrap_or_else(|e| { + warn!("Error checking for local Hub config: {e}"); + false + }); + + Result::<_>::Ok(if local_exists { + local + } else { + config_path(&project_dirs()?) + }) + }, + Ok, + )?; + + Ok(Self { path, mode }) + } + + #[inline] + pub fn load(&self) -> Result { Config::load(self) } + + #[inline] + pub fn path(&self) -> &Path { &self.path } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct ConfigFile<'a> { + hub: Cow<'a, Config>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Config { + graphql_endpoint: Url, + hub_endpoint: Option, + token: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + graphql_endpoint: "https://api.holaplex.com/graphql".try_into().unwrap(), + hub_endpoint: None, + token: String::default(), + } + } +} + +impl Config { + pub fn load(location: &ConfigLocation) -> Result { + let text = match fs::read_to_string(&location.path) { + Ok(s) => s, + Err(e) + if e.kind() == ErrorKind::NotFound && location.mode != ConfigMode::ExplicitRead => + { + return Ok(Self::default()); + }, + Err(e) => return Err(e).context("Error reading Hub config file"), + }; + + let ConfigFile { hub } = toml::from_str(&text).context("Error parsing Hub config")?; + + Ok(hub.into_owned()) + } + + pub fn save(&self, location: &ConfigLocation) -> Result<()> { + if location.mode == ConfigMode::Implicit { + if let Some(dir) = location.path.parent() { + fs::create_dir_all(dir).context("Error creating Hub config directory")?; + } + } + + let text = toml::to_string_pretty(&ConfigFile { + hub: Cow::Borrowed(self), + }) + .context("Error serializing Hub config")?; + + let swap = swap_path(location.path.clone()).context("Invalid Hub config path")?; + + fs::write(&swap, text.as_bytes()).context("Error writing Hub config swapfile")?; + fs::rename(swap, &location.path).context("Error writing Hub config file")?; + + Ok(()) + } + + #[inline] + pub fn set_graphql_endpoint(&mut self, endpoint: Url) { self.graphql_endpoint = endpoint; } + + #[inline] + pub fn set_hub_endpoint(&mut self, endpoint: Option) { self.hub_endpoint = endpoint; } + + #[inline] + pub fn set_token(&mut self, token: String) { self.token = token; } + + pub fn graphql_client_builder(&self) -> Result { + Ok(reqwest::ClientBuilder::default() + .gzip(true) + .deflate(true) + .brotli(true) + .default_headers( + [( + reqwest::header::AUTHORIZATION, + (&self.token) + .try_into() + .context("API token is not a valid HTTP header")?, + )] + .into_iter() + .collect(), + )) + } + + #[inline] + pub fn graphql_client(&self) -> Result { + self.graphql_client_builder()? + .build() + .context("Error building HTTP client") + } + + #[inline] + pub fn post_graphql<'a, Q: graphql_client::GraphQLQuery + 'a>( + &self, + client: &'a Client, + vars: Q::Variables, + ) -> impl std::future::Future< + Output = Result, reqwest::Error>, + > + 'a { + graphql_client::reqwest::post_graphql::(client, self.graphql_endpoint.clone(), vars) + } + + pub fn hub_endpoint(&self) -> Result> { + self.hub_endpoint.as_ref().map(Cow::Borrowed).map_or_else( + || { + // TODO: try-block hack + Some(()) + .and_then(|()| { + let mut url = self.graphql_endpoint.clone(); + + let host = url.host_str()?; + let mut any_match = false; + + let host = host + .split('.') + .map(|s| { + if !any_match && s == "api" { + any_match = true; + "hub" + } else { + s + } + }) + .join("."); + + url.set_host(Some(any_match.then_some(host.as_ref())?)) + .ok()?; + + if let Some("graphql") = url.path_segments()?.last() { + url.path_segments_mut().unwrap().pop(); + } + + Some(Cow::Owned(url)) + }) + .context( + "Unable to infer Hub root endpoint from Hub GraphQL API endpoint, \ + consider setting hub-endpoint in your config", + ) + }, + Ok, + ) + } + + pub fn upload_endpoint(&self) -> Result { + let mut url = self.hub_endpoint()?.into_owned(); + url.path_segments_mut() + .map_err(|()| anyhow!("Invalid Hub endpoint"))? + .push("uploads"); + Ok(url) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec2d578 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,59 @@ +//! Entry point for the hub command + +#![deny( + clippy::disallowed_methods, + clippy::suspicious, + clippy::style, + clippy::clone_on_ref_ptr, + missing_debug_implementations, + missing_copy_implementations +)] +#![warn(clippy::pedantic, missing_docs)] +#![allow(clippy::module_name_repetitions)] + +mod cache; +mod cli; +mod config; + +mod commands { + pub mod config; + pub mod upload_drop; +} + +use anyhow::{Context, Result}; +use cli::{Opts, Subcommand, UploadSubcommand}; +use config::{Config, ConfigLocation}; + +fn main() { + match run() { + Ok(()) => (), + Err(e) => { + println!("ERROR: {e:?}"); + std::process::exit(1); + }, + } +} + +#[inline] +fn read(config: impl FnOnce(bool) -> Result) -> Result { + config(false)?.load() +} + +fn runtime() -> Result { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("Error initializing async runtime") +} + +fn run() -> Result<()> { + let Opts { config, subcmd } = clap::Parser::parse(); + let config = |w| ConfigLocation::new(config, w); + + match subcmd { + Subcommand::Config(c) => commands::config::run(&config(true)?, c), + Subcommand::Upload(u) => match u.subcmd { + UploadSubcommand::Drop(d) => commands::upload_drop::run(&read(config)?, d), + }, + } +} diff --git a/src/queries/queue-mint-to-drop.graphql b/src/queries/queue-mint-to-drop.graphql new file mode 100644 index 0000000..98b9eef --- /dev/null +++ b/src/queries/queue-mint-to-drop.graphql @@ -0,0 +1,8 @@ +mutation QueueMintToDrop($in: QueueMintToDropInput!) { + queueMintToDrop(input: $in) { + collectionMint { + id + creationStatus + } + } +} diff --git a/src/queries/schema.graphql b/src/queries/schema.graphql new file mode 100644 index 0000000..2b88f78 --- /dev/null +++ b/src/queries/schema.graphql @@ -0,0 +1,1533 @@ +schema { + query: Query + mutation: Mutation +} + +"""Input required for accepting an invitation to the organization.""" +input AcceptInviteInput { + """The ID of the invitation.""" + invite: UUID! +} + +"""The response returned after accepting an invitation to the organization.""" +type AcceptInvitePayload { + """The invitation to the organization that has been accepted.""" + invite: Invite! +} + +"""An access token used to authenticate and authorize access to the Hub API.""" +type AccessToken { + """A string representing the access token used to authenticate requests.""" + accessToken: String! + """A timestamp indicating when the access token will expire.""" + expiresAt: NaiveDateTime! + """A string indicating the type of access token, such as "Bearer".""" + tokenType: String! +} + +enum Action { + CREATE_DROP + CREATE_WALLET + MINT_EDITION + RETRY_DROP + RETRY_MINT + TRANSFER_ASSET + CREATE_COLLECTION + RETRY_COLLECTION + MINT + MINT_COMPRESSED + UPDATE_MINT + SWITCH_COLLECTION +} + +"""Represents the cost of performing a certain action on different blockchains""" +type ActionCost { + """enum that represents the type of action being performed.""" + action: Action! + """a vector of BlockchainCost structs that represents the cost of performing the action on each blockchain.""" + blockchains: [BlockchainCost!]! +} + +"""An enum type named Affiliation that defines a user's association to an organization. The enum is derived using a Union attribute. It has two variants, each containing an associated data type:""" +union Affiliation = Owner | Member + +"""Fireblocks-defined blockchain identifiers.""" +enum AssetType { + """Mainnet Solana""" + SOL + """Mainnet Polygon""" + MATIC + """Ethereum Mainnet""" + ETH +} + +enum Blockchain { + ETHEREUM + POLYGON + SOLANA +} + +"""Represents the cost of performing an action on a specific blockchain""" +type BlockchainCost { + """enum that represents the blockchain on which the action is being performed.""" + blockchain: Blockchain! + """represents the cost in credits for performing the action on the blockchain. If nil then the action is not supported on the blockchain.""" + credits: Int +} + +type Collection { + """The unique identifier for the collection.""" + id: UUID! + analytics(interval: Interval, order: Order, limit: Int): [DataPoint!]! + """The blockchain of the collection.""" + blockchain: Blockchain! + """The total supply of the collection. Setting to `null` implies unlimited minting.""" + supply: Int + """The creation status of the collection. When the collection is in a `CREATED` status you can mint NFTs from the collection.""" + creationStatus: CreationStatus! + projectId: UUID! + """The date and time in UTC when the collection was created.""" + createdAt: DateTime! + """The user id of the person who created the collection.""" + createdById: UUID! + creditsDeductionId: UUID + """ + The blockchain address of the collection used to view it in blockchain explorers. + On Solana this is the mint address. + On EVM chains it is the concatenation of the contract address and the token id `{contractAddress}:{tokenId}`. + """ + address: String + """The current number of NFTs minted from the collection.""" + totalMints: Int! + """The transaction signature of the collection.""" + signature: String + """The royalties assigned to mints belonging to the collection expressed in basis points.""" + sellerFeeBasisPoints: Int! + """ + The metadata json associated to the collection. + ## References + [Metaplex v1.1.0 Standard](https://docs.metaplex.com/programs/token-metadata/token-standard) + """ + metadataJson: MetadataJson + """The list of minted NFTs from the collection including the NFTs address and current owner's wallet address.""" + mints: [CollectionMint!] + """The list of attributed creators for the collection.""" + creators: [CollectionCreator!] + """The list of current holders of NFTs from the collection.""" + holders: [Holder!] + """A list of all NFT purchases from the collection, including both primary and secondary sales.""" + purchases: [MintHistory!] @deprecated(reason: "Use `mint_histories` instead") + """A list of all NFT mints from the collection, including both primary and secondary sales.""" + mintHistories: [MintHistory!] + drop: Drop +} + +type CollectionCreator { + collectionId: UUID! + address: String! + verified: Boolean! + share: Int! +} + +"""Represents a single NFT minted from a collection.""" +type CollectionMint { + """The unique ID of the minted NFT.""" + id: UUID! + """The ID of the collection the NFT was minted from.""" + collectionId: UUID! + """ + The address of the NFT + On Solana this is the mint address. + On EVM chains it is the concatenation of the contract address and the token id `{contractAddress}:{tokenId}`. + """ + address: String + """The wallet address of the owner of the NFT.""" + owner: String + """The status of the NFT creation.""" + creationStatus: CreationStatus! + """The unique ID of the creator of the NFT.""" + createdBy: UUID! + """The date and time when the NFT was created.""" + createdAt: DateTime! + """The transaction signature associated with the NFT.""" + signature: String + """The unique edition number of the NFT.""" + edition: Int! + """The seller fee basis points (ie royalties) for the NFT.""" + sellerFeeBasisPoints: Int! + """credits deduction id""" + creditsDeductionId: UUID + """Indicates if the NFT is compressed. Compression is only supported on Solana.""" + compressed: Boolean + """The collection the NFT was minted from.""" + collection: Collection + """ + The metadata json associated to the collection. + [Metaplex v1.1.0 Standard](https://docs.metaplex.com/programs/token-metadata/token-standard) + """ + metadataJson: MetadataJson + """The update history of the mint.""" + updateHistories: [UpdateHistory!] + """The creators of the mint. Includes the creator addresses and their shares.""" + creators: [MintCreator!] + """The record of the original mint.""" + mintHistory: MintHistory + """The history of transfers for the mint.""" + transferHistories: [NftTransfer!] + """The history of switched collections for the mint.""" + switchCollectionHistories: [SwitchCollectionHistory!] +} + +"""Input object for creating a collection.""" +input CreateCollectionInput { + project: UUID! + blockchain: Blockchain! + creators: [CreatorInput!]! + metadataJson: MetadataJsonInput! +} + +"""Result of a successful create collection mutation.""" +type CreateCollectionPayload { + collection: Collection! +} + +"""This struct represents the input for creating a new API credential, including the ID of the organization that the credential will be associated with and the friendly name assigned to the credential.""" +input CreateCredentialInput { + """The ID of the organization that the new API credential will be associated with.""" + organization: UUID! + """The friendly name assigned to the new API credential.""" + name: String! +} + +"""The response payload returned after successfully creating an API credential. It includes the newly created Credential object, which represents the API credential, as well as an `AccessToken` object that can be used to authenticate requests to the Hub API.""" +type CreateCredentialPayload { + """A `Credential` object representing the newly created API credential.""" + credential: Credential! + """An `AccessToken` object that can be used to authenticate requests to the Hub API.""" + accessToken: AccessToken! +} + +"""This input object is used for creating a customer and associated treasury for holding custodial wallets on behalf of the user.""" +input CreateCustomerInput { + """The unique identifier of the project to which the customer is associated.""" + project: UUID! +} + +"""This response represents the payload returned after successfully creating a new `customer` record. It contains a single field customer which is a `Customer` object representing the newly created customer record.""" +type CreateCustomerPayload { + """The customer record created by the create customer mutation.""" + customer: Customer! +} + +"""Input for creating a customer wallet.""" +input CreateCustomerWalletInput { + """The customer ID.""" + customer: UUID! + """Blockchain for wallet creation.""" + assetType: AssetType! +} + +"""Response after wallet creation.""" +type CreateCustomerWalletPayload { + wallet: Wallet! +} + +input CreateDropInput { + project: UUID! + price: Int + sellerFeeBasisPoints: Int + supply: Int + startTime: DateTime + endTime: DateTime + blockchain: Blockchain! + creators: [CreatorInput!]! + metadataJson: MetadataJsonInput! + type: DropType! = EDITION +} + +type CreateDropPayload { + drop: Drop! +} + +input CreateOrganizationInput { + name: String! + profileImageUrl: String +} + +type CreateOrganizationPayload { + organization: Organization! +} + +"""The input used for creating a project.""" +input CreateProjectInput { + """The ID of the organization the project belongs to.""" + organization: UUID! + """The friendly name to denote the project from others belonging to the organization.""" + name: String! + """The URL of the project's profile image.""" + profileImageUrl: String +} + +"""* The payload returned by the `createProject` mutation.""" +type CreateProjectPayload { + """* The project that was created.""" + project: Project! +} + +input CreateWebhookInput { + url: String! + organization: UUID! + description: String! + projects: [UUID!]! + filterTypes: [FilterType!]! +} + +type CreateWebhookPayload { + webhook: Webhook! + secret: String! +} + +enum CreationStatus { + BLOCKED + CANCELED + CREATED + FAILED + PENDING + REJECTED + QUEUED +} + +"""An attributed creator for a collection or mint.""" +input CreatorInput { + """The wallet address of the creator.""" + address: String! + """ + This field indicates whether the creator has been verified. This feature is only supported on the Solana blockchain. + ## References + [Metaplex Token Metadata - Verify creator instruction](https://docs.metaplex.com/programs/token-metadata/instructions#verify-a-creator) + """ + verified: Boolean + """The share of royalties payout the creator should receive.""" + share: Int! +} + +"""An `OAuth2` client application used for authentication with the Hub API.""" +type Credential { + """A user-friendly name assigned to the credential.""" + name: String! + """A unique identifier for the credential.""" + clientId: String! + """The ID of the user who created the credential.""" + createdById: UUID! + """The ID of the organization the credential belongs to.""" + organizationId: UUID! + """The datetime in UTC when the credential was created.""" + createdAt: NaiveDateTime! + """This field represents the user who created the credential.""" + createdBy: User +} + +type CreditDeposit { + id: UUID! + organization: UUID! + initiatedBy: UUID! + credits: Int! + cost: Float! + reason: DepositReason! + createdAt: DateTime! + perCreditCost: Float! +} + +type Credits { + id: UUID! + balance: Int! + deposits: [CreditDeposit!] +} + +"""A customer record represents a user in your service and is used to group custodial wallets within a specific project. This allows for easy management of wallets and associated assets for a particular customer within your service.""" +type Customer { + """The unique identifier for the customer record.""" + id: UUID! + """The ID of the project to which the customer record belongs.""" + projectId: UUID! + """The datetime when the customer record was created.""" + createdAt: NaiveDateTime! + """An optional datetime indicating the last time the customer record was updated. If the customer record has not been updated, this field will be `null`.""" + updatedAt: NaiveDateTime + """ + Returns all the wallet addresses associated with the customer. The blockchain of the address is not included and they are in no particular order. In the future, the blockchain may be indicated with a pattern of {blockchain}:{address}. + This field returns null when there is no treasury assigned to the customer yet. + """ + addresses: [String!] + """The NFTs owned by any of the customers' wallets.""" + mints: [CollectionMint!] + """The NFTs minted by the customer.""" + mintHistories: [MintHistory!] + """The treasury assigned to the customer, which contains the customer's wallets.""" + treasury: Treasury + wallet(assetId: AssetType): [Wallet!] +} + +type Data { + """Count for the metric.""" + count: Int + """The ID of the organization the data belongs to.""" + organizationId: UUID + """The ID of the collection the data belongs to.""" + collectionId: UUID + """The ID of the project the data belongs to.""" + projectId: UUID + """the timestamp associated with the data point.""" + timestamp: NaiveDateTime +} + +"""A `DataPoint` object containing analytics information.""" +type DataPoint { + """Analytics data for mints.""" + mints: [Data!] + """Analytics data for customers.""" + customers: [Data!] + """Analytics data for collections.""" + collections: [Data!] + """Analytics data for wallets.""" + wallets: [Data!] + """Analytics data for projects.""" + projects: [Data!] + webhooks: [Data!] + credits: [Data!] + transfers: [Data!] + timestamp: NaiveDateTime +} + +""" +Implement the DateTime scalar + +The input/output is a string in RFC3339 format. +""" +scalar DateTime + +input DeactivateMemberInput { + id: UUID! +} + +type DeductionTotals { + action: Action! + spent: Int! +} + +"""The input for deleting a credential.""" +input DeleteCredentialInput { + """The unique identifier assigned to the credential to be deleted.""" + credential: String! +} + +"""The response for deleting a credential.""" +type DeleteCredentialPayload { + """The unique identifier assigned to the deleted credential.""" + credential: String! +} + +input DeleteWebhookInput { + webhook: UUID! +} + +type DeleteWebhookPayload { + webhook: UUID! +} + +enum DepositReason { + GIFTED + PURCHASED +} + +type Drop { + """The unique identifier for the drop.""" + id: UUID! + dropType: DropType! + """The identifier of the project to which the drop is associated.""" + projectId: UUID! + """The creation status of the drop.""" + creationStatus: CreationStatus! + """The date and time in UTC when the drop is eligible for minting. A value of `null` means the drop can be minted immediately.""" + startTime: DateTime + """The end date and time in UTC for the drop. A value of `null` means the drop does not end until it is fully minted.""" + endTime: DateTime + """The cost to mint the drop in US dollars. When purchasing with crypto the user will be charged at the current conversion rate for the blockchain's native coin at the time of minting.""" + price: Int! + """The user id of the person who created the drop.""" + createdById: UUID! + """The date and time in UTC when the drop was created.""" + createdAt: DateTime! + pausedAt: DateTime + """ + The shutdown_at field represents the date and time in UTC when the drop was shutdown + If it is null, the drop is currently not shutdown + """ + shutdownAt: DateTime + """The collection for which the drop is managing mints.""" + collection: Collection! + """The current status of the drop.""" + status: DropStatus! + queuedMints: [CollectionMint!] + """A list of all NFT purchases from this drop.""" + purchases: [MintHistory!] @deprecated(reason: "Use `mint_histories` under `Collection` Object instead.") +} + +"""The different phases of a drop.""" +enum DropStatus { + """Actively minting.""" + MINTING + """The minting process for the collection is complete.""" + MINTED + """The drop is scheduled for minting.""" + SCHEDULED + """The drop has expired and its end time has passed.""" + EXPIRED + """The drop is still being created and is not ready to mint.""" + CREATING + """The drop is temporarily paused and cannot be minted at the moment.""" + PAUSED + """The drop is permanently shut down and can no longer be minted.""" + SHUTDOWN + """The creation process for the drop has failed""" + FAILED +} + +enum DropType { + EDITION + OPEN +} + +"""The input for editing the name of an existing credential by providing the `client_id` of the credential and the new `name` to be assigned.""" +input EditCredentialInput { + """A unique string identifier assigned to the credential during creation.""" + clientId: String! + """The new name to be assigned to the credential.""" + name: String! +} + +"""The response for editing the name of a credential.""" +type EditCredentialPayload { + """The updated credential with the edited name.""" + credential: Credential! +} + +input EditOrganizationInput { + id: UUID! + name: String! + profileImageUrl: String +} + +type EditOrganizationPayload { + organization: Organization! +} + +input EditProjectInput { + id: UUID! + name: String! + profileImageUrl: String +} + +type EditProjectPayload { + project: Project! +} + +input EditWebhookInput { + webhook: UUID! + url: String! + description: String! + projects: [UUID!]! + filterTypes: [FilterType!]! + disabled: Boolean +} + +type EditWebhookPayload { + webhook: Webhook! +} + +"""An event to which an external service can subscribe.""" +type EventType { + """Whether the event is archived or not.""" + archived: Boolean + """The date and time when the event was created, in string format.""" + createdAt: String! + """A description of the event.""" + description: String! + """The name of the event.""" + name: String! + """The JSON schema for the event payload.""" + schemas: JSON! + """The date and time when the event was last updated, in string format.""" + updatedAt: String! +} + +"""An enumeration of event types that can be subscribed to by a webhook.""" +enum FilterType { + """Event triggered when a new project is created""" + PROJECT_CREATED + """Event triggered when a new customer is created""" + CUSTOMER_CREATED + """Event triggered when a new customer treasury is created""" + CUSTOMER_TREASURY_CREATED + """Event triggered when a new wallet is created for a project""" + PROJECT_WALLET_CREATED + """Event triggered when a new wallet is created for a customer""" + CUSTOMER_WALLET_CREATED + """Event triggered when a new drop is created""" + DROP_CREATED + """Event triggered when a new drop is minted""" + DROP_MINTED + """Event triggered when a mint has been successfully transfered""" + MINT_TRANSFERED + """Event triggered when a new collection is created""" + COLLECTION_CREATED + """Event triggered when an NFT is minted to a collection""" + MINTED_TO_COLLECTION +} + +"""The holder of a collection.""" +type Holder { + """The collection ID that the holder owns.""" + collectionId: UUID! + """The wallet address of the holder.""" + address: String! + """The number of NFTs that the holder owns in the collection.""" + owns: Int! + """The specific mints from the collection that the holder owns.""" + mints: [UUID!]! +} + +"""Input object for importing a collection.""" +input ImportCollectionInput { + project: UUID! + collection: String! +} + +"""Represents the result of a successful import collection mutation.""" +type ImportCollectionPayload { + """The status of the collection import.""" + status: CreationStatus! +} + +enum Interval { + ALL + TODAY + YESTERDAY + THIS_WEEK + THIS_MONTH + THIS_YEAR + LAST_7_DAYS + LAST_30_DAYS + LAST_WEEK + LAST_MONTH + LAST_QUARTER + LAST_YEAR +} + +"""An invitation sent to join a Holaplex organization.""" +type Invite { + """The ID of the invitation.""" + id: UUID! + """The email address of the user being invited to become a member of the organization.""" + email: String! + """The status of the invitation.""" + status: InviteStatus! + """The ID of the organization to which the invitation belongs.""" + organizationId: UUID! + """The ID of the user who created the invitation.""" + createdBy: UUID! + """The datetime, in UTC, when the invitation to join the organization was created.""" + createdAt: DateTime! + """The datetime, in UTC, when the invitation status was updated.""" + updatedAt: DateTime + """The member record that is generated after the invitation to join the organization is accepted. When the user has not accepted the invitation, this field returns `null`.""" + member: Member + """The organization to which the invitation to join belongs.""" + organization: Organization +} + +"""Input required for inviting a member to the organization.""" +input InviteMemberInput { + """The ID of the organization.""" + organization: UUID! + """The email address of the invited user.""" + email: String! +} + +"""The status of a member invitation.""" +enum InviteStatus { + """The member invitation has been accepted by the invited user.""" + ACCEPTED + """The member invitation has been revoked by an existing member of the organization and is no longer valid.""" + REVOKED + """The member invitation has been sent to the invited user.""" + SENT +} + +"""A scalar that can represent any JSON value.""" +scalar JSON + +"""A member of a Holaplex organization, representing an individual who has been granted access to the organization.""" +type Member { + """The ID of the user who has been granted access to the Holaplex organization as a member.""" + userId: UUID! + """The user identity who is a member of the organization.""" + user: User + """The unique identifier of the member.""" + id: UUID! + """The ID of the Holaplex organization to which the user has been granted access.""" + organizationId: UUID! + """The datetime, in UTC, when the member joined the organization.""" + createdAt: DateTime! + """The datetime, in UTC, when the member was revoked from the organization.""" + revokedAt: DateTime + """The ID of the invitation that the member accepted to join the organization.""" + inviteId: UUID! + """The datetime, in UTC, when the member was deactivated from the organization.""" + deactivatedAt: DateTime + """The Holaplex organization to which the member belongs, representing an individual who has been granted access to the organization.""" + organization: Organization + """The invitation to join the Holaplex organization that the member accepted in order to gain access to the organization.""" + invite: Invite +} + +""" +The collection's associated metadata JSON. +## References +[Metaplex v1.1.0 Standard](https://docs.metaplex.com/programs/token-metadata/token-standard) +""" +type MetadataJson { + id: UUID! + identifier: String! + """The assigned name of the NFT.""" + name: String! + """The URI for the complete metadata JSON.""" + uri: String! + """The symbol of the NFT.""" + symbol: String! + """The description of the NFT.""" + description: String! + """The image URI for the NFT.""" + imageOriginal: String! + """An optional animated version of the NFT art.""" + animationUrl: String + """An optional URL where viewers can find more information on the NFT, such as the collection's homepage or Twitter page.""" + externalUrl: String + attributes: [MetadataJsonAttribute!] + image: String! +} + +"""An attribute of the NFT.""" +type MetadataJsonAttribute { + id: UUID! + metadataJsonId: UUID! + """The name of the attribute.""" + traitType: String! + """The value of the attribute.""" + value: String! +} + +input MetadataJsonAttributeInput { + traitType: String! + value: String! +} + +input MetadataJsonCollectionInput { + name: String + family: String +} + +input MetadataJsonFileInput { + uri: String + fileType: String +} + +input MetadataJsonInput { + name: String! + symbol: String! + description: String! + image: String! + animationUrl: String + collection: MetadataJsonCollectionInput + attributes: [MetadataJsonAttributeInput!]! + externalUrl: String + properties: MetadataJsonPropertyInput +} + +input MetadataJsonPropertyInput { + files: [MetadataJsonFileInput!] + category: String +} + +type MintCreator { + collectionMintId: UUID! + address: String! + verified: Boolean! + share: Int! +} + +"""Represents input data for `mint_edition` mutation with a UUID and recipient as fields""" +input MintDropInput { + """The ID of the drop to mint to""" + drop: UUID! + """The recipient of the mint""" + recipient: String! +} + +"""Represents payload data for the `mint_edition` mutation""" +type MintEditionPayload { + collectionMint: CollectionMint! +} + +"""A record of a minted NFT.""" +type MintHistory { + id: UUID! + """The ID of the NFT minted.""" + mintId: UUID! + """The wallet address of the buyer.""" + wallet: String! + """The signature of the transaction, if any.""" + txSignature: String + """The status of the creation of the NFT.""" + status: CreationStatus! + """The date and time when the purchase was created.""" + createdAt: DateTime! + """The ID of the collection that facilitated the mint, if any.""" + collectionId: UUID! + """The minted NFT.""" + mint: CollectionMint +} + +"""Represents input data for `mint_queued` mutation""" +input MintQueuedInput { + mint: UUID! + recipient: String! + compressed: Boolean! +} + +"""Represents payload data for `mint_queued` mutation""" +type MintQueuedPayload { + collectionMint: CollectionMint! +} + +"""Represents input data for `mint_random_queued` mutation""" +input MintRandomQueuedInput { + drop: UUID! + recipient: String! + compressed: Boolean! +} + +"""Represents input data for `mint_to_collection` mutation with a collection ID, recipient, metadata, and optional seller fee basis points as fields""" +input MintToCollectionInput { + """The ID of the collection to mint to""" + collection: UUID! + """The recipient of the mint""" + recipient: String! + """The metadata of the mint""" + metadataJson: MetadataJsonInput! + """The optional seller fee basis points""" + sellerFeeBasisPoints: Int + """ + The creators to be assigned to the NFT. + For Solana, this can be up to five creators. If the project treasury wallet is set as a creator and verified set to true the creator will be verified on chain. + For Polygon, this can be only 1 creator. + """ + creators: [CreatorInput!]! + compressed: Boolean +} + +"""Represents payload data for `mint_to_collection` mutation""" +type MintToCollectionPayload { + """The minted NFT""" + collectionMint: CollectionMint! +} + +type Mutation { + """Create an API credential to authenticate and authorize API requests to the Holaplex Hub.""" + createCredential(input: CreateCredentialInput!): CreateCredentialPayload! + """Edit the name assigned to the API credential.""" + editCredential(input: EditCredentialInput!): EditCredentialPayload! + """Delete the OAuth2 API credential.""" + deleteCredential(input: DeleteCredentialInput!): DeleteCredentialPayload! + """This mutation creates a customer record and a corresponding treasury that holds custodial wallets on behalf of a user. The treasury serves as a way to group the customer's wallets together. This makes it easier to manage wallets and associated assets for the user within a specific project. The customer and treasury are associated with the specified project ID. The response includes the newly created customer record. If there is an error connecting to the database or unable to emit a customer created event, the mutation will fail and an error will be returned.""" + createCustomer(input: CreateCustomerInput!): CreateCustomerPayload! + """ + This mutation creates a new NFT collection. The collection returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the collection is ready to be minted. + For Solana, the collection is a sized Metaplex certified collection. + """ + createCollection(input: CreateCollectionInput!): CreateCollectionPayload! + """This mutation tries to re-create a failed collection.""" + retryCollection(input: RetryCollectionInput!): CreateCollectionPayload! + """This mutation imports a Solana collection. See the [guide](https://docs.holaplex.com/hub/Guides/import-collection) for importing instructions.""" + importSolanaCollection(input: ImportCollectionInput!): ImportCollectionPayload! + """Update a collection attributes or creators.""" + patchCollection(input: PatchCollectionInput!): PatchCollectionPayload! + """ + This mutation allows you to change the collection to which a mint belongs. + For Solana, the mint specified by `input` must already belong to a Metaplex Certified Collection. + The collection you are aiming to switch to must also be Metaplex Certified Collection. + """ + switchCollection(input: SwitchCollectionInput!): SwitchCollectionPayload! + """ + This mutation mints an NFT edition for a specific drop ID. The mint returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the mint is accepted by the blockchain. + # Errors + If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + mintEdition(input: MintDropInput!): MintEditionPayload! + """ + This mutation retries a mint which failed or is in pending state. The mint returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the mint is accepted by the blockchain. + # Errors + If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + retryMintEdition(input: RetryMintEditionInput!): RetryMintEditionPayload! + """ + This mutation mints either a compressed or standard NFT to a collection. + For Solana, the mint is verified and the collection size incremented. + """ + mintToCollection(input: MintToCollectionInput!): MintToCollectionPayload! + """ + This mutation updates a mint. + # Errors + If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + updateMint(input: UpdateMintInput!): UpdateMintPayload! + """ + This mutation retries updating a mint that failed by providing the ID of the `update_history`. + # Errors + If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + retryUpdateMint(input: RetryUpdateMintInput!): RetryUpdateMintPayload! + """ + Retries a mint which failed by passing its ID. + # Errors + """ + retryMintToCollection(input: RetryMintEditionInput!): RetryMintEditionPayload! + queueMintToDrop(input: QueueMintToDropInput!): QueueMintToDropPayload! + """This mutation mints a specific queued drop mint.""" + mintQueued(input: MintQueuedInput!): MintQueuedPayload! + """This mutation mints a random queued drop mint.""" + mintRandomQueuedToDrop(input: MintRandomQueuedInput!): MintQueuedPayload! + """ + Transfers an asset from one user to another on a supported blockchain network. + The mutation supports transferring standard or compressed NFTs. + The mutation is rejected if the wallet address is not managed by HUB. + """ + transferAsset(input: TransferAssetInput!): TransferAssetPayload! + """ + This mutation creates a new NFT drop and its associated collection. The drop returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + Error + If the drop cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + createDrop(input: CreateDropInput!): CreateDropPayload! + """ + This mutation retries an existing drop. + The drop returns immediately with a creation status of CREATING. + You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + Errors + The mutation will fail if the drop and its related collection cannot be located, + if the transaction response cannot be built, + or if the transaction event cannot be emitted. + """ + retryDrop(input: RetryDropInput!): CreateDropPayload! + """This mutation allows for the temporary blocking of the minting of editions and can be resumed by calling the resumeDrop mutation.""" + pauseDrop(input: PauseDropInput!): PauseDropPayload! + """This mutation resumes a paused drop, allowing minting of editions to be restored""" + resumeDrop(input: ResumeDropInput!): ResumeDropPayload! + """ + Shuts down a drop by writing the current UTC timestamp to the shutdown_at field of drop record. + Returns the `Drop` object on success. + + # Errors + Fails if the drop or collection is not found, or if updating the drop record fails. + """ + shutdownDrop(input: ShutdownDropInput!): ShutdownDropPayload! + """ + This mutation allows updating a drop and it's associated collection by ID. + It returns an error if it fails to reach the database, emit update events or assemble the on-chain transaction. + Returns the `PatchDropPayload` object on success. + """ + patchDrop(input: PatchDropInput!): PatchDropPayload! + """ + This mutation creates a new Holaplex organization, with the user triggering the mutation automatically assigned as the owner of the organization. + # Errors + This mutation produces an error if it is unable to connect to the database, emit the organization creation event, or if the user is not set in the X-USER-ID header. + """ + createOrganization(input: CreateOrganizationInput!): CreateOrganizationPayload! + """This mutation edits the name or profile image of the organization.""" + editOrganization(input: EditOrganizationInput!): EditOrganizationPayload! + """ + This mutation creates a new project under the specified organization. + + # Errors + This mutation produces an error if it is unable to connect to the database, emit the project creation event, or if the user is not set in the X-USER-ID header. + """ + createProject(input: CreateProjectInput!): CreateProjectPayload! + """This mutations edits the name and profile image of the project.""" + editProject(input: EditProjectInput!): EditProjectPayload! + """ + To invite a person to the organization, provide their email address. + # Error + This mutation will produce an error if it is unable to connect to the database or if there is no associated user set in the X-USER-ID header. + """ + inviteMember(input: InviteMemberInput!): Invite! + """ + Accept an invite to the organization. + # Error + This mutation will produce an error if it is unable to connect to the database or if the user's email does not match the invitation. + """ + acceptInvite(input: AcceptInviteInput!): AcceptInvitePayload! + """ + Returns member object on success + + # Errors + This code may result in an error if the update to the database fails or if it fails to produce an event. + """ + deactivateMember(input: DeactivateMemberInput!): Member! + """ + Returns member object on success + + # Errors + This code may result in an error if the update to the database fails or if it fails to produce an event. + """ + reactivateMember(input: ReactivateMemberInput!): Member! + """ + Create a wallet for a customer and assign it to the customer's treasury account. + + # Errors + The mutation will result in an error if it is unable to interact with the database or communicate with Fireblocks. + """ + createCustomerWallet(input: CreateCustomerWalletInput!): CreateCustomerWalletPayload! + """ + Res + + # Errors + This function fails if ... + """ + createWebhook(input: CreateWebhookInput!): CreateWebhookPayload! + """ + Res + + # Errors + This function fails if ... + """ + deleteWebhook(input: DeleteWebhookInput!): DeleteWebhookPayload! + """ + Res + + # Errors + This function fails if ... + """ + editWebhook(input: EditWebhookInput!): EditWebhookPayload! +} + +""" +ISO 8601 combined date and time without timezone. + +# Examples + +* `2015-07-01T08:59:60.123`, +""" +scalar NaiveDateTime + +"""A record of a transfer of an NFT.""" +type NftTransfer { + """The ID of the NFT transfer.""" + id: UUID! + """The transaction signature of the transfer.""" + txSignature: String + """The ID of the NFT that was transferred.""" + collectionMintId: UUID! + """The wallet address of the sender.""" + sender: String! + """The wallet address of the recipient.""" + recipient: String! + """The date and time when the transfer was created.""" + createdAt: DateTime! +} + +enum Order { + ASC + DESC +} + +"""A Holaplex organization is the top-level account within the Holaplex ecosystem. Each organization has a single owner who can invite members to join. Organizations use projects to organize NFT campaigns or initiatives.""" +type Organization { + """The unique identifier assigned to the Holaplex organization, which is used to distinguish it from other organizations within the Holaplex ecosystem.""" + id: UUID! + analytics(interval: Interval, order: Order, limit: Int): [DataPoint!]! + """ + Get a single API credential by client ID. + + # Arguments + + * `ctx` - The GraphQL context object containing the database connection pool and other data. + * `client_id` - The client ID of the API credential to retrieve. + + # Returns + + The API credential with the specified client ID. + """ + credential(clientId: String!): Credential! + """ + Get a list of API credentials associated with this organization. + + # Arguments + + * `ctx` - The GraphQL context object containing the database connection pool and other data. + * `limit` - Optional limit on the number of credentials to retrieve. + * `offset` - Optional offset for the credentials to retrieve. + + # Returns + + A list of API credentials associated with this organization. + """ + credentials(limit: Int, offset: Int): [Credential!]! + """ + Define an asynchronous function to load the credits for the organization + Returns `Credits` object + #Errors + returns error if credits_loader is not found in the context or if the loader fails to load the credits + """ + credits: Credits + """ + Define an asynchronous function to load the total credits deducted for each action + Returns `DeductionTotals` object + #Errors + returns error if total_deductions_loader is not found in the context or if the loader fails to load the total deductions + """ + deductionTotals: [DeductionTotals!] + """The name given to the Holaplex organization, which is used to identify it within the Holaplex ecosystem and to its members and users.""" + name: String! + """The datetime, in UTC, when the Holaplex organization was created by its owner.""" + createdAt: DateTime! + """The datetime, in UTC, when the Holaplex organization was deactivated by its owner.""" + deactivatedAt: DateTime + """The optional profile image associated with the Holaplex organization, which can be used to visually represent the organization.""" + profileImageUrlOriginal: String + """The members who have been granted access to the Holaplex organization, represented by individuals who have been invited and accepted the invitation to join the organization.""" + members: [Member!] + """The owner of the Holaplex organization, who has created the organization and has full control over its settings and members.""" + owner: Owner + """The invitations to join the Holaplex organization that have been sent to email addresses and are either awaiting or have been accepted by the recipients.""" + invites(status: InviteStatus): [Invite!]! + """The projects that have been created and are currently associated with the Holaplex organization, which are used to organize NFT campaigns or initiatives within the organization.""" + projects: [Project!]! + profileImageUrl: String + """ + Retrieves a list of all webhooks associated with the organization. + + # Arguments + + * `ctx` - The context object representing the current request. + + # Returns + + A vector of all Webhook objects associated with the Organization, or None if there are none. + + # Errors + + This function will return an error if the data context cannot be retrieved. + """ + webhooks: [Webhook!] + """ + Retrieves a specific webhook associated with the organization, based on its ID. + + # Arguments + + * `ctx` - The context object representing the current request. + * `id` - The UUID of the Webhook to retrieve. + + # Returns + + The specified Webhook object, or None if it does not exist. + + # Errors + + This function will return an error if the data context cannot be retrieved. + """ + webhook(id: UUID!): Webhook +} + +"""The owner of the Holaplex organization, who is the individual that created the organization.""" +type Owner { + """The ID of the user who created the Holaplex organization and serves as its owner.""" + userId: UUID! + """The user identity associated with the owner of the organization.""" + user: User + """The unique identifier assigned to the record of the user who created the Holaplex organization and serves as its owner, which is used to distinguish their record from other records within the Holaplex ecosystem.""" + id: UUID! + """ + The ID assigned to the Holaplex organization owned by the user, which is used to distinguish it from other organizations within the Holaplex ecosystem." + """ + organizationId: UUID! + """The datetime, in UTC, when the organization was created.""" + createdAt: DateTime! + """The Holaplex organization owned by the user.""" + organization: Organization +} + +"""Input object for patching a collection by ID.""" +input PatchCollectionInput { + """The unique identifier of the drop""" + id: UUID! + """The new metadata JSON for the drop""" + metadataJson: MetadataJsonInput + """The creators of the drop""" + creators: [CreatorInput!] +} + +"""Represents the result of a successful patch collection mutation.""" +type PatchCollectionPayload { + """The collection that has been patched.""" + collection: Collection! +} + +"""Input object for patching a drop and associated collection by ID""" +input PatchDropInput { + """The unique identifier of the drop""" + id: UUID! + """The new price for the drop in the native token of the blockchain""" + price: Int + """The new start time for the drop in UTC""" + startTime: DateTime + """The new end time for the drop in UTC""" + endTime: DateTime + """The new seller fee basis points for the drop""" + sellerFeeBasisPoints: Int + """The new metadata JSON for the drop""" + metadataJson: MetadataJsonInput + """The creators of the drop""" + creators: [CreatorInput!] +} + +"""Represents the result of a successful patch drop mutation.""" +type PatchDropPayload { + """The drop that has been patched.""" + drop: Drop! +} + +"""Represents input fields for pausing a drop.""" +input PauseDropInput { + drop: UUID! +} + +"""Represents the result of a successful pause drop mutation.""" +type PauseDropPayload { + """The drop that has been paused.""" + drop: Drop! +} + +"""A Holaplex project that belongs to an organization. Projects are used to group unique NFT campaigns or initiatives, and are used to assign objects that end customers will interact with, such as drops and wallets.""" +type Project { + """The unique identifier assigned to the Holaplex project.""" + id: UUID! + analytics(interval: Interval, order: Order, limit: Int): [DataPoint!]! + """Retrieve a customer record associated with the project, using its ID.""" + customer(id: UUID!): Customer + """Retrieve all customer records associated with a given project.""" + customers: [Customer!] + """The drops associated with the project.""" + drops: [Drop!] + """Look up a drop associated with the project by its ID.""" + drop(id: UUID!): Drop @deprecated(reason: "Use `drop` root query field instead") + """The collections associated with the project.""" + collections: [Collection!] + """Look up a collection associated with the project by its ID.""" + collection(id: UUID!): Collection @deprecated(reason: "Use `collection` root query field instead") + """The friendly name assigned to the Holaplex project to differentiate it from other projects belonging to the organization.""" + name: String! + """The ID of the Holaplex organization to which the project belongs.""" + organizationId: UUID! + """The datetime, in UTC, when the project was created.""" + createdAt: DateTime! + """The date and time in Coordinated Universal Time (UTC) when the Holaplex project was created. Once a project is deactivated, objects that were assigned to the project can no longer be interacted with.""" + deactivatedAt: DateTime + """The optional profile image associated with the project, which can be used to visually represent the project.""" + profileImageUrlOriginal: String + organization: Organization + profileImageUrl: String + """The treasury assigned to the project, which contains the project's wallets.""" + treasury: Treasury +} + +type Query { + """ + Returns a list of data points for a specific collection and timeframe. + + # Arguments + * `organizationId` - The ID of the organization + * `projectId` - The ID of the project. + * `collectionId` - The ID of the collection. + * `measures` - An map array of resources to query (resource, operation). + * `interval` - The timeframe interval. `TODAY` | `YESTERDAY` | `THIS_MONTH` | `LAST_MONTH` + * `order` - order the results by ASC or DESC. + * `limit` - Optional limit on the number of data points to retrieve. + + # Returns + A vector of Analytics objects representing the analytics data. + + # Errors + This function returns an error if there was a problem with retrieving the data points. + """ + analytics(organizationId: UUID, projectId: UUID, collectionId: UUID, interval: Interval, order: Order, limit: Int): [DataPoint!]! + """ + Returns a list of `ActionCost` which represents the cost of each action on different blockchains. + + # Errors + This function fails if it fails to get `CreditsClient` or if blockchain enum conversion fails. + """ + creditSheet: [ActionCost!]! + """Retrieve a user identity by providing their ID.""" + user(id: UUID!): User + """Look up a `collection_mint` by its ID.""" + mint(id: UUID!): CollectionMint + """Look up a `collection` by its ID.""" + collection(id: UUID!): Collection + """Look up a `drop` by its ID.""" + drop(id: UUID!): Drop + """Query an organization by its ID, this query returns `null` if the organization does not exist.""" + organization(id: UUID!): Organization + """Query a project by it's ID, this query returns `null` if the project does not exist.""" + project(id: UUID!): Project + """Retrieve a member invitation by its ID.""" + invite(id: UUID!): Invite + """ + Query to find a `Wallet` by its blockchain address. + + # Errors + This function fails if the `AppContext` cannot be accessed, + the address provided is not a valid blockchain address + or fails to load from the database. + """ + wallet(address: String!): Wallet + """ + Returns a list of event types that an external service can subscribe to. + + # Returns + + A vector of EventType objects representing the different event types that can be subscribed to. + + # Errors + + This function returns an error if there was a problem with retrieving the event types. + """ + eventTypes: [EventType!]! +} + +"""Represents input data for `queue_mint_to_drop` mutation""" +input QueueMintToDropInput { + drop: UUID! + metadataJson: MetadataJsonInput! +} + +"""Represents payload data for `queue_mint_to_drop` mutation""" +type QueueMintToDropPayload { + collectionMint: CollectionMint! +} + +input ReactivateMemberInput { + id: UUID! +} + +"""Represents input fields for resuming a paused drop.""" +input ResumeDropInput { + drop: UUID! +} + +"""Represents the result of a successful resume drop mutation.""" +type ResumeDropPayload { + """The drop that has been resumed.""" + drop: Drop! +} + +"""Input object for retrying a collection by ID.""" +input RetryCollectionInput { + id: UUID! +} + +input RetryDropInput { + drop: UUID! +} + +"""Represents input data for `retry_mint` mutation with an ID as a field of type UUID""" +input RetryMintEditionInput { + id: UUID! +} + +"""Represents payload data for `retry_mint` mutation""" +type RetryMintEditionPayload { + collectionMint: CollectionMint! +} + +input RetryUpdateMintInput { + """Update History ID""" + revisionId: UUID! +} + +type RetryUpdateMintPayload { + status: CreationStatus! +} + +"""Represents the input fields for shutting down a drop""" +input ShutdownDropInput { + drop: UUID! +} + +"""Represents the result of a successful shutdown drop mutation""" +type ShutdownDropPayload { + """Drop that has been shutdown""" + drop: Drop! +} + +type SwitchCollectionHistory { + id: UUID! + collectionMintId: UUID! + collectionId: UUID! + creditDeductionId: UUID! + signature: String + status: CreationStatus! + initiatedBy: UUID! + createdAt: NaiveDateTime! +} + +"""Input object for switching a mint's collection.""" +input SwitchCollectionInput { + mint: UUID! + collectionAddress: String! +} + +"""Represents the result of a successful switch collection mutation.""" +type SwitchCollectionPayload { + collectionMint: CollectionMint! +} + +input TransferAssetInput { + id: UUID! + recipient: String! +} + +type TransferAssetPayload { + mint: CollectionMint! +} + +"""A collection of wallets assigned to different entities in the Holaplex ecosystem.""" +type Treasury { + """The unique identifier for the treasury.""" + id: UUID! + """ + The associated Fireblocks vault ID. + ## Reference + [Vault Objects](https://docs.fireblocks.com/api/#vault-objects) + """ + vaultId: String! + """The creation DateTimeWithTimeZone of the vault.""" + createdAt: DateTime! + """The treasury's associated wallets.""" + wallets: [Wallet!] + """Lookup a wallet based on its `asset_type`.""" + wallet(assetType: AssetType!): Wallet +} + +type UpdateHistory { + id: UUID! + mintId: UUID! + txnSignature: String + status: CreationStatus! + creditDeductionId: UUID! + createdBy: UUID! + createdAt: NaiveDateTime! +} + +input UpdateMintInput { + """The ID of the mint to be updated""" + id: UUID! + """The metadata of the mint""" + metadataJson: MetadataJsonInput! + """The optional seller fee basis points""" + sellerFeeBasisPoints: Int + """ + The creators to be assigned to the NFT. + For Solana, this can be up to five creators. If the project treasury wallet is set as a creator and verified set to true the creator will be verified on chain. + For Polygon, this can be only 1 creator. + """ + creators: [CreatorInput!]! +} + +type UpdateMintPayload { + collectionMint: CollectionMint! +} + +"""A unique user identity across the entire Holaplex ecosystem. A user can be associated with multiple organizations, but they are not required to have separate login credentials.""" +type User { + """The unique identifier for the user identity.""" + id: UUID! + """The first name of the user identity.""" + firstName: String! + """The last name of the user identity.""" + lastName: String! + """The email address associated with the user identity.""" + email: String! + """The profile image associated with the user identity.""" + profileImage: String + """The timestamp in UTC when the user identity was created.""" + createdAt: String! + """The timestamp in UTC when the user identity was last updated.""" + updatedAt: String! + affiliations: [Affiliation!]! +} + +""" +A UUID is a unique 128-bit number, stored as 16 octets. UUIDs are parsed as +Strings within GraphQL. UUIDs are used to assign unique identifiers to +entities without requiring a central allocating authority. + +# References + +* [Wikipedia: Universally Unique Identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier) +* [RFC4122: A Universally Unique IDentifier (UUID) URN Namespace](http://tools.ietf.org/html/rfc4122) +""" +scalar UUID + +"""A blockchain wallet is a digital wallet that allows users to securely store, manage, and transfer their cryptocurrencies or other digital assets on a blockchain network.""" +type Wallet { + """The wallet address.""" + address: String + """The NFTs that were minted from Holaplex and are owned by the wallet's address.""" + mints: [CollectionMint!] + treasuryId: UUID! + createdAt: DateTime! + removedAt: DateTime + createdBy: UUID! + """The wallet's associated blockchain.""" + assetId: AssetType! + id: UUID! + deductionId: UUID +} + +"""A webhook represents an endpoint registered to receive notifications for specific events within a project.""" +type Webhook { + """Retrieves the ID of the user who created the webhook.""" + createdById: UUID! + """The user who created the webhook.""" + createdBy: User + """Retrieves the ID of the webhook.""" + id: UUID! + """Retrieves the channels the webhook is subscribed to.""" + channels: [String!]! + """This field specifies the list of projects for which an associated object will trigger a webhook event.""" + projects: [Project!]! + """Retrieves the ID of the webhook's endpoint.""" + endpointId: String! + """Retrieves the URL of the webhook's endpoint.""" + url: String! + """Retrieves the events the webhook is subscribed to.""" + events: [FilterType!]! + """Retrieves the webhook's description.""" + description: String! + """Retrieves the creation datetime of the webhook.""" + createdAt: NaiveDateTime! + """Retrieves the ID of the organization the webhook belongs to.""" + organizationId: UUID! + """Retrieves the last update datetime of the webhook.""" + updatedAt: NaiveDateTime +}