npm package: @paulstinchcombe/kami721c-sdk
Description: SDK for interacting with KAMI721C NFT contracts
Author: Paul Stinchcombe
Latest version: 0.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

```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