@paulstinchcombe/kami721c-sdk
Version:
SDK for interacting with KAMI721C NFT contracts
534 lines (456 loc) • 24.6 kB
text/typescript
import { ethers } from 'ethers';
import { KAMI721C, RoyaltyData } from '../contracts/KAMI721C';
import { KAMI721CFactory } from '../factories/KAMI721CFactory';
import { colorLog, logStyles, formatKeyValue } from '../utils/console-colors';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
async function main() {
try {
// Environment variables
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const RPC_URL = process.env.RPC_URL || 'https://testnet.skalenodes.com/v1/giant-half-dual-testnet';
const ROYALTY_RECEIVER = process.env.ROYALTY_RECEIVER;
const BUYER_ADDRESS = process.env.BUYER_ADDRESS;
const DEPLOY_NEW_CONTRACT = process.env.DEPLOY_NEW_CONTRACT === 'true';
const PLATFORM_ADDRESS = process.env.PLATFORM_ADDRESS;
const PLATFORM_COMMISSION = process.env.PLATFORM_COMMISSION ? parseInt(process.env.PLATFORM_COMMISSION) : 500; // Default 5%
if (!PRIVATE_KEY) {
throw new Error('PRIVATE_KEY environment variable is required');
}
colorLog.info('Connecting to provider...');
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Create a wallet instance from the private key
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
colorLog.success(`Connected with wallet address: ${logStyles.address(wallet.address)}`);
// Variable to hold our contract instance
let nftContract: KAMI721C;
// DEPLOYMENT SECTION: Deploy a new contract or connect to existing one
if (DEPLOY_NEW_CONTRACT) {
colorLog.section('DEPLOYING NEW CONTRACT');
// Create a factory instance for deploying KAMI721C contracts
const factory = new KAMI721CFactory(wallet);
// Deploy parameters
const contractName = process.env.CONTRACT_NAME || 'KAMI NFT Collection';
const contractSymbol = process.env.CONTRACT_SYMBOL || 'KNFT';
const baseURI = process.env.BASE_URI || 'ipfs://bafkreid4xrk5mxq4t3ilfhwmvpn5scy2kmsra7gm7a3kscnudsyb4dtsqq/';
const usdcAddress = process.env.USDC_ADDRESS;
const initialMintPrice = process.env.MINT_PRICE ? BigInt(process.env.MINT_PRICE) : 1000000n; // Default 1 USDC (6 decimals)
const platformAddress = PLATFORM_ADDRESS || wallet.address; // Default to deployer address
if (!usdcAddress) {
throw new Error('USDC_ADDRESS environment variable is required for deployment');
}
colorLog.info(`Deploying new KAMI721C contract with the following parameters:`);
console.log(formatKeyValue('USDC Address', logStyles.address(usdcAddress)));
console.log(formatKeyValue('Name', contractName));
console.log(formatKeyValue('Symbol', contractSymbol));
console.log(formatKeyValue('Base URI', baseURI));
console.log(formatKeyValue('Initial Mint Price', `${ethers.formatUnits(initialMintPrice, 6)} USDC`));
console.log(formatKeyValue('Platform Address', logStyles.address(platformAddress)));
console.log(formatKeyValue('Platform Commission', `${PLATFORM_COMMISSION / 100}%`));
// Deploy the contract
nftContract = await factory.deploy(
usdcAddress,
contractName,
contractSymbol,
baseURI,
initialMintPrice,
platformAddress,
PLATFORM_COMMISSION
);
const deployedAddress = nftContract.getAddress();
colorLog.success(`Contract deployed successfully at address: ${logStyles.address(deployedAddress)}`);
colorLog.info(`Please save this address for future use. You can set it as CONTRACT_ADDRESS in your .env file.`);
} else {
// Connect to an existing contract
if (!CONTRACT_ADDRESS) {
throw new Error('CONTRACT_ADDRESS environment variable is required when not deploying a new contract');
}
colorLog.info(`Connecting to existing KAMI721C contract at ${logStyles.address(CONTRACT_ADDRESS)}...`);
nftContract = new KAMI721C(wallet, CONTRACT_ADDRESS);
}
// Get basic contract information
const name = await nftContract.name();
const symbol = await nftContract.symbol();
const totalSupply = await nftContract.totalSupply();
const currentMintPrice = await nftContract.mintPrice();
const platformCommission = await nftContract.getPlatformCommission();
const royaltyPct = await nftContract.royaltyPercentage();
colorLog.section('CONTRACT INFORMATION');
console.log(formatKeyValue('Name', name));
console.log(formatKeyValue('Symbol', symbol));
console.log(formatKeyValue('Total Supply', totalSupply.toString()));
console.log(formatKeyValue('Current Mint Price', `${ethers.formatUnits(currentMintPrice, 6)} USDC`));
console.log(
formatKeyValue(
'Platform Commission',
`${Number(platformCommission.percentage) / 100}% to ${logStyles.address(platformCommission.address)}`
)
);
console.log(formatKeyValue('Royalty Percentage', `${Number(royaltyPct) / 100}%`));
// Example: Update mint price (requires owner role)
const shouldUpdateMintPrice = process.env.UPDATE_MINT_PRICE === 'true';
if (shouldUpdateMintPrice) {
try {
const newMintPrice = process.env.NEW_MINT_PRICE ? BigInt(process.env.NEW_MINT_PRICE) : 2000000n; // Default 2 USDC
colorLog.info(`Updating mint price to ${logStyles.value(ethers.formatUnits(newMintPrice, 6))} USDC...`);
const tx = await nftContract.setMintPrice(newMintPrice);
colorLog.transaction('Mint price update transaction submitted', tx.hash);
await tx.wait();
colorLog.success('Mint price updated successfully');
const updatedPrice = await nftContract.mintPrice();
console.log(formatKeyValue('New mint price', `${ethers.formatUnits(updatedPrice, 6)} USDC`));
} catch (error: any) {
colorLog.error(`Error updating mint price: ${error.message}`);
colorLog.warning('Note: Updating mint price requires the OWNER_ROLE');
}
}
// Example: Mint a new token (requires USDC approval)
const shouldMint = process.env.MINT_TOKEN === 'true';
let tokenId: bigint | undefined;
if (shouldMint) {
try {
colorLog.section('MINTING NEW TOKEN');
const mintPrice = await nftContract.mintPrice();
colorLog.info(
`This will require ${logStyles.value(ethers.formatUnits(mintPrice, 6))} USDC and USDC approval for the contract`
);
// Create USDC contract instance
const usdcAddress = process.env.USDC_ADDRESS;
if (!usdcAddress) {
throw new Error('USDC_ADDRESS environment variable is required for minting');
}
// USDC contract ABI (only what we need)
const usdcAbi = [
'function approve(address spender, uint256 amount) external returns (bool)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function balanceOf(address account) external view returns (uint256)',
'function decimals() external view returns (uint8)',
];
const usdcContract = new ethers.Contract(usdcAddress, usdcAbi, wallet);
// Check USDC balance
const balance = await usdcContract.balanceOf(wallet.address);
const decimals = await usdcContract.decimals();
console.log(formatKeyValue('USDC Balance', `${ethers.formatUnits(balance, decimals)} USDC`));
console.log(formatKeyValue('Required for mint', `${ethers.formatUnits(mintPrice, decimals)} USDC`));
if (balance < mintPrice) {
throw new Error(
`Insufficient USDC balance. Need ${ethers.formatUnits(mintPrice, decimals)} USDC but have ${ethers.formatUnits(
balance,
decimals
)} USDC`
);
}
// Check and set USDC approval if needed
const contractAddress = nftContract.getAddress();
const currentAllowance = await usdcContract.allowance(wallet.address, contractAddress);
console.log(formatKeyValue('Current USDC allowance', `${ethers.formatUnits(currentAllowance, decimals)} USDC`));
if (currentAllowance < mintPrice) {
colorLog.info('Approving USDC spend...');
const approveTx = await usdcContract.approve(contractAddress, mintPrice);
colorLog.transaction('Approval transaction submitted', approveTx.hash);
await approveTx.wait();
colorLog.success('USDC approved successfully');
// Verify approval
const newAllowance = await usdcContract.allowance(wallet.address, contractAddress);
console.log(formatKeyValue('New USDC allowance', `${ethers.formatUnits(newAllowance, decimals)} USDC`));
if (newAllowance < mintPrice) {
throw new Error('USDC approval failed - allowance not set correctly');
}
}
// Mint the token
colorLog.info('Sending mint transaction...');
const mintTx = await nftContract.mint();
colorLog.transaction('Mint transaction submitted', mintTx.hash);
// Wait for transaction to be confirmed
colorLog.info('Waiting for mint transaction confirmation...');
const receipt = await mintTx.wait();
colorLog.success(`Token minted successfully in block ${logStyles.value(receipt?.blockNumber)}`);
// For demonstration, we'll assume the token ID is the current total supply
// In a real application, you would get the token ID from the mint event
const newTotalSupply = await nftContract.totalSupply();
tokenId = newTotalSupply > 0n ? newTotalSupply - 1n : 0n;
colorLog.success(`Newly minted token ID: ${logStyles.value(tokenId.toString())}`);
} catch (error: any) {
colorLog.error(`Error minting token: ${error.message}`);
if (error.data) {
console.error('Error data:', error.data);
}
if (error.transaction) {
console.error('Transaction:', error.transaction);
}
const mintPrice = await nftContract.mintPrice();
colorLog.warning(
`Note: Minting requires ${logStyles.value(ethers.formatUnits(mintPrice, 6))} USDC and USDC approval for the contract`
);
}
} else if (totalSupply > 0n) {
// If we didn't mint, use the first token for demonstration
tokenId = 0n; // Token IDs are 0-based in the new contract
} else {
colorLog.warning('\nNo tokens available for demonstration. Set MINT_TOKEN=true to mint a new token.');
}
// If we have a token to work with, proceed with royalty and selling examples
if (tokenId !== undefined) {
// SECTION 1: Setting Royalties
colorLog.section('ROYALTY SETUP EXAMPLE');
try {
// Get the royalty receiver address (default to the wallet address if not provided)
const royaltyReceiver = ROYALTY_RECEIVER || wallet.address;
colorLog.info(`Setting royalties with receiver: ${logStyles.address(royaltyReceiver)}`);
// Set default royalty percentage for all transfers (requires OWNER_ROLE)
const newRoyaltyPercentage = 1000; // 10% (out of 10000)
colorLog.info(`Setting default royalty percentage to ${logStyles.value(newRoyaltyPercentage / 100)}%...`);
const setRoyaltyPctTx = await nftContract.setRoyaltyPercentage(newRoyaltyPercentage);
colorLog.transaction('Royalty percentage transaction submitted', setRoyaltyPctTx.hash);
await setRoyaltyPctTx.wait();
colorLog.success('Royalty percentage set successfully');
// Create royalty data with the royalty receiver getting 100% of the royalties
const royalties: RoyaltyData[] = [
{
receiver: royaltyReceiver,
feeNumerator: 1000n, // 10% of the royalties (matches the royalty percentage set earlier)
},
];
// Add this before trying setMintRoyalties
const hasOwnerRole = await nftContract.hasRole(await nftContract.OWNER_ROLE(), wallet.address);
colorLog.info(`Wallet has OWNER_ROLE: ${hasOwnerRole ? 'Yes' : 'No'}`);
// Set global mint royalties (for all newly minted tokens)
colorLog.info('Setting global mint royalties...');
const setMintRoyaltiesTx = await nftContract.setMintRoyalties(royalties);
colorLog.transaction('Mint royalties transaction submitted', setMintRoyaltiesTx.hash);
await setMintRoyaltiesTx.wait();
colorLog.success('Global mint royalties set successfully');
// Set global transfer royalties with the same receiver but using 10000n as total share
colorLog.info('Setting global transfer royalties...');
const transferRoyalties: RoyaltyData[] = [
{
receiver: royaltyReceiver,
feeNumerator: 10000n, // 100% of the royalties (must total 10000 for transfer royalties)
},
];
const setTransferRoyaltiesTx = await nftContract.setTransferRoyalties(transferRoyalties);
colorLog.transaction('Transfer royalties transaction submitted', setTransferRoyaltiesTx.hash);
await setTransferRoyaltiesTx.wait();
colorLog.success('Global transfer royalties set successfully');
// Set token-specific transfer royalties (optional)
colorLog.info(`Setting transfer royalties for token #${logStyles.value(tokenId)}...`);
const setTokenRoyaltiesTx = await nftContract.setTokenTransferRoyalties(tokenId, transferRoyalties);
colorLog.transaction('Token royalties transaction submitted', setTokenRoyaltiesTx.hash);
await setTokenRoyaltiesTx.wait();
colorLog.success(`Transfer royalties for token #${logStyles.value(tokenId)} set successfully`);
// Verify royalty information
const salePrice = ethers.parseEther('1.0'); // 1 ETH as the example sale price
const royaltyInfo = await nftContract.royaltyInfo(tokenId, salePrice);
colorLog.info('\nRoyalty Information:');
console.log(formatKeyValue('Receiver', logStyles.address(royaltyInfo.receiver)));
console.log(
formatKeyValue(
'Amount for 1 ETH sale',
`${ethers.formatEther(royaltyInfo.royaltyAmount)} ETH (${(
(Number(royaltyInfo.royaltyAmount) * 100) /
Number(salePrice)
).toFixed(2)}%)`
)
);
// SECTION 2: Token Sale Example
colorLog.section('TOKEN SALE EXAMPLE');
// Simulate a token sale
if (BUYER_ADDRESS) {
colorLog.info(`Simulating sale to buyer: ${logStyles.address(BUYER_ADDRESS)}`);
// Get USDC for the sale price calculation
const usdcAddress = process.env.USDC_ADDRESS;
if (!usdcAddress) {
throw new Error('USDC_ADDRESS environment variable is required for token sale');
}
// Sale price in USDC (with 6 decimals)
const salePrice = 5000000n; // 5 USDC
colorLog.info(`Setting sale price to ${logStyles.value(ethers.formatUnits(salePrice, 6))} USDC`);
// Check token ownership first
const currentOwner = await nftContract.ownerOf(tokenId);
colorLog.info(`Current owner of token #${logStyles.value(tokenId)}: ${logStyles.address(currentOwner)}`);
if (currentOwner.toLowerCase() !== wallet.address.toLowerCase()) {
colorLog.warning(`Wallet does not own token #${logStyles.value(tokenId)} - sale may fail`);
}
// USDC contract ABI (only what we need)
const usdcAbi = [
'function approve(address spender, uint256 amount) external returns (bool)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function balanceOf(address account) external view returns (uint256)',
'function decimals() external view returns (uint8)',
'function transfer(address to, uint256 amount) external returns (bool)',
];
// Need to create a signer for the buyer to approve USDC
const buyerContract = new ethers.Contract(usdcAddress, usdcAbi, wallet);
// Check and display USDC balances
const buyerBalance = await buyerContract.balanceOf(BUYER_ADDRESS);
const sellerBalance = await buyerContract.balanceOf(wallet.address);
const decimals = await buyerContract.decimals();
colorLog.info('USDC Balances:');
console.log(formatKeyValue('Seller', `${ethers.formatUnits(sellerBalance, decimals)} USDC`));
console.log(formatKeyValue('Buyer', `${ethers.formatUnits(buyerBalance, decimals)} USDC`));
// For sellToken to work, the BUYER needs to approve the contract to spend their USDC
// But since we don't have the buyer's private key in this example, we'll simulate by:
// 1. First transfer USDC from seller to buyer (if needed)
// 2. Then use the seller's key to approve on behalf of the buyer (would need to be done by buyer in real scenario)
// 1. Transfer USDC to buyer if they don't have enough
if (buyerBalance < salePrice) {
colorLog.info(
`Buyer needs USDC. Transferring ${logStyles.value(
ethers.formatUnits(salePrice, decimals)
)} USDC to buyer for demo...`
);
if (sellerBalance < salePrice) {
colorLog.error(`Seller doesn't have enough USDC to transfer to buyer for this demo.`);
throw new Error('Insufficient USDC for demo');
}
const transferTx = await buyerContract.transfer(BUYER_ADDRESS, salePrice);
colorLog.transaction('USDC transfer transaction submitted', transferTx.hash);
await transferTx.wait();
colorLog.success('USDC transferred to buyer');
// Verify new balance
const newBuyerBalance = await buyerContract.balanceOf(BUYER_ADDRESS);
console.log(formatKeyValue("Buyer's new balance", `${ethers.formatUnits(newBuyerBalance, decimals)} USDC`));
}
// For the example, since we can't sign as the buyer, we'll mock the approval
colorLog.warning(`NOTE: In a real application, the BUYER would need to call the approve function themselves.`);
colorLog.info(`For this demo, we're showing the approval that the buyer would need to make.`);
// Approve the NFT transfer before selling
colorLog.info(`Approving NFT contract to transfer token #${logStyles.value(tokenId)}...`);
// We need to use ethers.js directly to approve the NFT since we can't access the private contract property
const ERC721_ABI = [
'function setApprovalForAll(address operator, bool approved) external',
'function isApprovedForAll(address owner, address operator) external view returns (bool)',
];
const nftERC721 = new ethers.Contract(nftContract.getAddress(), ERC721_ABI, wallet);
const approveTknTx = await nftERC721.setApprovalForAll(nftContract.getAddress(), true);
colorLog.transaction('NFT approval transaction submitted', approveTknTx.hash);
await approveTknTx.wait();
// Verify approval
const isApproved = await nftERC721.isApprovedForAll(wallet.address, nftContract.getAddress());
colorLog.info(`NFT approved for contract: ${isApproved ? 'Yes' : 'No'}`);
// Try the built-in sellToken function
colorLog.section('TOKEN TRANSFER');
try {
// Try using the built-in sellToken function
colorLog.info(
`Selling token #${logStyles.value(tokenId)} to ${logStyles.address(BUYER_ADDRESS)} for ${logStyles.value(
ethers.formatUnits(salePrice, 6)
)} USDC...`
);
// colorLog.warning(`In this example, we can demonstrate the sellToken function call, but it will fail because:`);
// console.log('1. The buyer needs to approve USDC spending (which requires their private key)');
// console.log('2. The contract expects the buyer to have already approved USDC spending');
// USDC contract ABI (only what we need)
const usdcAbi = [
'function approve(address spender, uint256 amount) external returns (bool)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function balanceOf(address account) external view returns (uint256)',
'function decimals() external view returns (uint8)',
'function transfer(address to, uint256 amount) external returns (bool)',
];
// Create a buyer wallet and fund it with USDC
colorLog.info('Creating buyer wallet and funding with USDC...');
const bw = ethers.Wallet.createRandom();
colorLog.info(`Buyer wallet address: ${bw.address}`);
colorLog.info(`Buyer wallet private key: ${bw.privateKey}`);
const buyerWallet = new ethers.Wallet(bw.privateKey, provider);
// const buyerSigner = buyerWallet.connect(provider);
const usdcContract = new ethers.Contract(usdcAddress, usdcAbi, wallet);
const transferTx = await usdcContract.transfer(buyerWallet.address, salePrice);
colorLog.transaction('USDC transfer transaction submitted', transferTx.hash);
await transferTx.wait();
const buyerBalance = await usdcContract.balanceOf(buyerWallet.address);
colorLog.success(`Buyer wallet funded with ${ethers.formatUnits(buyerBalance, decimals)} USDC`);
// Log all relevant parameters for debugging
colorLog.info('Parameters for sellToken:');
console.log(formatKeyValue('From address', logStyles.address(wallet.address)));
console.log(formatKeyValue('To address', logStyles.address(buyerWallet.address)));
console.log(formatKeyValue('Token ID', tokenId.toString()));
console.log(formatKeyValue('Sale Price', `${ethers.formatUnits(salePrice, 6)} USDC`));
// For a real application, suggest this code for the buyer:
// colorLog.info('Code the BUYER would need to run:');
// console.log(`const usdcContract = new ethers.Contract('${usdcAddress}', usdcAbi, buyerSigner);`);
// console.log(`await usdcContract.approve('${nftContract.getAddress()}', ${salePrice});`);
// Approve the NFT contract to spend the USDC
const buyerUsdcContract = new ethers.Contract(usdcAddress, usdcAbi, buyerWallet);
const approveTx = await buyerUsdcContract.approve(nftContract.getAddress(), salePrice);
colorLog.transaction('USDC approval transaction submitted', approveTx.hash);
await approveTx.wait();
colorLog.success('USDC approved successfully');
// Send some native tokens for gas fees
const gasAmount = ethers.parseEther('0.05'); // Small amount for gas
const gasTx = await wallet.sendTransaction({
to: buyerWallet.address,
value: gasAmount,
});
colorLog.transaction('Gas transaction submitted', gasTx.hash);
await gasTx.wait();
colorLog.success('Gas sent successfully');
// Now attempt the sellToken call (expected to fail without buyer's approval)
const sellTokenTx = await nftContract.sellToken(buyerWallet.address, tokenId, salePrice);
colorLog.transaction('Sale transaction submitted', sellTokenTx.hash);
await sellTokenTx.wait();
colorLog.success(`Sale completed successfully!`);
// Verify new owner
const newOwner = await nftContract.ownerOf(tokenId);
colorLog.success(`New owner of token #${logStyles.value(tokenId)}: ${logStyles.address(newOwner)}`);
} catch (saleError: any) {
colorLog.error(`Sale failed: ${saleError.message}`);
// Enhanced error logging
if (saleError.data) {
colorLog.error('Error data:');
console.dir(saleError.data, { depth: null });
}
if (saleError.transaction) {
colorLog.error('Transaction details:');
console.dir(saleError.transaction, { depth: null });
}
if (saleError.error) {
colorLog.error('Error details:');
console.dir(saleError.error, { depth: null });
}
// Fallback to direct ERC721 transfer
colorLog.info(`Trying direct ERC721 transfer as fallback...`);
try {
// Try direct ERC721 transfer
const transferABI = [
'function safeTransferFrom(address from, address to, uint256 tokenId) external',
'function transferFrom(address from, address to, uint256 tokenId) external',
];
const erc721 = new ethers.Contract(nftContract.getAddress(), transferABI, wallet);
const tx = await erc721.safeTransferFrom(wallet.address, BUYER_ADDRESS, tokenId);
colorLog.transaction('Transfer transaction submitted', tx.hash);
await tx.wait();
colorLog.success(`Transfer completed successfully!`);
// Verify new owner
const newOwner = await nftContract.ownerOf(tokenId);
colorLog.success(`New owner of token #${logStyles.value(tokenId)}: ${logStyles.address(newOwner)}`);
} catch (transferError: any) {
colorLog.error(`Direct transfer failed: ${transferError.message}`);
colorLog.warning(`\nToken remains with original owner.`);
}
}
} else {
colorLog.warning('\nSkipping token sale simulation (BUYER_ADDRESS not provided)');
colorLog.info('To simulate a sale, set the BUYER_ADDRESS environment variable');
}
} catch (error: any) {
colorLog.error(`Error in royalty/sales operations: ${error.message}`);
colorLog.warning('Note: Setting royalties and other operations require appropriate roles.');
}
}
colorLog.section('COMPLETE');
colorLog.success('Basic usage example completed.');
} catch (error: any) {
colorLog.error(`Error in example: ${error.message}`);
}
}
// Run the example
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});