@paulstinchcombe/kami721c-sdk
Version:
SDK for interacting with KAMI721C NFT contracts
450 lines (337 loc) • 15.5 kB
Markdown
# 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
```bash
npm install kami721c-sdk
# or
yarn add kami721c-sdk
```
## Features
- 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:
```env
# === 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=0xRoyaltyReceiverAddress
```
## Core 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
```typescript
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.
```typescript
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 instance
```
### Deploying a Standard (Non-Upgradeable) Contract
Use the `deployStandard` method. This is generally discouraged if future upgrades might be needed.
```typescript
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.
```typescript
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
```typescript
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.
```typescript
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`.
```typescript
// 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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
// --- Owner or Renter Side ---
// const endTx = await nftContract.connect(ownerOrRenterSigner).endRental(tokenIdToRent);
// await endTx.wait();
// console.log('Rental ended.');
```
### Checking Rental Status
```typescript
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`.
```typescript
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`.
```typescript
// 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.
```typescript
const tokenIdToBurn = 2;
// const burnTx = await nftContract.connect(ownerSigner).burn(tokenIdToBurn);
// await burnTx.wait();
```
### Setting Base URI
Requires `OWNER_ROLE`.
```typescript
const newBaseUri = 'https://new.api.example.com/metadata/';
// const setUriTx = await nftContract.setBaseURI(newBaseUri);
// await setUriTx.wait();
```
## Development
- Clone the repository.
- Install dependencies: `npm install` or `yarn install`
- Compile TypeScript: `npm run build` or `yarn build`
- Run examples: `node dist/examples/basic-usage.js` (configure `.env` first)
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
## License
MIT