mod common;
use std::io::Write;
use std::process::Command;
use anyhow::Result;
use regex::Regex;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signer::Signer;
use std::str::FromStr;
use common::{
assert_success, decode_onchain_metadata, parse_mint_from_output, strip_debug_quotes, trim_null,
TestContext,
};
fn parse_edition_mints(output: &str) -> Vec<String> {
let re = Regex::new(r"Edition with mint: (\S+)").expect("invalid regex");
re.captures_iter(output)
.filter_map(|c| c.get(1))
.map(|m| strip_debug_quotes(m.as_str()))
.collect()
}
#[test]
#[ignore = "requires solana-test-validator (run with --ignored)"]
fn test_create_master_edition_and_mint_editions() -> Result<()> {
let mut ctx = TestContext::new()?;
let temp_dir = ctx.create_temp_dir("mint-editions");
let nft_json = temp_dir.join("test_nft.json");
ctx.create_test_nft_json(&nft_json)?;
let nft_json_str = nft_json.to_string_lossy().to_string();
let output = ctx.run_metaboss(&[
"mint",
"one",
"-d",
&nft_json_str,
"-k",
&ctx.keypair_path,
"--max-editions",
"10",
]);
assert_success(&output);
let raw_mint = parse_mint_from_output(&output.stdout);
let master_mint = strip_debug_quotes(&raw_mint);
let output = ctx.run_metaboss(&[
"mint",
"editions",
"-k",
&ctx.keypair_path,
"-a",
&master_mint,
"--next-editions",
"3",
]);
assert_success(&output);
let edition_mints = parse_edition_mints(&output.stdout);
assert_eq!(
edition_mints.len(),
3,
"should have minted exactly 3 editions"
);
for edition_mint in &edition_mints {
let metadata = decode_onchain_metadata(&ctx, edition_mint)?;
assert_eq!(
trim_null(&metadata.name),
"Test NFT",
"edition metadata name should match master"
);
assert_eq!(
trim_null(&metadata.symbol),
"TNFT",
"edition metadata symbol should match master"
);
}
Ok(())
}
#[test]
#[ignore = "requires solana-test-validator (run with --ignored)"]
fn test_mint_one_with_immutable() -> Result<()> {
let mut ctx = TestContext::new()?;
let temp_dir = ctx.create_temp_dir("mint-immutable");
let nft_json = temp_dir.join("test_nft.json");
ctx.create_test_nft_json(&nft_json)?;
let nft_json_str = nft_json.to_string_lossy().to_string();
let output = ctx.run_metaboss(&[
"mint",
"one",
"-d",
&nft_json_str,
"-k",
&ctx.keypair_path,
"--immutable",
]);
assert_success(&output);
let raw_mint = parse_mint_from_output(&output.stdout);
let mint = strip_debug_quotes(&raw_mint);
let metadata = decode_onchain_metadata(&ctx, &mint)?;
assert!(
!metadata.is_mutable,
"metadata should be immutable when minted with --immutable flag"
);
Ok(())
}
#[test]
#[ignore = "requires solana-test-validator (run with --ignored)"]
fn test_mint_one_with_sign() -> Result<()> {
let mut ctx = TestContext::new()?;
let temp_dir = ctx.create_temp_dir("mint-sign");
let nft_json = temp_dir.join("test_nft.json");
ctx.create_test_nft_json(&nft_json)?;
let nft_json_str = nft_json.to_string_lossy().to_string();
let output = ctx.run_metaboss(&[
"mint",
"one",
"-d",
&nft_json_str,
"-k",
&ctx.keypair_path,
"--sign",
]);
assert_success(&output);
let raw_mint = parse_mint_from_output(&output.stdout);
let mint = strip_debug_quotes(&raw_mint);
let metadata = decode_onchain_metadata(&ctx, &mint)?;
let creators = metadata.creators.expect("creators should be present");
let creator = creators
.iter()
.find(|c| c.address == ctx.keypair.pubkey())
.expect("test keypair should be listed as a creator");
assert!(
creator.verified,
"creator should be verified when minted with --sign flag"
);
Ok(())
}
#[test]
#[ignore = "requires solana-test-validator (run with --ignored)"]
fn test_create_metadata() -> Result<()> {
let mut ctx = TestContext::new()?;
let temp_dir = ctx.create_temp_dir("create-metadata");
let pubkey_str = ctx.keypair.pubkey().to_string();
let create_token_output = Command::new("spl-token")
.args([
"create-token",
"--url",
&ctx.rpc_url,
"--fee-payer",
&ctx.keypair_path,
"--mint-authority",
&pubkey_str,
])
.output()
.expect("Failed to run spl-token create-token");
assert!(
create_token_output.status.success(),
"spl-token create-token failed: {}",
String::from_utf8_lossy(&create_token_output.stderr)
);
let stdout = String::from_utf8_lossy(&create_token_output.stdout);
let mint_re = Regex::new(r"Creating token (\S+)").expect("invalid regex");
let mint_address = mint_re
.captures(&stdout)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
.expect("Could not parse mint address from spl-token output");
let metadata_json_path = temp_dir.join("metadata.json");
{
let metadata = serde_json::json!({
"name": "Test Fungible",
"symbol": "TFNG",
"uri": "https://arweave.net/FPGAv1XnyZidnqquOdEbSY6_ES735ckcDTdaAtI7GFw"
});
let mut f = std::fs::File::create(&metadata_json_path)?;
f.write_all(serde_json::to_string_pretty(&metadata)?.as_bytes())?;
}
let metadata_json_str = metadata_json_path.to_string_lossy().to_string();
let output = ctx.run_metaboss(&[
"create",
"metadata",
"-k",
&ctx.keypair_path,
"-a",
&mint_address,
"-m",
&metadata_json_str,
]);
assert_success(&output);
let metadata = decode_onchain_metadata(&ctx, &mint_address)?;
assert_eq!(
trim_null(&metadata.name),
"Test Fungible",
"metadata name should match"
);
assert_eq!(
trim_null(&metadata.symbol),
"TFNG",
"metadata symbol should match"
);
assert!(
trim_null(&metadata.uri).contains("arweave.net"),
"metadata uri should contain the expected domain"
);
Ok(())
}
#[test]
#[ignore = "requires solana-test-validator (run with --ignored)"]
fn test_mint_asset() -> Result<()> {
let mut ctx = TestContext::new()?;
let temp_dir = ctx.create_temp_dir("mint-asset");
let creator_address = ctx.keypair.pubkey().to_string();
let asset_data_path = temp_dir.join("asset_data.json");
{
let asset_data = serde_json::json!({
"name": "Test Core Asset",
"symbol": "TCA",
"uri": "https://example.com/asset.json",
"seller_fee_basis_points": 500,
"creators": [
{
"address": creator_address,
"verified": false,
"share": 100
}
],
"primary_sale_happened": false,
"is_mutable": true,
"token_standard": "NonFungible",
"collection": null,
"uses": null,
"collection_details": null,
"rule_set": null
});
let mut f = std::fs::File::create(&asset_data_path)?;
f.write_all(serde_json::to_string_pretty(&asset_data)?.as_bytes())?;
}
let asset_data_str = asset_data_path.to_string_lossy().to_string();
let output = ctx.run_metaboss(&[
"mint",
"asset",
"-d",
&asset_data_str,
"-k",
&ctx.keypair_path,
"--max-print-edition-supply",
"0",
]);
assert_success(&output);
let re = Regex::new(r"Minted asset: (\S+)").expect("invalid regex");
let raw_mint = re
.captures(&output.stdout)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
.expect("Could not find 'Minted asset: <pubkey>' in output");
let mint = strip_debug_quotes(&raw_mint);
let metadata = decode_onchain_metadata(&ctx, &mint)?;
assert_eq!(
trim_null(&metadata.name),
"Test Core Asset",
"asset metadata name should match"
);
assert_eq!(
trim_null(&metadata.symbol),
"TCA",
"asset metadata symbol should match"
);
assert!(
trim_null(&metadata.uri).contains("example.com/asset.json"),
"asset metadata uri should contain the expected URL"
);
assert!(
Pubkey::from_str(&mint).is_ok(),
"minted asset address should be a valid pubkey"
);
Ok(())
}