UNPKG

@paulstinchcombe/kami721c-sdk

Version:

SDK for interacting with KAMI721C NFT contracts

450 lines (337 loc) 15.5 kB
# 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