moonshot-blindbox-sdk
v1.1.14
Published
Official SDK for Moonshot Blindbox Smart Contract - A Solana-based blind box platform
Maintainers
Readme
MoonshotBlindbox SDK - How to Use
This guide demonstrates how to use the MoonshotBlindbox SDK to interact with the Moonshot Blindbox Smart Contract on Solana.
Table of Contents
- Installation & Setup
- Basic Usage
- Account Management
- Buy Operations
- Transaction Patterns
- Event Parsing
- Error Handling
- Complete Examples
- Best Practices
Installation & Setup
npm install moonshot-blindbox-sdkBackend Scripts
import { MoonshotBlindbox } from "moonshot-blindbox-sdk";
import { generateKeyPairSigner } from "@solana/kit";
// Initialize SDK with your signer and RPC endpoints
const signer = await generateKeyPairSigner();
const moonshot = new MoonshotBlindbox(
signer,
"https://api.devnet.solana.com", // RPC URL
"wss://api.devnet.solana.com", // WebSocket URL
true // skipSimulation (optional, defaults to false)
);Frontend Integration
import { MoonshotBlindbox } from "moonshot-blindbox-sdk";
import {
useWalletAccountTransactionSendingSigner,
useWallets,
} from "@solana/react";
const wallets = useWallets();
// TODO: Have to implement the logic to handle the wallet selection of users
const transactionSendingSigner = useWalletAccountTransactionSendingSigner(
wallets[0],
"solana:devnet" // TODO: Detect the network
);
const moonshot = new MoonshotBlindbox(
transactionSendingSigner,
"https://api.devnet.solana.com", // RPC URL
"wss://api.devnet.solana.com", // WebSocket URL
true // skipSimulation (optional, defaults to false)
);Basic Usage
1. Initialize a Config
// Create a new config with ID 1
const configId = 1;
const initializeConfigIx = await moonshot.getInitializeConfigInstruction({
id: configId,
});
await moonshot.signAndSendTransaction([initializeConfigIx]);
console.log("Config initialized successfully");2. Grant Roles
import { Role } from "moonshot-blindbox-sdk";
// Grant Admin role to an account
const adminAddress = "YourAdminAddressHere";
const grantAdminIx = await moonshot.getGrantRoleInstruction({
configId,
accountToGrant: adminAddress,
role: Role.Admin,
});
await moonshot.signAndSendTransaction([grantAdminIx]);
console.log("Admin role granted");3. Create a Campaign
const campaignId = 0;
const now = Math.floor(Date.now() / 1000);
// Define stages with start/end times and purchase limits
const stages = [
{
startTime: BigInt(now),
endTime: BigInt(now + 600),
maxItemsPerUser: BigInt(2),
itemsAvailable: BigInt(100),
itemsPurchased: BigInt(0),
},
{
startTime: BigInt(now + 600),
endTime: BigInt(now + 1200),
maxItemsPerUser: BigInt(5),
itemsAvailable: BigInt(100),
itemsPurchased: BigInt(0),
},
];
// Define metadata for the campaign collection
const campaignMetadata = {
name: "My Campaign Collection",
symbol: "MCC",
uri: "https://metadata.example.com/campaign.json",
};
// Define metadata for the individual items/NFTs
const itemMetadata = {
name: "Campaign Item",
symbol: "CI",
uri: "https://metadata.example.com/item.json",
};
// Create campaign returns [instruction, mintAddress]
const [createCampaignIx, mintAddress] =
await moonshot.getCreateCampaignInstruction({
configId,
campaignId,
stages,
priceInLamports: BigInt(1000000000), // 1 SOL in lamports
paymentDestination: adminAddress,
itemsAvailable: BigInt(200), // Total items available in this campaign
campaignMetadata,
itemMetadata,
});
await moonshot.signAndSendTransaction([createCampaignIx]);
console.log("Campaign created successfully with mint:", mintAddress);Account Management
Fetch Account Data
import {
getConfigAddress,
getCampaignAddress,
getUserAddress,
} from "moonshot-blindbox-sdk";
// Get config data
const configAddress = await getConfigAddress(configId);
const config = await moonshot.fetchConfig(configAddress);
console.log("Config:", config);
// Get campaign data
const campaignAddress = await getCampaignAddress(campaignId, configAddress);
const campaign = await moonshot.fetchCampaign(campaignAddress);
console.log("Campaign:", campaign);
// Get user data
const userAddress = await getUserAddress(campaignAddress, buyerAddress);
const user = await moonshot.fetchUser(userAddress);
console.log("User:", user);Check Capabilities
import { Role, getCapabilityAddress } from "moonshot-blindbox-sdk";
// Check if an account has a specific role
const capabilityAddress = await getCapabilityAddress(
configAddress,
adminAddress,
Role.Admin
);
const capability = await moonshot.fetchCapability(capabilityAddress);
if (capability) {
console.log("Account has Admin role");
} else {
console.log("Account does not have Admin role");
}Buy Operations
Single Transaction Pattern (Operator signs everything)
// For scenarios where the operator controls all signatures
const buyerAddress = "BuyerWalletAddressHere";
// getBuyInstruction returns [instruction, mintAddress]
const [buyIx, mintAddress] = await moonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerAddress,
});
await moonshot.signAndSendTransaction([buyIx]);
console.log("Purchase completed. Mint address:", mintAddress);Multi-Signature Pattern (Buyer maintains control)
import { generateKeyPairSigner } from "@solana/kit";
// For scenarios where buyer maintains control of their signature
const buyerSigner = await generateKeyPairSigner();
const buyerMoonshot = new MoonshotBlindbox(buyerSigner, RPC_URL, WSS_URL, true);
// Operator prepares the transaction (returns [instruction, mintAddress])
const [buyIx, mintAddress] = await moonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerSigner.address,
});
// Operator partially signs the transaction
const encodedTransaction = await moonshot.partiallySignTransaction(
[buyIx],
buyerSigner.address
);
const encodedTransactionFromAPI = Buffer.from(encodedTransaction).toString("base64");
// Frontend recovers the original encoded transaction
const recoveredEncodedTransaction = Buffer.from(encodedTransactionFromAPI, "base64");
// Buyer completes the signature and sends
await buyerMoonshot.signAndSendEncodedTransaction(encodedTransaction);
console.log("Purchase completed with buyer signature");
console.log("Mint address:", mintAddress);
Transaction Patterns
Batch Operations
import { Role } from "moonshot-blindbox-sdk";
// Perform multiple operations in a single transaction
const instructions = [];
// Example: Grant multiple roles in one transaction
const accounts = ["Address1Here", "Address2Here", "Address3Here"];
for (const account of accounts) {
const grantRoleIx = await moonshot.getGrantRoleInstruction({
configId,
accountToGrant: account,
role: Role.Operator,
});
instructions.push(grantRoleIx);
}
// Send all instructions in one transaction
await moonshot.signAndSendTransaction(instructions);
console.log("Batch operations completed");Conditional Operations
import { Status, getCampaignAddress } from "moonshot-blindbox-sdk";
import { some, none } from "@solana/kit";
// Check if campaign exists before updating
const campaignAddress = await getCampaignAddress(campaignId, configAddress);
const campaign = await moonshot.fetchCampaign(campaignAddress);
if (campaign && campaign.status === Status.Active) {
// Update campaign to inactive status
const updateIx = await moonshot.getUpdateCampaignInstruction({
configId,
campaignId,
status: some(Status.Inactive),
stages: none(),
priceInLamports: none(),
paymentDestination: none(),
itemsAvailable: none(),
});
await moonshot.signAndSendTransaction([updateIx]);
console.log("Campaign deactivated");
}Event Parsing
The SDK includes a powerful EventParser for extracting and decoding events emitted by the smart contract. This is essential for monitoring transaction outcomes and building reactive applications.
Available Events
The following events can be parsed from transaction logs:
- InitializeEvent - Emitted when a new config is initialized
- UpdateConfigEvent - Emitted when config status is updated
- GrantRoleEvent - Emitted when a role is granted to an account
- RevokeRoleEvent - Emitted when a role is revoked from an account
- CreateCampaignEvent - Emitted when a new campaign is created
- UpdateCampaignEvent - Emitted when campaign parameters are updated
- UpdateCampaignMetadataEvent - Emitted when campaign or item metadata is updated
- BuyEvent - Emitted when a user purchases an item
Basic Event Parsing
import { EventParser, parseTransactionEventCPI } from "moonshot-blindbox-sdk";
// After sending a transaction, parse its events
const signature = await moonshot.signAndSendTransaction([buyIx]);
// Extract event CPI data from the transaction
const eventCPI = await parseTransactionEventCPI(
"https://api.devnet.solana.com",
signature
);
// Parse the event data
const parsedEvent = EventParser.parseEventCPI(Buffer.from(eventCPI.data, "base64"));
console.log(`Event type: ${parsedEvent.name}`);
console.log("Event data:", parsedEvent.data);Parsing Specific Events
// Parse a BuyEvent after purchase
const [buyIx, mintAddress] = await moonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerAddress,
});
const signature = await moonshot.signAndSendTransaction([buyIx]);
const eventCPI = await parseTransactionEventCPI(RPC_URL, signature);
const event = EventParser.parseEventCPI(Buffer.from(eventCPI.data, "base64"));
if (event.name === "BuyEvent") {
console.log("Purchase Details:");
console.log(`Buyer: ${event.data.buyer}`);
console.log(`Amount: ${event.data.amount}`);
console.log(`Total Cost: ${event.data.totalLamports} lamports`);
console.log(`NFT Mint: ${event.data.mint}`);
console.log(`Token Account: ${event.data.associatedTokenAccount}`);
console.log(`Metadata: ${event.data.metadata}`);
}Event Discovery and Validation
import { EventParser } from "moonshot-blindbox-sdk";
// Get all available events
const availableEvents = EventParser.getAvailableEvents();
console.log("Available events:", availableEvents);
// Output: ['InitializeEvent', 'UpdateConfigEvent', 'GrantRoleEvent', ...]
// Check if a specific event is supported
const isBuySupported = EventParser.isEventSupported("BuyEvent");
console.log("BuyEvent supported:", isBuySupported); // true
const isCustomSupported = EventParser.isEventSupported("CustomEvent");
console.log("CustomEvent supported:", isCustomSupported); // falseComplete Example: Transaction Monitoring
import {
MoonshotBlindbox,
EventParser,
parseTransactionEventCPI,
} from "moonshot-blindbox-sdk";
async function monitorPurchase(
configId: number,
campaignId: number,
buyerAddress: string
) {
try {
// Create and send purchase transaction
const [buyIx, expectedMint] = await moonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerAddress,
});
// Operator partially signs
const encodedTransaction = await operatorMoonshot.partiallySignTransaction(
[buyIx],
buyerSigner.address
);
// Buyer completes signature and sends
const signature = await buyerMoonshot.signAndSendEncodedTransaction(
encodedTransaction
);
console.log(`Transaction signature: ${signature}`);
// Parse the transaction events
console.log("Parsing transaction events...");
const eventCPI = await parseTransactionEventCPI(
"https://api.devnet.solana.com",
signature
);
const parsedEvent = EventParser.parseEventCPI(
Buffer.from(eventCPI.data, "base64")
);
// Handle the event based on type
switch (parsedEvent.name) {
case "BuyEvent":
console.log("✅ Purchase Successful!");
console.log(`Buyer: ${parsedEvent.data.buyer}`);
console.log(`Items Purchased: ${parsedEvent.data.amount}`);
console.log(`Total Paid: ${parsedEvent.data.totalLamports} lamports`);
console.log(`NFT Mint Address: ${parsedEvent.data.mint}`);
console.log(`Token Account: ${parsedEvent.data.associatedTokenAccount}`);
// Verify the mint matches what we expected
if (parsedEvent.data.mint === expectedMint) {
console.log("✅ Mint address verified!");
}
break;
default:
console.log(`Unexpected event: ${parsedEvent.name}`);
}
return parsedEvent;
} catch (error) {
console.error("Transaction or parsing failed:", error);
throw error;
}
}Event Data Structures
Each event has a specific data structure. Here are the key event types:
BuyEvent
{
buyer: string; // Buyer's public key
paymentDestination: string; // Where payment was sent
campaign: string; // Campaign public key
config: string; // Config public key
amount: bigint; // Number of items purchased
totalLamports: bigint; // Total amount paid
mint: string; // NFT mint address
associatedTokenAccount: string; // Buyer's token account
metadata: string; // Metadata account
edition: string; // Master edition account
}CreateCampaignEvent
{
campaignId: number;
campaign: string;
config: string;
stages: Array<{
startTime: bigint;
endTime: bigint;
maxItemsPerUser: bigint;
itemsAvailable: bigint;
itemsPurchased: bigint;
}>;
priceInLamports: bigint;
paymentDestination: string;
itemsAvailable: bigint;
campaignMetadata: {
name: string;
symbol: string;
uri: string;
};
itemMetadata: {
name: string;
symbol: string;
uri: string;
};
}GrantRoleEvent / RevokeRoleEvent
{
role: number; // Role enum value (Admin = 0, Operator = 1)
account: string; // Account that was granted/revoked the role
}Error Handling
try {
const [buyIx, mintAddress] = await moonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerAddress,
});
await moonshot.signAndSendTransaction([buyIx]);
} catch (error: any) {
if (error.message.includes("InvalidAmount")) {
console.error("Invalid purchase amount");
} else if (error.message.includes("PurchaseLimitExceeded")) {
console.error("Purchase limit exceeded for this stage");
} else if (error.message.includes("OutOfCampaignDuration")) {
console.error("Campaign is not currently active");
} else {
console.error("Transaction failed:", error.message);
}
}Complete Examples
Example 1: Full Campaign Setup
import { MoonshotBlindbox, Status, Role } from "moonshot-blindbox-sdk";
import { generateKeyPairSigner, some, none } from "@solana/kit";
async function setupCompleteCampaign() {
// Initialize SDK
const adminSigner = await generateKeyPairSigner();
const moonshot = new MoonshotBlindbox(
adminSigner,
"https://api.devnet.solana.com",
"wss://api.devnet.solana.com"
);
const configId = 1;
const campaignId = 0;
try {
// 1. Initialize config
console.log("Initializing config...");
const initConfigIx = await moonshot.getInitializeConfigInstruction({
id: configId,
});
await moonshot.signAndSendTransaction([initConfigIx]);
// 2. Grant admin role to operator
console.log("Granting admin role...");
const operatorAddress = "OperatorWalletAddress";
const grantAdminIx = await moonshot.getGrantRoleInstruction({
configId,
accountToGrant: operatorAddress,
role: Role.Admin,
});
await moonshot.signAndSendTransaction([grantAdminIx]);
// 3. Create campaign with stages and metadata
console.log("Creating campaign...");
const now = Math.floor(Date.now() / 1000);
const stages = [
{
startTime: BigInt(now),
endTime: BigInt(now + 600),
maxItemsPerUser: BigInt(2),
itemsAvailable: BigInt(100),
itemsPurchased: BigInt(0),
},
{
startTime: BigInt(now + 600),
endTime: BigInt(now + 1200),
maxItemsPerUser: BigInt(5),
itemsAvailable: BigInt(100),
itemsPurchased: BigInt(0),
},
];
const campaignMetadata = {
name: "Pudgy Penguins",
symbol: "PP",
uri: "https://metadata.example.com/campaign.json",
};
const itemMetadata = {
name: "Blindbox Item",
symbol: "BI",
uri: "https://metadata.example.com/item.json",
};
const [createCampaignIx, mintAddress] =
await moonshot.getCreateCampaignInstruction({
configId,
campaignId,
stages,
priceInLamports: BigInt(1000000000), // 1 SOL
paymentDestination: operatorAddress,
itemsAvailable: BigInt(100), // Total 100 items available
campaignMetadata,
itemMetadata,
});
await moonshot.signAndSendTransaction([createCampaignIx]);
console.log("Campaign setup completed successfully!");
console.log("Mint address:", mintAddress);
return { configId, campaignId, mintAddress };
} catch (error) {
console.error("Campaign setup failed:", error);
throw error;
}
}Example 2: Purchase Flow with Multi-Signature
import {
MoonshotBlindbox,
Status,
getConfigAddress,
getCampaignAddress,
getUserAddress,
} from "moonshot-blindbox-sdk";
import { generateKeyPairSigner } from "@solana/kit";
async function purchaseWithBuyerSignature(
configId: number,
campaignId: number
) {
// Setup operator and buyer SDKs
const operatorSigner = await generateKeyPairSigner();
const operatorMoonshot = new MoonshotBlindbox(
operatorSigner,
"https://api.devnet.solana.com",
"wss://api.devnet.solana.com"
);
const buyerSigner = await generateKeyPairSigner();
const buyerMoonshot = new MoonshotBlindbox(
buyerSigner,
"https://api.devnet.solana.com",
"wss://api.devnet.solana.com",
true // Skip simulation for faster transactions
);
try {
// Check campaign status
const configAddress = await getConfigAddress(configId);
const campaignAddress = await getCampaignAddress(campaignId, configAddress);
const campaign = await operatorMoonshot.fetchCampaign(campaignAddress);
if (!campaign || campaign.status !== Status.Active) {
throw new Error("Campaign is not active");
}
console.log(`Campaign has ${campaign.itemsAvailable} items available`);
// Check buyer's current purchase status
const userAddress = await getUserAddress(
campaignAddress,
buyerSigner.address
);
const user = await operatorMoonshot.fetchUser(userAddress);
const currentPurchases = user?.itemsPurchased || BigInt(0);
console.log(`Buyer has already purchased ${currentPurchases} items`);
// Determine purchase amount (respect stage limits)
const stageIndex = 0; // Current stage
const stageLimit = campaign.stages[stageIndex]?.purchaseLimit || BigInt(0);
const purchasedInStage =
user?.itemsPurchasedPerStage[stageIndex] || BigInt(0);
const remainingInStage = stageLimit - purchasedInStage;
const purchaseAmount = remainingInStage > BigInt(0) ? BigInt(1) : BigInt(0);
if (purchaseAmount === BigInt(0)) {
throw new Error("Purchase limit reached for current stage");
}
// Create buy instruction (returns [instruction, mintAddress])
const [buyIx, mintAddress] = await operatorMoonshot.getBuyInstruction({
configId,
campaignId,
buyer: buyerSigner.address,
});
// Operator partially signs
const encodedTransaction = await operatorMoonshot.partiallySignTransaction(
[buyIx],
buyerSigner.address
);
// Buyer completes signature and sends
const signature = await buyerMoonshot.signAndSendEncodedTransaction(
encodedTransaction
);
console.log(`Purchase successful! Transaction signature: ${signature}`);
console.log(`NFT mint address: ${mintAddress}`);
// Verify final state
const updatedUser = await operatorMoonshot.fetchUser(userAddress);
const updatedCampaign = await operatorMoonshot.fetchCampaign(
campaignAddress
);
console.log(`Buyer now has ${updatedUser?.itemsPurchased} total purchases`);
console.log(
`Campaign now has ${updatedCampaign?.itemsAvailable} items remaining`
);
return signature;
} catch (error) {
console.error("Purchase failed:", error);
throw error;
}
}Best Practices
- Always check account existence before performing operations
- Handle errors gracefully with specific error type checking
- Use multi-signature patterns for user-facing applications
- Implement retry logic for network-related failures
- Validate amounts and limits before creating transactions
- Monitor transaction confirmations for critical operations
- Use batch operations when possible to reduce transaction fees
- Keep private keys secure and never expose them in client-side code
- Parse and log events for all critical transactions to ensure proper execution
- Implement real-time event monitoring for interactive applications that need to react to blockchain state changes
- Cache parsed events to avoid redundant RPC calls when displaying transaction history
- Validate event data against expected values (e.g., verify mint addresses match) before considering a transaction successful
