diff --git a/bun.lockb b/bun.lockb index 79338419..9c9ef5cd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/next.config.mjs b/next.config.mjs index 2d4a137d..101dc105 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -4,8 +4,7 @@ const nextConfig = { reactStrictMode: true, images: { dangerouslyAllowSVG: true, - contentSecurityPolicy: - "default-src 'self'; script-src 'none'; sandbox;", + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", remotePatterns: [ { protocol: "https", diff --git a/package.json b/package.json index b2ca75bb..eb931978 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "critters": "^0.0.20", "crypto-es": "^2.1.0", "daisyui": "^4.11.1", + "decimal.js": "^10.4.3", "dompurify": "^3.0.9", "framer-motion": "^11.1.7", "isomorphic-dompurify": "^2.4.0", diff --git a/src/app/collection/generate/page.tsx b/src/app/collection/generate/page.tsx new file mode 100644 index 00000000..d7256d80 --- /dev/null +++ b/src/app/collection/generate/page.tsx @@ -0,0 +1,7 @@ +import CraftingItemGenerator from "@/components/pages/collection/generate"; + +const CollectionGenerate = async () => { + return +} + +export default CollectionGenerate; \ No newline at end of file diff --git a/src/app/holders/[type]/[id]/details/route.ts b/src/app/holders/[type]/[id]/details/route.ts index c1586b05..3784d198 100644 --- a/src/app/holders/[type]/[id]/details/route.ts +++ b/src/app/holders/[type]/[id]/details/route.ts @@ -1,5 +1,5 @@ import { API_HOST, AssetType } from "@/constants"; -import { NextRequest, NextResponse } from "next/server"; +import { type NextRequest, NextResponse } from "next/server"; interface TickHolder { address: string; diff --git a/src/app/listings/[tab]/page.tsx b/src/app/listings/[tab]/page.tsx index f8e89dda..bc0561b5 100644 --- a/src/app/listings/[tab]/page.tsx +++ b/src/app/listings/[tab]/page.tsx @@ -1,6 +1,6 @@ import ListingsPage from "@/components/pages/listings"; import { API_HOST, AssetType } from "@/constants"; -import type { BSV20TXO, OrdUtxo } from "@/types/ordinals"; +import type { BSV20TXO } from "@/types/ordinals"; import { getCapitalizedAssetType } from "@/utils/assetType"; import * as http from "@/utils/httpClient"; diff --git a/src/app/market/[tab]/new/page.tsx b/src/app/market/[tab]/new/page.tsx index 03f8ddae..d027d397 100644 --- a/src/app/market/[tab]/new/page.tsx +++ b/src/app/market/[tab]/new/page.tsx @@ -3,65 +3,6 @@ import type { AssetType } from "@/constants"; import { getCapitalizedAssetType } from "@/utils/assetType"; const Market = async ({ params }: { params: { tab: AssetType } }) => { - // switch (params.tab) { - // case AssetType.Ordinals: - // // TODO: Featured ordinals - // const urlImages = `${API_HOST}/api/market?sort=recent&dir=desc&limit=20&offset=0&type=image/png`; - // const { promise } = http.customFetch(urlImages); - // const imageListings = await promise; - // return ( - // - // ); - // case AssetType.BSV20: - // return ( - // - // ); - // case AssetType.BSV21: - // return ( - // - // ); - // case AssetType.LRC20: - // const q = { - // insc: { - // json: { - // p: "lrc-20", - // }, - // }, - // }; - - // const urlLrc20 = `${API_HOST}/api/market?sort=recent&dir=desc&limit=20&offset=0&q=${btoa( - // JSON.stringify(q) - // )}`; - // const { promise: promiseLrc20 } = http.customFetch(urlLrc20); - // const lrc20Listings = await promiseLrc20; - - // const lrc20TokenIds = lrc20Listings - // .filter((l) => !!l.origin?.data?.insc?.json?.id) - // .map((l) => l.origin?.data?.insc?.json?.id!); - - // const urlLrc20Tokens = `${API_HOST}/api/txos/outpoints`; - // const { promise: promiseLrc20Tokens } = http.customFetch( - // urlLrc20Tokens, - // { - // method: "POST", - // body: JSON.stringify(lrc20TokenIds), - // } - // ); - // const lrc20Tokens = await promiseLrc20Tokens; - - // return ( - - // ); - // default: - // return null; - // } return ; }; export default Market; diff --git a/src/components/Collections/featured.tsx b/src/components/Collections/featured.tsx index eec5cd1f..9c1f8347 100644 --- a/src/components/Collections/featured.tsx +++ b/src/components/Collections/featured.tsx @@ -48,7 +48,7 @@ const FeaturedCollections = () => {
{collection.name} { <>

Featured Collections

-

Current Hype

+

+
+ Current Hype +
+ New +

{data?.map((c) => (
@@ -38,7 +44,7 @@ const Collections = () => { {/* */} diff --git a/src/components/Footer/footer.tsx b/src/components/Footer/footer.tsx index 8e6d5928..46ac80a1 100644 --- a/src/components/Footer/footer.tsx +++ b/src/components/Footer/footer.tsx @@ -1,7 +1,6 @@ import Link from "next/link"; import { BsGpuCard } from "react-icons/bs"; import { FaBook, FaDiscord } from "react-icons/fa6"; -import Vivi from "../vivi"; const Footer = () => { const linkClass = "hover:text-yellow-500 text-yellow-400/25 transition-color duration-1000" diff --git a/src/components/artifact/json.tsx b/src/components/artifact/json.tsx index 1a7efcc4..5398e87c 100644 --- a/src/components/artifact/json.tsx +++ b/src/components/artifact/json.tsx @@ -1,6 +1,5 @@ import { FetchStatus, ORDFS } from "@/constants"; import type { BSV20 } from "@/types/bsv20"; -import type { LRC20 } from "@/types/ordinals"; import type React from "react"; import { useEffect, useState } from "react"; import { LoaderIcon } from "react-hot-toast"; @@ -26,7 +25,6 @@ const JsonArtifact: React.FC = ({ }) => { const [json, setJson] = useState(j); const [bsv20, setBsv20] = useState | undefined>(); - const [lrc20, setLrc20] = useState | undefined>(undefined); const [fetchTextStatus, setFetchTextStatus] = useState( FetchStatus.Idle ); @@ -45,12 +43,10 @@ const JsonArtifact: React.FC = ({ setFetchTextStatus(FetchStatus.Success); setJson(resultText); - if (type === ArtifactType.LRC20 || type === ArtifactType.BSV20) { + if (type === ArtifactType.BSV20) { const txJson = artifact.origin; setFetchBsv20Status(FetchStatus.Success); - if (type === ArtifactType.LRC20) { - setLrc20(txJson); - } else if (type === ArtifactType.BSV20) { + if (type === ArtifactType.BSV20) { setBsv20(txJson); } } @@ -85,9 +81,8 @@ const JsonArtifact: React.FC = ({
{!mini && (
           {JSON.stringify(json, null, 2)}
         
@@ -97,25 +92,6 @@ const JsonArtifact: React.FC = ({
)} - {/* {!mini && - type === ArtifactType.BSV20 && - bsv20 && - bsv20.status !== Bsv20Status.Valid && ( -
- {" "} - {`${ - bsv20.status === Bsv20Status.Pending - ? "PENDING VALIDATION" - : "INVALID BSV20" - }`} -
- )} */}
) : ( diff --git a/src/components/marketMenu/index.tsx b/src/components/marketMenu/index.tsx index daef6d42..278801fd 100644 --- a/src/components/marketMenu/index.tsx +++ b/src/components/marketMenu/index.tsx @@ -65,6 +65,17 @@ const MarketMenu: React.FC = () => {
NFT
+
  • + +
    Inscribe
    +
    NFT / FT
    + +
  • + +
    Token Market
  • { Listing Price{" "}
    diff --git a/src/components/pages/collection/Traits.tsx b/src/components/pages/collection/Traits.tsx index a7de48a6..6aa8e144 100644 --- a/src/components/pages/collection/Traits.tsx +++ b/src/components/pages/collection/Traits.tsx @@ -1,10 +1,10 @@ "use client"; import { useEffect, useState } from "react"; -import type { CollectionSubTypeData, MAP } from "js-1sat-ord"; +import type { CollectionSubTypeData, CollectionTraits, CreateOrdinalsCollectionMetadata, MAP, PreMAP } from "js-1sat-ord"; interface TraitsProps { - collection: Collection; + collection: PreMAP; } export type Collection = MAP & { diff --git a/src/components/pages/collection/generate.tsx b/src/components/pages/collection/generate.tsx new file mode 100644 index 00000000..9ae1f0e6 --- /dev/null +++ b/src/components/pages/collection/generate.tsx @@ -0,0 +1,328 @@ + +"use client" + +import type React from 'react'; +import { useState } from 'react'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; +import { Trash2, Copy } from 'lucide-react'; +import type { CollectionItemSubTypeData, CollectionItemTrait, CollectionSubTypeData } from 'js-1sat-ord'; + +type CraftingItem = { + id: string; + name: string; + mintNumber: number; + rarity: string; + traits: CollectionItemTrait[]; + recipe?: string[]; + isCraftingMaterial: boolean; + prompt: string; +}; + +// Helper functions +const generateId = () => Math.random().toString(36).substr(2, 9); + +const getWeightedRandomElement = (values: string[], percentages: string[]): string => { + const random = Math.random() * 100; + let sum = 0; + for (let i = 0; i < values.length; i++) { + sum += Number.parseFloat(percentages[i]); + if (random <= sum) return values[i]; + } + return values[values.length - 1]; +}; + +// Descriptions mapping for prompt generation +const descriptionsMap: { [key: string]: { [key: string]: string } } = { + itemType: { + "Crafting Item": "a valuable crafting material", + "Consumable": "a consumable item", + "Tool": "a useful tool", + "Potion": "a magical potion" + }, + category: { + "Ingredient": "raw ingredient", + "Spell": "magical spell scroll", + "Food": "edible item", + "Drink": "drinkable concoction", + "Utility": "utility item" + }, + quality: { + "Crude": "roughly made", + "Standard": "well-crafted", + "Fine": "finely crafted", + "Exquisite": "exquisitely crafted", + "Masterwork": "masterfully crafted" + }, + rarity: { + "common": "ordinary", + "rare": "uncommon", + "epic": "legendary", + "legendary": "mythical" + } +}; + +// Name generation components +const adjectives = ["Ancient", "Mystic", "Enchanted", "Radiant", "Shadowy", "Celestial", "Ethereal", "Arcane", "Pristine", "Runic"]; +const nouns = { + "Crafting Item": ["Essence", "Crystal", "Shard", "Ingot", "Dust", "Fragment", "Nugget"], + "Consumable": ["Elixir", "Tincture", "Brew", "Concoction", "Mixture", "Infusion"], + "Tool": ["Hammer", "Chisel", "Anvil", "Mortar", "Pestle", "Cauldron", "Furnace"], + "Potion": ["Philter", "Draught", "Remedy", "Tonic", "Serum", "Solution"] +}; + +// Collection generation +const generateCraftingItemsData = (): CollectionSubTypeData => ({ + description: "A unique collection of crafting items and tools", + quantity: 10, + rarityLabels: [ + { common: "60%" }, + { rare: "25%" }, + { epic: "10%" }, + { legendary: "5%" } + ], + traits: { + itemType: { + values: ["Crafting Item", "Consumable", "Tool", "Potion"], + occurancePercentages: ["40", "30", "20", "10"] + }, + category: { + values: ["Ingredient", "Spell", "Food", "Drink", "Utility"], + occurancePercentages: ["30", "20", "20", "15", "15"] + }, + quality: { + values: ["Crude", "Standard", "Fine", "Exquisite", "Masterwork"], + occurancePercentages: ["20", "30", "25", "15", "10"] + } + } +}); + +// Function to generate name +const generateName = (itemType: string): string => { + const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; + const noun = nouns[itemType as keyof typeof nouns][Math.floor(Math.random() * nouns[itemType as keyof typeof nouns].length)]; + return `${adjective} ${noun}`; +}; + +// Function to generate prompt +const generatePrompt = (item: CraftingItem): string => { + const itemTypeDesc = descriptionsMap.itemType[item.traits.find(t => t.name === 'itemType')?.value || '']; + const categoryDesc = descriptionsMap.category[item.traits.find(t => t.name === 'category')?.value || '']; + const qualityDesc = descriptionsMap.quality[item.traits.find(t => t.name === 'quality')?.value || '']; + const rarityDesc = descriptionsMap.rarity[item.rarity]; + + let prompt = `${item.name}: A ${rarityDesc} ${qualityDesc} ${itemTypeDesc}: ${categoryDesc}`; + + if (item.isCraftingMaterial) { + prompt += ". This item is a crafting material, appearing as a valuable resource."; + } else { + prompt += ". The item appears as a fully crafted and usable object."; + } + + return prompt; +}; + +// Item generation +const generateItem = (data: CollectionSubTypeData, index: number, forcedRarity?: string): CraftingItem => { + const rarity = forcedRarity || getWeightedRandomElement( + data.rarityLabels.map(label => Object.keys(label)[0]), + data.rarityLabels.map(label => Object.values(label)[0].replace('%', '')) + ); + + const traits: CollectionItemTrait[] = []; + for (const [traitName, traitData] of Object.entries(data.traits)) { + const value = getWeightedRandomElement(traitData.values, traitData.occurancePercentages); + traits.push({ + name: traitName, + value: value, + rarityLabel: rarity, + occurancePercentrage: traitData.occurancePercentages[traitData.values.indexOf(value)] + }); + } + + const itemType = traits.find(t => t.name === 'itemType')?.value || 'Crafting Item'; + const name = generateName(itemType); + + const item: CraftingItem = { + id: generateId(), + name, + mintNumber: index + 1, + rarity, + traits, + isCraftingMaterial: itemType === 'Crafting Item', + prompt: "" + }; + + // Generate recipe for craftable items (except for legendary items) + if (rarity !== 'legendary' && !item.isCraftingMaterial && Math.random() > 0.5) { + item.recipe = generateRecipe(item); + } + + // Generate prompt + item.prompt = generatePrompt(item); + + return item; +}; + +const generateRecipe = (item: CraftingItem): string[] => { + const baseIngredients = [ + "Clover", "Iron Ore", "Oak Log", "Lemon", "Wheat", "Corn", "Cherry", + "White Sand", "Rain Water", "Coal Ore", "Quartz", "Moss", "Salt Water" + ]; + const recipe = []; + const ingredientCount = Math.floor(Math.random() * 3) + 2; // 2-4 ingredients + for (let i = 0; i < ingredientCount; i++) { + recipe.push(baseIngredients[Math.floor(Math.random() * baseIngredients.length)]); + } + return recipe; +}; + +// Updated function to format item as CollectionItemSubTypeData +const formatAsCollectionItem = (item: CraftingItem): CollectionItemSubTypeData => { + return { + collectionId: item.id, + mintNumber: item.mintNumber, + rank: Math.floor(Math.random() * 1000) + 1, // Random rank for demonstration + rarityLabel: [{ [item.rarity]: "100%" }], + traits: item.traits, + attachments: [{ + name: "Image Prompt", + description: "AI image generation prompt", + "content-type": "text/plain", + url: `data:text/plain,${encodeURIComponent(item.prompt)}` + }] + }; +}; + +// Function to copy item to clipboard +const copyToClipboard = (item: CraftingItem) => { + const collectionItemString = JSON.stringify(formatAsCollectionItem(item), null, 2); + navigator.clipboard.writeText(collectionItemString) + .then(() => alert('Item copied to clipboard!')) + .catch(err => console.error('Failed to copy item: ', err)); +}; + +const CraftingItemGenerator: React.FC = () => { + const [itemsData, setItemsData] = useState(null); + const [items, setItems] = useState([]); + const [quantity, setQuantity] = useState(10); + + const generateNewCollection = () => { + const newItemsData = generateCraftingItemsData(); + newItemsData.quantity = quantity; + setItemsData(newItemsData); + + const newItems: CraftingItem[] = []; + + // Generate the user-specified quantity of items + for (let i = 0; i < quantity; i++) { + newItems.push(generateItem(newItemsData, i)); + } + + setItems(newItems); + }; + + const addItem = () => { + if (itemsData) { + const newItem = generateItem(itemsData, items.length); + setItems([...items, newItem]); + } + }; + + const removeItem = (id: string) => { + const newItems = items.filter(item => item.id !== id); + setItems(newItems); + }; + + const rarityData = items.reduce((acc, item) => { + acc[item.rarity] = (acc[item.rarity] || 0) + 1; + return acc; + }, {} as Record); + + const chartData = Object.entries(rarityData).map(([name, value]) => ({ name, value })); + + return ( +
    +

    Crafting Item Generator

    + +
    + setQuantity(Number.parseInt(e.target.value))} + placeholder="Number of items" + className="mr-2 p-2 border rounded" + /> + +
    + + {itemsData && ( +
    +

    Crafting Items Data

    +
    {JSON.stringify(itemsData, null, 2)}
    +
    + )} + + {items.length > 0 && ( +
    +

    Generated Items (Total: {items.length})

    + +
    + + + + + + + + + + + + + + + + + {items.map((item) => ( + + + + + + + + + + + + + ))} + +
    Mint NumberNameRarityItem TypeCategoryQualityCrafting MaterialRecipePromptActions
    {item.mintNumber}{item.name}{item.rarity}{item.traits.find(t => t.name === 'itemType')?.value}{item.traits.find(t => t.name === 'category')?.value}{item.traits.find(t => t.name === 'quality')?.value}{item.isCraftingMaterial ? 'Yes' : 'No'}{item.recipe ? item.recipe.join(', ') : 'N/A'}{item.prompt} + + +
    +
    + +

    Rarity Distribution

    +
    + + + + + + + + +
    +
    + )} +
    + ); +}; + +export default CraftingItemGenerator; \ No newline at end of file diff --git a/src/components/pages/collection/index.tsx b/src/components/pages/collection/index.tsx index 64423cb2..ae31f57f 100644 --- a/src/components/pages/collection/index.tsx +++ b/src/components/pages/collection/index.tsx @@ -6,7 +6,8 @@ import Link from "next/link"; import { FaChevronLeft } from "react-icons/fa"; import { CollectionList } from "./CollectionList"; import { CollectionNavigation } from "./CollectionNavigation"; -import Traits, { type Collection } from "./Traits"; +import Traits from "./Traits"; +import type { CreateOrdinalsCollectionMetadata, PreMAP } from "js-1sat-ord"; interface Props { stats: CollectionStats; @@ -56,7 +57,7 @@ const CollectionPage = async ({ stats, collection, bannerImage }: Props) => { )} - {collection.origin?.data?.map && } + {collection.origin?.data?.map && }
  • ); diff --git a/src/components/pages/inscribe/bsv21.tsx b/src/components/pages/inscribe/bsv21.tsx index ca74ca84..4daa1356 100644 --- a/src/components/pages/inscribe/bsv21.tsx +++ b/src/components/pages/inscribe/bsv21.tsx @@ -1,7 +1,7 @@ "use client"; import Artifact from "@/components/artifact"; -import { FetchStatus, toastErrorProps } from "@/constants"; +import { FetchStatus, knownImageTypes, toastErrorProps } from "@/constants"; import { chainInfo, indexers, @@ -31,7 +31,6 @@ import toast from "react-hot-toast"; import { IoMdWarning } from "react-icons/io"; import { RiSettings2Fill } from "react-icons/ri"; import { IconWithFallback } from "../TokenMarket/heading"; -import { knownImageTypes } from "./image"; import type { InscriptionTab } from "./tabs"; const top10 = ["FREN", "LOVE", "TRMP", "GOLD", "TOPG", "CAAL"]; diff --git a/src/components/pages/inscribe/collection.tsx b/src/components/pages/inscribe/collection.tsx deleted file mode 100644 index 8b396867..00000000 --- a/src/components/pages/inscribe/collection.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client" - -import { useSignals } from "@preact/signals-react/runtime"; -import type React from "react"; - -interface InscribeCollectionProps { - inscribedCallback: () => void; -} - -const InscribeCollection: React.FC = ({ inscribedCallback }) => { - useSignals(); - return
    ; -}; - -export default InscribeCollection; diff --git a/src/components/pages/inscribe/collection/index.tsx b/src/components/pages/inscribe/collection/index.tsx new file mode 100644 index 00000000..bb0f1f2c --- /dev/null +++ b/src/components/pages/inscribe/collection/index.tsx @@ -0,0 +1,494 @@ +// collection/index.tsx +"use client"; + +import Artifact from "@/components/artifact"; +import { knownImageTypes, marketAddress, toastErrorProps } from "@/constants"; +import { ordPk, payPk } from "@/signals/wallet"; +import { fundingAddress, ordAddress } from "@/signals/wallet/address"; +import type { FileEvent } from "@/types/file"; +import type { TxoData } from "@/types/ordinals"; +import { getUtxos } from "@/utils/address"; +import { useSignals } from "@preact/signals-react/runtime"; +import mime from "mime"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { IconWithFallback } from "../../TokenMarket/heading"; +import TraitsForm, { validateTraits } from "./traitsForm"; +import RarityLabelForm, { validateRarities } from "./rarityLabelForm"; +import RoyaltyForm, { validateRoyalties } from "./royaltyForm"; +import { + type CollectionSubTypeData, + type CreateOrdinalsCollectionMetadata, + type CreateOrdinalsCollectionConfig, + type Destination, + type CollectionTraits, + type Royalty, + type RarityLabels, + createOrdinals, + validateSubTypeData, + RoytaltyType, +} from "js-1sat-ord"; +import { PrivateKey } from "@bsv/sdk"; +import type { PendingTransaction } from "@/types/preview"; +import { useLocalStorage } from "@/utils/storage"; +import { setPendingTxs } from "@/signals/wallet/client"; + +interface InscribeCollectionProps { + inscribedCallback: () => void; +} + +const InscribeCollection: React.FC = ({ + inscribedCallback, +}) => { + useSignals(); + + const [collectionName, setCollectionName] = useLocalStorage("1sc-cn", ""); + const [collectionDescription, setCollectionDescription] = useLocalStorage("1sc-cd", ""); + const [collectionQuantity, setCollectionQuantity] = useLocalStorage("1sc-cq", undefined); + const [collectionRarities, setCollectionRarities] = useLocalStorage("1sc-rl", + [], + ); + const [collectionTraits, setCollectionTraits] = useLocalStorage("1sc-ct", undefined); + const [collectionCoverImage, setCollectionCoverImage] = useState( + null, + ); + const [collectionRoyalties, setCollectionRoyalties] = useLocalStorage("1sc-rlt", [defaultRoyalty]); + const [preview, setPreview] = useLocalStorage< + string | ArrayBuffer | undefined + >("1sc-preview", undefined); + + const [isImage, setIsImage] = useLocalStorage("1sc-isc", false); + const [mintError, setMintError] = useState(); + const [iconFileName, setIconFileName] = useLocalStorage("1sc-ifn", "icon"); + const fileInputRef = useRef(null); + + useEffect(() => { + if (preview && typeof preview === "string") { + const img = new Image(); + img.onload = () => { + setIsImage(true); + const file = dataURLtoFile(preview, iconFileName || "icon"); + setCollectionCoverImage(file); + + // Set the filename using the ref + if (fileInputRef.current) { + // Create a DataTransfer object and add the file + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + // Set the files property of the input element + fileInputRef.current.files = dataTransfer.files; + } + }; + img.src = preview; + } + }, [iconFileName, preview, setIsImage]); + + const validateForm = useCallback(() => { + if (collectionTraits) { + const traitError = validateTraits(collectionTraits); + if (traitError) { + toast.error(traitError, toastErrorProps); + throw new Error(traitError); + } + } + + const rarityError = validateRarities(collectionRarities || [], collectionQuantity); + if (rarityError) { + toast.error(rarityError, toastErrorProps); + throw new Error(rarityError); + } + + const royaltyError = validateRoyalties(collectionRoyalties || []); + if (royaltyError) { + toast.error(royaltyError, toastErrorProps); + throw new Error(royaltyError); + } + + if (!collectionName) { + toast.error("Name is required", toastErrorProps); + throw new Error("Name is required"); + } + }, [collectionName, collectionQuantity, collectionRarities, collectionRoyalties, collectionTraits]); + + const resetForm = useCallback(() => { + setCollectionCoverImage(null); + setCollectionDescription(""); + setCollectionName(""); + setCollectionQuantity(undefined); + setCollectionRarities([]); + setCollectionRoyalties([]); + setCollectionTraits(undefined); + setMintError(undefined); + setPreview(undefined); + setIsImage(false); + }, [ + setCollectionCoverImage, + setCollectionDescription, + setCollectionName, + setCollectionQuantity, + setCollectionRarities, + setCollectionRoyalties, + setCollectionTraits, + setMintError, + setPreview, + setIsImage, + ]); + + const inscribeCollection = useCallback(async () => { + if ( + !payPk.value || + !ordPk.value || + !ordAddress.value || + !fundingAddress.value + ) { + toast.error("Wallet not ready", toastErrorProps); + return; + } + + if (!collectionCoverImage) { + toast.error("Please upload a cover image", toastErrorProps); + return; + } + + + + try { + validateForm(); + } catch (e) { + console.error(e); + return; + } + + const utxos = await getUtxos(fundingAddress.value); + + const metaData = { + app: "1sat.market", + type: "ord", + subType: "collection", + name: collectionName, + } as CreateOrdinalsCollectionMetadata; + + if (collectionRoyalties && collectionRoyalties.length > 0) { + metaData.royalties = collectionRoyalties; + } + + const subTypeData: Partial = { + quantity: collectionQuantity, + description: collectionDescription, + }; + + if (!collectionQuantity) { + subTypeData.quantity = undefined; + } + + if (collectionRarities && collectionRarities.length > 0) { + subTypeData.rarityLabels = collectionRarities; + } + + if (collectionTraits) { + subTypeData.traits = collectionTraits; + } + + metaData.subTypeData = subTypeData as CollectionSubTypeData; + + let file: File | undefined; + if (collectionCoverImage.type === "") { + const newType = mime.getType(collectionCoverImage.name); + if (newType !== null) { + file = new File([collectionCoverImage], collectionCoverImage.name, { + type: newType, + }); + } + } + if (!file) { + file = collectionCoverImage; + } + + const dataB64 = Buffer.from(await file.arrayBuffer()).toString("base64"); + + const destinations: Destination[] = [ + { + address: ordAddress.value, + inscription: { + dataB64, + contentType: "application/json", + }, + }, + ]; + + const error = validateSubTypeData("collection", metaData.subTypeData); + if (error) { + console.error(error); + return; + } + + // const pendingTx = await inscribeFile(u, file, metadata, ordPk.value); + const config: CreateOrdinalsCollectionConfig = { + destinations, + paymentPk: PrivateKey.fromWif(payPk.value), + utxos, + metaData, + }; + const { tx, spentOutpoints, payChange } = await createOrdinals(config); + console.log("TX", tx.toHex()); + if (tx) { + setPendingTxs([ + { + txid: tx.id("hex"), + rawTx: tx.toHex(), + fee: tx.getFee(), + numInputs: tx.inputs.length, + numOutputs: tx.outputs.length, + inputTxid: tx.inputs[0].sourceTXID, + contentType: file.type, + metadata: metaData, + size: tx.toBinary().length, + returnTo: "/inscribe", + spentOutpoints, + payChange, + } as PendingTransaction, + ]) + resetForm(); + inscribedCallback(); + } + }, [collectionCoverImage, collectionDescription, collectionName, collectionQuantity, collectionRarities, collectionRoyalties, collectionTraits, fundingAddress.value, inscribedCallback, ordAddress.value, ordPk.value, payPk.value, resetForm, validateForm]); + + // (e) => setCollectionCoverImage(e.target.files?.[0] || null)} + + const changeFile = useCallback( + async (e: FileEvent) => { + // TODO: This reads the file twice which is pretty inefficient + // would be nice to get dimensions and ArrayBuffer for preview in one go + + const file = e.target.files[0] as File; + + // make sure the width and height are identical + const img = new Image(); + img.onload = () => { + if (img.width !== img.height) { + toast.error("Image must be square", toastErrorProps); + setCollectionCoverImage(null); + setPreview(undefined); + setIsImage(false); + setMintError("Image must be square"); + return; + } + // max size is 400px + if (img.width > 2048) { + toast.error("Width must be 2048px or less", toastErrorProps); + setCollectionCoverImage(null); + setPreview(undefined); + setIsImage(false); + setMintError("Width must be 2048px or less"); + return; + } + if (file.size > 1024 * 1024) { + toast.error("Image must be less than 1MB", toastErrorProps); + setCollectionCoverImage(null); + setPreview(undefined); + setIsImage(false); + setMintError("Image must be less than 1MB"); + return; + } + setMintError(undefined); + setCollectionCoverImage(file); + setIconFileName(file.name); + if (knownImageTypes.includes(file.type)) { + setIsImage(true); + } + const reader = new FileReader(); + + reader.onloadend = () => { + setPreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + img.src = URL.createObjectURL(file); + }, + [setIconFileName, setIsImage, setPreview], + ); + + const artifact = useMemo(async () => { + return ( + collectionCoverImage?.type && + preview && ( + + ) + ); + }, [preview, collectionCoverImage]); + + return ( +
    +

    Inscribe Collection

    +

    + Creating a new collection inscription enables minting images to this + collection. Member items are created later. +

    + +
    + +
    +
    + {(!collectionCoverImage || !preview) && ( +
    + +
    + )} + {collectionCoverImage && preview && isImage && artifact} + {collectionCoverImage && !isImage && ( +
    + X +
    + )} +
    + +
    + +
    + + setCollectionName(e.target.value)} + required + /> +
    + +
    + +