@paulstinchcombe/kami721c-sdk v0.0.5
KAMI721C SDK
A TypeScript SDK for interacting with KAMI721C and KAMI721CUpgradeable NFT contracts on Ethereum and EVM-compatible chains.
This SDK supports both standard KAMI721C deployments and upgradeable deployments using the transparent proxy pattern.
Installation
npm install kami721c-sdk
# or
yarn add kami721c-sdkFeatures
- Deploy standard KAMI721C contracts
- Deploy upgradeable KAMI721C contracts via Transparent Proxy
- Mint NFTs with USDC payment
- Programmable royalties (minting and transfer)
- Platform commission support
- Role-based access control (OpenZeppelin
AccessControl) - Token sales with royalty distribution
- NFT rental system with time-based access and USDC payments
- Pause/unpause functionality
- Burn functionality
- TypeScript support with comprehensive type definitions
- Uses ethers.js v6
Environment Setup
Create a .env file in the root of your project with the following variables:
# === Required ===
# Your private key for signing transactions (use a burner wallet for testing)
PRIVATE_KEY=0xYourPrivateKeyHere
# RPC URL for the target blockchain (e.g., Infura, Alchemy, local node, Skale Testnet)
RPC_URL=https://your.rpc.url
# Address of the USDC (or equivalent 6-decimal ERC20) token contract on the target network
USDC_ADDRESS=0xUsdcTokenAddressHere
# === Optional - for connecting to an existing contract ===
# If not deploying, set the address of the deployed KAMI721C (or proxy) contract
CONTRACT_ADDRESS=0xExistingContractOrProxyAddress
# === Optional - for deployment configuration ===
# Platform address to receive commissions (defaults to deployer address)
PLATFORM_ADDRESS=0xPlatformWalletAddress
# Platform commission percentage in basis points (100 = 1%, 500 = 5%)
PLATFORM_COMMISSION_PERCENTAGE=500
# Initial mint price in USDC (smallest unit, e.g., 1000000 for 1 USDC with 6 decimals)
MINT_PRICE=1000000
# NFT Collection Name
CONTRACT_NAME="My KAMI NFT Collection"
# NFT Collection Symbol
CONTRACT_SYMBOL="KAMI"
# Base URI for token metadata (can include trailing slash)
BASE_URI="https://api.example.com/nft/metadata/"
# === Optional - for example scripts (basic-usage.ts) ===
# Set to true to deploy a new contract instead of connecting to CONTRACT_ADDRESS
DEPLOY_NEW_CONTRACT=true
# Set to true to mint a token after deployment/connection
MINT_TOKEN=true
# Set to true to attempt updating the mint price (requires OWNER_ROLE)
UPDATE_MINT_PRICE=false
# Set to true to attempt setting royalties (requires OWNER_ROLE)
SET_ROYALTIES=false
# Set to true to attempt renting a token (requires RENTER_ADDRESS)
RENT_TOKEN=false
# Address of the wallet that will rent the token
RENTER_ADDRESS=0xRenterWalletAddress
# Address of the wallet that will buy the token in the sell example
BUYER_ADDRESS=0xBuyerWalletAddress
# Address to receive royalties
ROYALTY_RECEIVER=0xRoyaltyReceiverAddressCore Concepts
KAMI721CFactory
This class is used to deploy new instances of the KAMI721C contract. It supports deploying both the standard and upgradeable versions.
KAMI721C
This class is the main interface for interacting with a deployed KAMI721C contract (either standard or the proxy of an upgradeable one). It provides methods for all contract functions, including minting, sales, rentals, royalty management, and administration.
Usage Examples
Setting up the Provider and Signer
import { ethers } from 'ethers';
import dotenv from 'dotenv';
dotenv.config();
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
console.log(`Using wallet: ${wallet.address}`);Deploying an Upgradeable Contract (Recommended)
Use the deployUpgradeable method from the factory. This deploys the KAMI721CUpgradeable implementation contract and a TransparentUpgradeableProxy contract, then initializes the proxy.
import { KAMI721CFactory } from 'kami721c-sdk';
const factory = new KAMI721CFactory(wallet); // Pass the signer
console.log('Deploying upgradeable contract via proxy...');
const nftContract = await factory.deployUpgradeable(
process.env.USDC_ADDRESS!, // USDC token address
'My Upgradeable NFT Collection', // Collection name
'KAMIU', // Collection symbol
'https://api.example.com/nft/upgradeable/', // Base URI
1000000n, // Initial mint price (1 USDC)
process.env.PLATFORM_ADDRESS || wallet.address, // Platform address (also proxy admin)
500, // Platform commission (5%)
wallet.address // Initial owner address
);
const proxyAddress = nftContract.getAddress();
console.log(`Proxy contract deployed at: ${proxyAddress}`);
// Now interact with the contract via the nftContract instanceDeploying a Standard (Non-Upgradeable) Contract
Use the deployStandard method. This is generally discouraged if future upgrades might be needed.
import { KAMI721CFactory } from 'kami721c-sdk';
const factory = new KAMI721CFactory(wallet); // Pass the signer
console.log('Deploying standard non-upgradeable contract...');
const nftContract = await factory.deployStandard(
process.env.USDC_ADDRESS!,
'My Standard NFT Collection',
'KAMIS',
'https://api.example.com/nft/standard/',
1000000n,
process.env.PLATFORM_ADDRESS || wallet.address,
500
);
const contractAddress = nftContract.getAddress();
console.log(`Standard contract deployed at: ${contractAddress}`);Connecting to an Existing Contract
If the contract (or proxy) is already deployed, you can create an instance of the KAMI721C class directly.
import { KAMI721C } from 'kami721c-sdk';
const contractAddress = process.env.CONTRACT_ADDRESS!;
if (!contractAddress) {
throw new Error('CONTRACT_ADDRESS environment variable is not set');
}
const nftContract = new KAMI721C(wallet, contractAddress);
console.log(`Connected to contract at: ${nftContract.getAddress()}`);
// You can also connect with just a provider for read-only operations
// const readOnlyContract = new KAMI721C(provider, contractAddress);Reading Contract Information
const name = await nftContract.name();
const symbol = await nftContract.symbol();
const totalSupply = await nftContract.totalSupply();
const mintPrice = await nftContract.mintPrice();
const platformComm = await nftContract.getPlatformCommission(); // { percentage, address }
const royaltyPercent = await nftContract.royaltyPercentage();
const owner = await nftContract.ownerOf(0); // Owner of token ID 0
const balance = await nftContract.balanceOf(wallet.address);
console.log(`Name: ${name}, Symbol: ${symbol}`);
console.log(`Total Supply: ${totalSupply}`);
console.log(`Mint Price: ${ethers.formatUnits(mintPrice, 6)} USDC`);Minting a Token
Minting requires the caller to have enough USDC and to have approved the contract to spend it.
import { ethers } from 'ethers';
// 1. Get mint price
const currentMintPrice = await nftContract.mintPrice();
// 2. Get USDC contract instance (using a minimal ABI)
const usdcAddress = await nftContract.getUsdcTokenAddress();
const usdcAbi = [
'function approve(address spender, uint256 amount) external returns (bool)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function decimals() external view returns (uint8)',
];
const usdcContract = new ethers.Contract(usdcAddress, usdcAbi, wallet);
const decimals = await usdcContract.decimals();
// 3. Check and grant approval if necessary
const allowance = await usdcContract.allowance(wallet.address, nftContract.getAddress());
if (allowance < currentMintPrice) {
console.log('Approving USDC spend...');
const approveTx = await usdcContract.approve(nftContract.getAddress(), currentMintPrice);
await approveTx.wait();
console.log('USDC Approved');
}
// 4. Mint the token
console.log('Minting token...');
const mintTx = await nftContract.mint();
const receipt = await mintTx.wait();
console.log(`Token minted! Transaction: ${receipt.hash}`);
// Note: You would typically listen for the Transfer event to get the new tokenId
const newTotalSupply = await nftContract.totalSupply();
const newTokenId = newTotalSupply - 1n; // Assuming sequential IDs
console.log(`Minted token ID: ${newTokenId}`);Setting Royalties
Royalties can be set globally or per-token for both minting and transfers. Requires OWNER_ROLE.
// Example Royalty Structure
const royalties = [
{ receiver: '0xReceiverAddress1', feeNumerator: 9000n }, // 90% to receiver 1
{ receiver: '0xReceiverAddress2', feeNumerator: 1000n }, // 10% to receiver 2
];
// Set default royalties for new mints
await nftContract.setMintRoyalties(royalties);
// Set default royalties for transfers (applied based on royaltyPercentage)
await nftContract.setTransferRoyalties(royalties);
// Set the overall percentage for transfer royalties (e.g., 10%)
await nftContract.setRoyaltyPercentage(1000); // 1000 basis points = 10%
// Set royalties for a specific token (overrides defaults)
const tokenId = 0;
await nftContract.setTokenMintRoyalties(tokenId, [{ receiver: '0xSpecialReceiver', feeNumerator: 10000n }]);
await nftContract.setTokenTransferRoyalties(tokenId, [{ receiver: '0xSpecialReceiver', feeNumerator: 10000n }]);Selling a Token
The sellToken function handles the transfer and royalty/commission distribution. The buyer needs sufficient USDC and must approve the contract to spend it.
const tokenIdToSell = 0;
const buyerAddress = '0xBuyerAddress';
const salePrice = ethers.parseUnits('50', 6); // 50 USDC
// --- Buyer Side ---
// (Buyer needs a signer connected to the USDC contract)
// const buyerSigner = provider.getSigner(buyerAddress); // Or however buyer connects
// const buyerUsdcContract = usdcContract.connect(buyerSigner);
// const approveTx = await buyerUsdcContract.approve(nftContract.getAddress(), salePrice);
// await approveTx.wait();
// console.log('Buyer approved USDC');
// --- Seller Side ---
// Seller must own the token and approve the contract for transfer (if not already)
// await nftContract.approve(nftContract.getAddress(), tokenIdToSell);
console.log(`Selling token ${tokenIdToSell} to ${buyerAddress} for ${ethers.formatUnits(salePrice, 6)} USDC...`);
const sellTx = await nftContract.sellToken(buyerAddress, tokenIdToSell, salePrice);
const sellReceipt = await sellTx.wait();
console.log(`Token sold! Transaction: ${sellReceipt.hash}`);NFT Rental System
Allows owners to rent out NFTs temporarily. The renter gains usage rights (represented by the RENTER_ROLE for the specific token) without taking ownership. Payments are made in USDC.
Renting a Token
The renter pays the rental price + platform commission. Renter needs USDC and approval.
const tokenIdToRent = 1;
const rentalDurationSeconds = 60 * 60 * 24; // 1 day
const baseRentalPrice = ethers.parseUnits('2', 6); // 2 USDC base price
// Calculate total price including commission
const platformCommPercentage = await nftContract.getPlatformCommissionPercentage();
const commissionAmount = (baseRentalPrice * platformCommPercentage) / 10000n;
const totalRentalPrice = baseRentalPrice + commissionAmount;
// --- Renter Side ---
// (Renter needs a signer)
// const renterSigner = provider.getSigner(renterAddress);
// const renterUsdcContract = usdcContract.connect(renterSigner);
// const renterNftContract = nftContract.connect(renterSigner);
// 1. Approve USDC
// const approveTx = await renterUsdcContract.approve(nftContract.getAddress(), totalRentalPrice);
// await approveTx.wait();
// 2. Rent the token
// const rentTx = await renterNftContract.rentToken(tokenIdToRent, rentalDurationSeconds, totalRentalPrice);
// const rentReceipt = await rentTx.wait();
// console.log(`Token ${tokenIdToRent} rented! Transaction: ${rentReceipt.hash}`);Extending a Rental
Only the current renter can extend.
const additionalDuration = 60 * 60 * 12; // 12 hours
const additionalPayment = ethers.parseUnits('1', 6); // 1 USDC base price
// Calculate total additional payment
// ... (similar calculation as above for total price)
// --- Renter Side ---
// 1. Approve additional USDC
// ...
// 2. Extend rental
// const extendTx = await renterNftContract.extendRental(tokenIdToRent, additionalDuration, totalRentalPrice); // Pass TOTAL price
// await extendTx.wait();
// console.log('Rental extended!');Ending a Rental
Can be called by either the owner or the current renter.
// --- Owner or Renter Side ---
// const endTx = await nftContract.connect(ownerOrRenterSigner).endRental(tokenIdToRent);
// await endTx.wait();
// console.log('Rental ended.');Checking Rental Status
const isRented = await nftContract.isRented(tokenIdToRent);
if (isRented) {
const rentalInfo = await nftContract.getRentalInfo(tokenIdToRent);
console.log(`Token ${tokenIdToRent} is rented until ${new Date(Number(rentalInfo.endTime) * 1000)}`);
console.log(`Renter: ${rentalInfo.renter}`);
}
const userHasRentals = await nftContract.hasActiveRentals(wallet.address);
console.log(`Does user ${wallet.address} have active rentals? ${userHasRentals}`);Role Management
The contract uses OpenZeppelin's AccessControl.
Available Roles
DEFAULT_ADMIN_ROLE: Can grant/revoke any role.OWNER_ROLE: Can configure contract settings (mint price, royalties, platform fee, base URI, etc.).PLATFORM_ROLE: Designated address to receive platform commissions.RENTER_ROLE: Automatically granted/revoked to renters for specific tokens during the rental period. Not manually managed.PAUSER_ROLE: Can pause/unpause the contract.UPGRADER_ROLE(Upgradeable Version Only): Can upgrade the implementation contract via the proxy.
Managing Roles
Requires the caller to have the DEFAULT_ADMIN_ROLE.
const ownerRole = await nftContract.OWNER_ROLE();
const targetAccount = '0xNewOwnerAddress';
// Grant role
// const grantTx = await nftContract.grantRole(ownerRole, targetAccount);
// await grantTx.wait();
// Revoke role
// const revokeTx = await nftContract.revokeRole(ownerRole, targetAccount);
// await revokeTx.wait();
// Check role
const hasRole = await nftContract.hasRole(ownerRole, targetAccount);Contract Administration
Pausing/Unpausing
Requires PAUSER_ROLE.
// Pause
// const pauseTx = await nftContract.pause();
// await pauseTx.wait();
// Check status
const isPaused = await nftContract.paused();
// Unpause
// const unpauseTx = await nftContract.unpause();
// await unpauseTx.wait();Burning Tokens
Only the token owner can burn their token.
const tokenIdToBurn = 2;
// const burnTx = await nftContract.connect(ownerSigner).burn(tokenIdToBurn);
// await burnTx.wait();Setting Base URI
Requires OWNER_ROLE.
const newBaseUri = 'https://new.api.example.com/metadata/';
// const setUriTx = await nftContract.setBaseURI(newBaseUri);
// await setUriTx.wait();Development
- Clone the repository.
- Install dependencies:
npm installoryarn install - Compile TypeScript:
npm run buildoryarn build - Run examples:
node dist/examples/basic-usage.js(configure.envfirst)
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
License
MIT