Advanced use cases
We strongly recommend following this tutorial if you plan to support an Edition product with any of the following configurations:
Price set using an ERC-20 token
As discussed in Transaction Steps, purchasing a product might require more than one on-chain transaction. We recommend executing each transaction explicitly (e.g., via button clicks).
This tutorial demonstrates how to handle the above using the SDK.
The repo ships with a full example at examples/step-by-step-mint, showing how to implement minting with a Blind Mint product.
Install dependencies
cd examples/step-by-step-mint
npm installConfigure environment – Copy .env.example to .env and set the following:
NEXT_PUBLIC_INSTANCE_ID= # You blind mint instance ID from Manifold Studio
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # Get one at https://dashboard.reown.com/sign-inRun locally
npm run devKey implementation steps
Make sure you render ConnectButton on the page. This handles the user’s wallet connection, which is required to create an Account that the SDK uses to perform checks and execute transactions later.
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
export default function Home() {
return (
<main>
<div>
<ConnectButton />
</div>
</main>
);
}Implement the MintButton.tsx
On button click, initialize the client, fetch the product, and call preparePurchase to get the steps.
This is handled within handlePreparePurchase.
a. Create a Manifold Client
import { createClient, createPublicProviderWagmi } from '@manifoldxyz/client-sdk';
import { useConfig } from 'wagmi';
// Get Wagmi config from React context
const config = useConfig();
// Create public provider and client
const publicProvider = createPublicProviderWagmi({ config });
const client = createClient({ publicProvider });b. Fetch the product and validate the type
const product = await client.getProduct(INSTANCE_ID);
if (!isBlindMintProduct(product)) {
throw new Error('Is not a blind mint instance')
}c. Check the product status to ensure it’s still active
const productStatus = await product.getStatus();
if (productStatus !== 'active') {
throw new Error(`Product is ${productStatus}`);
}e. Prepare the purchase by specifying the purchase amount and capture the returned PreparedPurchase
const preparedPurchase = await product.preparePurchase({
address: address,
payload: {
quantity
}
});
setPreparedPurchase(prepared)Implement a function responsible for executing an individual step. This is handled in handleExecuteStep
a. Create an Account representing the connected user.
import { createAccountViem } from '@manifoldxyz/client-sdk'
export default function MintButton({ instanceId, quantity = 1 }: MintButtonButtonProps) {
const { data: walletClient } = useWalletClient()
//...other codes
const handlePreparePurchase = async () => {
const account = createAccountViem({
walletClient: walletClient
})
//...other codes
}
}
b. Execute the step by calling the execute function on each step
import { createAccountViem } from '@manifoldxyz/client-sdk'
export default function MintButton({ instanceId, quantity = 1 }: MintButtonButtonProps) {
const { data: walletClient } = useWalletClient()
//...other codes
const handlePreparePurchase = async (stepIndex: number) => {
//...other codes
const step = preparedPurchase.steps[stepIndex]
if (!step) {
setError('Invalid step index')
return
}
try {
const result = await step.execute(account)
catch (error) {
setError(err.message || 'Transaction failed')
}
//...other codes
}
}
c. Render the StepModal
{preparedPurchase && (
<StepModal
isOpen={isModalOpen}
onClose={handleCloseModal}
steps={preparedPurchase.steps}
onExecuteStep={handleExecuteStep}
currentStepIndex={currentStepIndex}
stepStatuses={stepStatuses}
totalCost={BigInt(preparedPurchase.cost.total.native.value.toString())}
/>
)}Putting it all together
'use client'
import { useState } from 'react'
import { useAccount, useConfig } from 'wagmi'
import { createClient, BlindMintProduct, PreparedPurchase, createAccountViem, createPublicProviderWagmi } from '@manifoldxyz/client-sdk'
import { useWalletClient } from 'wagmi'
import StepModal from './StepModal'
interface MintButtonButtonProps {
instanceId: string
quantity?: number
}
export default function MintButton({ instanceId, quantity = 1 }: MintButtonButtonProps) {
const { address, isConnected } = useAccount()
const { data: walletClient } = useWalletClient()
const config = useConfig()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [product, setProduct] = useState<BlindMintProduct | null>(null)
const [preparedPurchase, setPreparedPurchase] = useState<PreparedPurchase | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [stepStatuses, setStepStatuses] = useState<string[]>([])
const [purchaseComplete, setPurchaseComplete] = useState(false)
const handlePreparePurchase = async () => {
if (!address || !walletClient) {
setError('Please connect your wallet first')
return
}
setIsLoading(true)
setError(null)
setPurchaseComplete(false)
try {
// Create client with Wagmi public provider
const publicProvider = createPublicProviderWagmi({ config });
const client = createClient({ publicProvider });
const fetchedProduct = await client.getProduct(instanceId) as BlindMintProduct
setProduct(fetchedProduct)
const status = await fetchedProduct.getStatus()
if (status !== 'active') {
throw new Error(`Product is ${status}`)
}
const allocations = await fetchedProduct.getAllocations({ recipientAddress: address })
if (!allocations.isEligible) {
throw new Error(allocations.reason || 'Not eligible to mint')
}
const prepared = await fetchedProduct.preparePurchase({
address,
payload: { quantity }
})
setPreparedPurchase(prepared)
setStepStatuses(new Array(prepared.steps.length).fill('idle'))
setCurrentStepIndex(0)
setIsModalOpen(true)
} catch (err) {
console.error('Error preparing purchase:', err)
setError(err instanceof Error ? err.message : 'Failed to prepare purchase')
} finally {
setIsLoading(false)
}
}
const handleExecuteStep = async (stepIndex: number) => {
if (!walletClient || !preparedPurchase || !product) {
setError('Missing required data')
return
}
const step = preparedPurchase.steps[stepIndex]
if (!step) {
setError('Invalid step index')
return
}
setStepStatuses(prev => {
const newStatuses = [...prev]
newStatuses[stepIndex] = 'executing'
return newStatuses
})
try {
const account = createAccountViem({
walletClient: walletClient as any
})
const result = await step.execute(account)
setStepStatuses(prev => {
const newStatuses = [...prev]
newStatuses[stepIndex] = 'completed'
return newStatuses
})
if (stepIndex < preparedPurchase.steps.length - 1) {
setCurrentStepIndex(stepIndex + 1)
} else {
setPurchaseComplete(true)
setTimeout(() => {
setIsModalOpen(false)
setCurrentStepIndex(0)
setStepStatuses([])
}, 2000)
}
console.log(`Step ${stepIndex + 1} completed:`, result)
} catch (err) {
console.error(`Error executing step ${stepIndex + 1}:`, err)
setStepStatuses(prev => {
const newStatuses = [...prev]
newStatuses[stepIndex] = 'failed'
return newStatuses
})
setError(err instanceof Error ? err.message : 'Transaction failed')
}
}
const handleCloseModal = () => {
if (!stepStatuses.some(status => status === 'executing')) {
setIsModalOpen(false)
setPreparedPurchase(null)
setCurrentStepIndex(0)
setStepStatuses([])
}
}
return (
<>
<div className="flex flex-col items-center gap-4">
{!isConnected ? (
<p className="text-gray-600">Please connect your wallet to mint</p>
) : (
<button
onClick={handlePreparePurchase}
disabled={isLoading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Preparing...' : `Mint ${quantity} Blind Box${quantity > 1 ? 'es' : ''}`}
</button>
)}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
{purchaseComplete && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-green-800 text-sm font-semibold">Purchase completed successfully!</p>
</div>
)}
</div>
{preparedPurchase && (
<StepModal
isOpen={isModalOpen}
onClose={handleCloseModal}
steps={preparedPurchase.steps}
onExecuteStep={handleExecuteStep}
currentStepIndex={currentStepIndex}
stepStatuses={stepStatuses}
formattedCost={preparedPurchase.cost.total.native.formatted}
/>
)}
</>
)
}Best practices:
Validate the product type using isBlindMintProduct or isEditionProduct to ensure proper TypeScript typings.
Run getStatus before attempting a purchase to verify the product is available.
Handle ClientSDKError codes for scenarios like ineligibility, sold-out items, or insufficient funds.
See each method’s documentation for detailed error descriptions.
Last updated