use chia_bls::PublicKey;
use chia_protocol::{Bytes32, Coin, CoinSpend};
use chia_puzzle_types::nft::NftMetadata;
use chia_puzzle_types::standard::StandardArgs;
use chia_puzzle_types::Memos;
use chia_sdk_driver::{
Did, IntermediateLauncher, NftMint, SingletonInfo, SpendContext, StandardLayer,
};
use chia_sdk_types::{conditions::TransferNft, Conditions};
use serde::{Deserialize, Serialize};
use crate::error::WalletError;
use crate::metadata::{Attribute, Chip0007Metadata, CollectionAttribute, CollectionRef};
use crate::nft::{DidAttribution, NftMediaMetadata};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Collection {
pub id: String,
pub name: String,
#[serde(default)]
pub attributes: Vec<CollectionAttribute>,
pub royalty_puzzle_hash: Bytes32,
pub royalty_basis_points: u16,
}
impl Collection {
pub fn as_ref_block(&self) -> CollectionRef {
CollectionRef {
id: self.id.clone(),
name: self.name.clone(),
attributes: self.attributes.clone(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ManifestItem {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub attributes: Vec<Attribute>,
pub media: ManifestMedia,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ManifestMedia {
#[serde(default)]
pub data_uris: Vec<String>,
#[serde(default)]
pub data_hash: Option<Bytes32>,
#[serde(default)]
pub metadata_uris: Vec<String>,
#[serde(default)]
pub metadata_hash: Option<Bytes32>,
#[serde(default)]
pub license_uris: Vec<String>,
#[serde(default)]
pub license_hash: Option<Bytes32>,
}
impl ManifestMedia {
fn to_media(&self, edition_number: u64, edition_total: u64) -> NftMediaMetadata {
NftMediaMetadata {
data_uris: self.data_uris.clone(),
data_hash: self.data_hash,
metadata_uris: self.metadata_uris.clone(),
metadata_hash: self.metadata_hash,
license_uris: self.license_uris.clone(),
license_hash: self.license_hash,
edition_number,
edition_total,
}
}
}
pub fn generate_item_metadata(
collection: &Collection,
items: &[ManifestItem],
) -> Vec<Chip0007Metadata> {
let total = items.len() as u64;
items
.iter()
.enumerate()
.map(|(i, item)| {
let mut md = Chip0007Metadata::new(item.name.clone());
md.description = item.description.clone();
md.attributes = item.attributes.clone();
md.collection = Some(collection.as_ref_block());
md.series_number = Some(i as u64 + 1);
md.series_total = Some(total);
md.minting_tool = Some("DIG".to_string());
md
})
.collect()
}
#[derive(Clone, Debug)]
pub struct BulkMintResponse {
pub coin_spends: Vec<CoinSpend>,
pub launcher_ids: Vec<Bytes32>,
}
#[allow(clippy::too_many_arguments)]
pub fn build_bulk_mint(
minter_synthetic_key: PublicKey,
did: Did,
collection: &Collection,
items: &[ManifestItem],
recipient_puzzle_hash: Bytes32,
) -> Result<BulkMintResponse, WalletError> {
let mut ctx = SpendContext::new();
let launcher_ids = build_bulk_mint_in(
&mut ctx,
minter_synthetic_key,
did,
collection,
items,
recipient_puzzle_hash,
)?;
Ok(BulkMintResponse {
coin_spends: ctx.take(),
launcher_ids,
})
}
#[allow(clippy::too_many_arguments)]
pub fn build_bulk_mint_funded(
minter_synthetic_key: PublicKey,
did: Did,
collection: &Collection,
items: &[ManifestItem],
recipient_puzzle_hash: Bytes32,
funding_coin: Coin,
funding_synthetic_key: PublicKey,
) -> Result<BulkMintResponse, WalletError> {
let needed = items.len() as u64;
if funding_coin.amount < needed {
return Err(WalletError::Parse(format!(
"funding coin has {} mojo but minting {} item(s) needs at least {needed} mojo (1 mojo \
per item — the DID's own value cannot fund the extra singleton launchers)",
funding_coin.amount,
items.len(),
)));
}
let mut ctx = SpendContext::new();
let launcher_ids = build_bulk_mint_in(
&mut ctx,
minter_synthetic_key,
did,
collection,
items,
recipient_puzzle_hash,
)?;
let change = funding_coin.amount - needed;
let mut funding_conditions = Conditions::new();
if change > 0 {
let funding_puzzle_hash: Bytes32 =
StandardArgs::curry_tree_hash(funding_synthetic_key).into();
funding_conditions =
funding_conditions.create_coin(funding_puzzle_hash, change, Memos::None);
}
StandardLayer::new(funding_synthetic_key).spend(&mut ctx, funding_coin, funding_conditions)?;
Ok(BulkMintResponse {
coin_spends: ctx.take(),
launcher_ids,
})
}
pub fn build_bulk_mint_funded_no_did(
minter_synthetic_key: PublicKey,
collection: &Collection,
items: &[ManifestItem],
recipient_puzzle_hash: Bytes32,
selected_coins: Vec<Coin>,
fee: u64,
) -> Result<BulkMintResponse, WalletError> {
if items.is_empty() {
return Err(WalletError::Parse("items is empty".to_string()));
}
if selected_coins.is_empty() {
return Err(WalletError::Parse("selected_coins is empty".to_string()));
}
let launcher_mojos = items.len() as u64; let needed = launcher_mojos
.checked_add(fee)
.ok_or_else(|| WalletError::Parse("launcher mojos + fee overflows u64".to_string()))?;
let total_amount: u64 = selected_coins.iter().try_fold(0u64, |acc, c| {
acc.checked_add(c.amount)
.ok_or_else(|| WalletError::Parse("selected-coin amount sum overflows u64".to_string()))
})?;
if total_amount < needed {
return Err(WalletError::Parse(format!(
"selected coins total {total_amount} mojo but minting {} item(s) with a {fee}-mojo fee \
needs at least {needed} mojo (1 mojo per item for the singleton launchers + the fee)",
items.len(),
)));
}
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(minter_synthetic_key);
let minter_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(minter_synthetic_key).into();
let lead_coin = selected_coins[0];
let lead_coin_name = lead_coin.coin_id();
let (all_mint_conditions, launcher_ids) = build_bulk_mint_launchers(
&mut ctx,
lead_coin_name,
None,
collection,
items,
recipient_puzzle_hash,
)?;
for coin in selected_coins.into_iter().skip(1) {
p2.spend(
&mut ctx,
coin,
Conditions::new().assert_concurrent_spend(lead_coin_name),
)?;
}
let mut lead_conditions = all_mint_conditions;
if fee > 0 {
lead_conditions = lead_conditions.reserve_fee(fee);
}
let change = total_amount
.checked_sub(needed)
.ok_or_else(|| WalletError::Parse("change calculation underflows u64".to_string()))?;
if change > 0 {
lead_conditions = lead_conditions.create_coin(minter_puzzle_hash, change, Memos::None);
}
p2.spend(&mut ctx, lead_coin, lead_conditions)?;
Ok(BulkMintResponse {
coin_spends: ctx.take(),
launcher_ids,
})
}
fn build_bulk_mint_in(
ctx: &mut SpendContext,
minter_synthetic_key: PublicKey,
did: Did,
collection: &Collection,
items: &[ManifestItem],
recipient_puzzle_hash: Bytes32,
) -> Result<Vec<Bytes32>, WalletError> {
let p2 = StandardLayer::new(minter_synthetic_key);
let did_attr = DidAttribution {
launcher_id: did.info.launcher_id,
inner_puzzle_hash: did.info.inner_puzzle_hash().into(),
};
let (all_mint_conditions, launcher_ids) = build_bulk_mint_launchers(
ctx,
did.coin.coin_id(),
Some(&did_attr),
collection,
items,
recipient_puzzle_hash,
)?;
let _recreated_did = did.update(ctx, &p2, all_mint_conditions)?;
Ok(launcher_ids)
}
fn build_bulk_mint_launchers(
ctx: &mut SpendContext,
parent_coin_id: Bytes32,
did_attr: Option<&DidAttribution>,
collection: &Collection,
items: &[ManifestItem],
recipient_puzzle_hash: Bytes32,
) -> Result<(Conditions, Vec<Bytes32>), WalletError> {
if items.is_empty() {
return Err(WalletError::Parse("items is empty".to_string()));
}
let total = items.len();
let mut all_mint_conditions = Conditions::new();
let mut launcher_ids = Vec::with_capacity(total);
for (i, item) in items.iter().enumerate() {
let metadata_ptr =
ctx.alloc_hashed(&item_to_chain_metadata(item, i as u64 + 1, total as u64))?;
let transfer = did_attr.map(|did_attr| {
TransferNft::new(
Some(did_attr.launcher_id),
Vec::new(),
Some(did_attr.inner_puzzle_hash),
)
});
let mut nft_mint = NftMint::new(
metadata_ptr,
recipient_puzzle_hash,
collection.royalty_basis_points,
transfer,
);
nft_mint.royalty_puzzle_hash = collection.royalty_puzzle_hash;
let (mint_conditions, nft) = IntermediateLauncher::new(parent_coin_id, i, total)
.create(ctx)?
.mint_nft(ctx, &nft_mint)?;
all_mint_conditions = all_mint_conditions.extend(mint_conditions);
launcher_ids.push(nft.info.launcher_id);
}
Ok((all_mint_conditions, launcher_ids))
}
fn item_to_chain_metadata(
item: &ManifestItem,
edition_number: u64,
edition_total: u64,
) -> NftMetadata {
let media = item.media.to_media(edition_number, edition_total);
NftMetadata {
edition_number: media.edition_number,
edition_total: media.edition_total,
data_uris: media.data_uris,
data_hash: media.data_hash,
metadata_uris: media.metadata_uris,
metadata_hash: media.metadata_hash,
license_uris: media.license_uris,
license_hash: media.license_hash,
}
}
pub fn synthetic_puzzle_hash(synthetic_key: PublicKey) -> Bytes32 {
StandardArgs::curry_tree_hash(synthetic_key).into()
}