@paulstinchcombe/kami721c-sdk
Version:
SDK for interacting with KAMI721C NFT contracts
556 lines (497 loc) • 17.2 kB
text/typescript
import { Contract, ContractRunner } from 'ethers';
import KAMI721CABI from '../abis/KAMI721C/KAMI721C.json';
import { colorLog, logStyles, formatKeyValue } from '../utils/console-colors';
import { ethers } from 'ethers';
/**
* Royalty data structure
*/
export interface RoyaltyData {
receiver: string;
feeNumerator: bigint;
}
/**
* Rental information structure
*/
export interface RentalInfo {
renter: string;
startTime: bigint;
endTime: bigint;
rentalPrice: bigint;
active: boolean;
}
/**
* KAMI721C contract wrapper class
*/
export class KAMI721C {
private contract: Contract;
/**
* Creates a new instance of the KAMI721C contract wrapper
* @param runner An ethers.js ContractRunner (Provider or Signer)
* @param contractAddress The address of the KAMI721C contract
* @param contractAbi Optional ABI to use (defaults to standard KAMI721C ABI)
*/
constructor(runner: ContractRunner, contractAddress: string, contractAbi?: any) {
// Use the provided ABI or default to the standard KAMI721C ABI
const abi = contractAbi || KAMI721CABI.abi;
this.contract = new Contract(contractAddress, abi, runner);
}
/**
* Connect a signer to the contract
* @param signer The signer to connect
* @returns A new instance of the KAMI721C contract with the connected signer
*/
connect(signer: ContractRunner): KAMI721C {
// Pass the current ABI when creating a connected instance
return new KAMI721C(signer, this.contract.target as string, this.contract.interface.fragments);
}
/**
* Get the contract address
* @returns The contract address
*/
getAddress(): string {
return this.contract.target as string;
}
/**
* Get the contract name
* @returns The name of the NFT collection
*/
async name(): Promise<string> {
return await this.contract.name();
}
/**
* Get the contract symbol
* @returns The symbol of the NFT collection
*/
async symbol(): Promise<string> {
return await this.contract.symbol();
}
/**
* Get the total supply of tokens
* @returns The total number of tokens minted
*/
async totalSupply(): Promise<bigint> {
try {
// Try to call totalSupply directly first
return await this.contract.totalSupply();
} catch (error) {
try {
// If totalSupply is not available, estimate using token counter
// In the new contract, we can check the current token ID counter
const tokenIdCounter = await this.contract._tokenIdCounter();
return tokenIdCounter;
} catch (error) {
// If both methods fail, return 0 as no tokens have been minted yet
return 0n;
}
}
}
/**
* Get the token URI for a specific token ID
* @param tokenId The ID of the token to query
* @returns The token URI
*/
async tokenURI(tokenId: number | bigint): Promise<string> {
return await this.contract.tokenURI(tokenId);
}
/**
* Get the owner of a specific token
* @param tokenId The ID of the token to query
* @returns The address of the token owner
*/
async ownerOf(tokenId: number | bigint): Promise<string> {
return await this.contract.ownerOf(tokenId);
}
/**
* Get the balance of an address
* @param owner The address to query
* @returns The number of tokens owned by the address
*/
async balanceOf(owner: string): Promise<bigint> {
return await this.contract.balanceOf(owner);
}
/**
* Approve another address to transfer a specific token
* @param spender The address to approve
* @param tokenId The ID of the token to approve
* @returns The transaction
*/
async approve(spender: string, tokenId: number | bigint) {
return await this.contract.approve(spender, tokenId);
}
/**
* Get the approved address for a specific token
* @param tokenId The ID of the token
* @returns The approved address, or the zero address if none is set
*/
async getApproved(tokenId: number | bigint): Promise<string> {
return await this.contract.getApproved(tokenId);
}
/**
* Check if an operator is approved for all tokens of an owner
* @param owner The owner address
* @param operator The operator address
* @returns True if the operator is approved, false otherwise
*/
async isApprovedForAll(owner: string, operator: string): Promise<boolean> {
return await this.contract.isApprovedForAll(owner, operator);
}
/**
* Set or unset approval for an operator for all tokens of the caller
* @param operator The operator address
* @param approved True to approve, false to revoke
* @returns The transaction
*/
async setApprovalForAll(operator: string, approved: boolean) {
return await this.contract.setApprovalForAll(operator, approved);
}
/**
* Get the current mint price
* @returns The mint price in USDC (with 6 decimals)
*/
async mintPrice(): Promise<bigint> {
return await this.contract.mintPrice();
}
/**
* Set the mint price (requires OWNER_ROLE)
* @param newMintPrice The new mint price in USDC (with 6 decimals)
* @returns The transaction
*/
async setMintPrice(newMintPrice: bigint | string) {
return await this.contract.setMintPrice(newMintPrice);
}
/**
* Mint a new token (requires USDC approval)
* @returns The transaction
*/
async mint() {
return await this.contract.mint();
}
/**
* Set royalties for all newly minted tokens (requires OWNER_ROLE)
* @param royalties Array of royalty receivers and fee numerators
* @returns The transaction
*/
async setMintRoyalties(royalties: RoyaltyData[]) {
return await this.contract.setMintRoyalties(royalties);
}
/**
* Set royalties for a specific token's mint event (requires OWNER_ROLE)
* @param tokenId The ID of the token to set royalties for
* @param royalties Array of royalty receivers and fee numerators
* @returns The transaction
*/
async setTokenMintRoyalties(tokenId: number | bigint, royalties: RoyaltyData[]) {
return await this.contract.setTokenMintRoyalties(tokenId, royalties);
}
/**
* Set global transfer royalties for all tokens (requires OWNER_ROLE)
* @param royalties Array of royalty receivers and fee numerators
* @returns The transaction
*/
async setTransferRoyalties(royalties: RoyaltyData[]) {
return await this.contract.setTransferRoyalties(royalties);
}
/**
* Set transfer royalties for a specific token (requires OWNER_ROLE)
* @param tokenId The ID of the token to set royalties for
* @param royalties Array of royalty receivers and fee numerators
* @returns The transaction
*/
async setTokenTransferRoyalties(tokenId: number | bigint, royalties: RoyaltyData[]) {
return await this.contract.setTokenTransferRoyalties(tokenId, royalties);
}
/**
* Get the current royalty percentage for transfers
* @returns The royalty percentage in basis points (e.g., 1000 = 10%)
*/
async royaltyPercentage(): Promise<number> {
return await this.contract.royaltyPercentage();
}
/**
* Set the royalty percentage for transfers (requires OWNER_ROLE)
* @param newRoyaltyPercentage New royalty percentage in basis points (e.g., 1000 = 10%)
* @returns The transaction
*/
async setRoyaltyPercentage(newRoyaltyPercentage: number) {
return await this.contract.setRoyaltyPercentage(newRoyaltyPercentage);
}
/**
* Get the platform commission details
* @returns The platform commission percentage and address
*/
async getPlatformCommission(): Promise<{ percentage: number; address: string }> {
const percentage = await this.contract.platformCommissionPercentage();
const address = await this.contract.platformAddress();
return { percentage, address };
}
/**
* Set the platform commission details (requires OWNER_ROLE)
* @param newPercentage New commission percentage in basis points (e.g., 500 = 5%)
* @param newAddress New platform address to receive commission
* @returns The transaction
*/
async setPlatformCommission(newPercentage: number, newAddress: string) {
return await this.contract.setPlatformCommission(newPercentage, newAddress);
}
/**
* Sell a token to another address with royalties handled automatically
* @param to The buyer address
* @param tokenId The token ID to sell
* @param salePrice The sale price in USDC
* @returns The transaction
*/
async sellToken(to: string, tokenId: number | bigint, salePrice: bigint | string) {
try {
// Verify the caller owns the token
const owner = await this.ownerOf(tokenId);
const signerAddress = await (this.contract.runner as ethers.Signer).getAddress();
if (owner.toLowerCase() !== signerAddress.toLowerCase()) {
colorLog.error(
`Error: Only the token owner can sell. Current owner: ${logStyles.address(owner)}, Caller: ${logStyles.address(
signerAddress
)}`
);
throw new Error('Only the token owner can sell this token');
}
// Verify that the contract is approved to transfer the token
const isApproved = await this.contract.isApprovedForAll(signerAddress, this.getAddress());
if (!isApproved) {
colorLog.warning(`Contract is not approved to transfer tokens. Please call setApprovalForAll first.`);
}
// Verify USDC allowance for the buyer
try {
const usdcAddress = await this.contract.usdcToken();
const usdcABI = [
'function allowance(address owner, address spender) external view returns (uint256)',
'function balanceOf(address owner) external view returns (uint256)',
];
const usdc = new ethers.Contract(usdcAddress, usdcABI, this.contract.runner);
const allowance = await usdc.allowance(to, this.getAddress());
if (allowance < salePrice) {
colorLog.warning(
`Buyer has insufficient USDC allowance. Required: ${ethers.formatUnits(
salePrice,
6
)}, Current: ${ethers.formatUnits(allowance, 6)}`
);
}
const balance = await usdc.balanceOf(to);
if (balance < salePrice) {
colorLog.warning(
`Buyer has insufficient USDC balance. Required: ${ethers.formatUnits(salePrice, 6)}, Current: ${ethers.formatUnits(
balance,
6
)}`
);
}
} catch (error) {
colorLog.warning(`Could not verify USDC allowance: ${error}`);
}
// Now call the sellToken function
colorLog.info(
`Calling sellToken with parameters: to=${logStyles.address(to)}, tokenId=${logStyles.value(
tokenId.toString()
)}, salePrice=${logStyles.value(salePrice.toString())}`
);
return await this.contract.sellToken(to, tokenId, salePrice);
} catch (error: any) {
colorLog.error(`sellToken failed: ${error.message}`);
throw error;
}
}
/**
* Get royalty information for a token sale
* @param tokenId The ID of the token being sold
* @param salePrice The sale price
* @returns The royalty receiver address and amount
*/
async royaltyInfo(tokenId: number | bigint, salePrice: bigint | string): Promise<{ receiver: string; royaltyAmount: bigint }> {
const [receiver, royaltyAmount] = await this.contract.royaltyInfo(tokenId, salePrice);
return { receiver, royaltyAmount };
}
/**
* Get mint royalty receivers for a token
* @param tokenId The ID of the token
* @returns Array of royalty data
*/
async getMintRoyaltyReceivers(tokenId: number | bigint): Promise<RoyaltyData[]> {
return await this.contract.getMintRoyaltyReceivers(tokenId);
}
/**
* Get transfer royalty receivers for a token
* @param tokenId The ID of the token
* @returns Array of royalty data
*/
async getTransferRoyaltyReceivers(tokenId: number | bigint): Promise<RoyaltyData[]> {
return await this.contract.getTransferRoyaltyReceivers(tokenId);
}
/**
* Check if an address has a specific role
* @param role The role to check (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
* @param address The address to check
* @returns True if the address has the role
*/
async hasRole(role: string, address: string): Promise<boolean> {
return await this.contract.hasRole(role, address);
}
/**
* Grant a role to an address (requires DEFAULT_ADMIN_ROLE)
* @param role The role to grant (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
* @param address The address to grant the role to
* @returns The transaction
*/
async grantRole(role: string, address: string) {
return await this.contract.grantRole(role, address);
}
/**
* Revoke a role from an address (requires DEFAULT_ADMIN_ROLE)
* @param role The role to revoke (OWNER_ROLE, PLATFORM_ROLE, or RENTER_ROLE)
* @param address The address to revoke the role from
* @returns The transaction
*/
async revokeRole(role: string, address: string) {
return await this.contract.revokeRole(role, address);
}
/**
* Get the OWNER_ROLE constant
* @returns The OWNER_ROLE bytes32 value
*/
async OWNER_ROLE(): Promise<string> {
return await this.contract.OWNER_ROLE();
}
/**
* Get the PLATFORM_ROLE constant
* @returns The PLATFORM_ROLE bytes32 value
*/
async PLATFORM_ROLE(): Promise<string> {
return await this.contract.PLATFORM_ROLE();
}
/**
* Get the RENTER_ROLE constant
* @returns The RENTER_ROLE bytes32 value
*/
async RENTER_ROLE(): Promise<string> {
return await this.contract.RENTER_ROLE();
}
/**
* Set the base URI for tokens (requires OWNER_ROLE)
* @param baseURI The new base URI
* @returns The transaction
*/
async setBaseURI(baseURI: string) {
return await this.contract.setBaseURI(baseURI);
}
/**
* Burn a token (requires token owner permission)
* @param tokenId The ID of the token to burn
* @returns The transaction
*/
async burn(tokenId: number | bigint) {
return await this.contract.burn(tokenId);
}
/**
* Set the security policy for the contract (requires OWNER_ROLE)
* @param securityLevel The security level
* @param operatorWhitelistId The operator whitelist ID
* @param permittedContractReceiversAllowlistId The permitted contract receivers allowlist ID
* @returns The transaction
*/
async setSecurityPolicy(securityLevel: number, operatorWhitelistId: number, permittedContractReceiversAllowlistId: number) {
return await this.contract.setSecurityPolicy(securityLevel, operatorWhitelistId, permittedContractReceiversAllowlistId);
}
/**
* Rent a token (requires USDC approval)
* @param tokenId The ID of the token to rent
* @param duration The rental duration in seconds
* @param rentalPrice The rental price in USDC
* @returns The transaction
*/
async rentToken(tokenId: number | bigint, duration: number | bigint, rentalPrice: bigint | string) {
return await this.contract.rentToken(tokenId, duration, rentalPrice);
}
/**
* End a rental early (can be called by either the owner or the renter)
* @param tokenId The ID of the token to end rental for
* @returns The transaction
*/
async endRental(tokenId: number | bigint) {
return await this.contract.endRental(tokenId);
}
/**
* Extend a rental period
* @param tokenId The ID of the token to extend rental for
* @param additionalDuration The additional duration in seconds
* @param additionalPayment The additional payment in USDC
* @returns The transaction
*/
async extendRental(tokenId: number | bigint, additionalDuration: number | bigint, additionalPayment: bigint | string) {
return await this.contract.extendRental(tokenId, additionalDuration, additionalPayment);
}
/**
* Check if a token is currently rented
* @param tokenId The ID of the token to check
* @returns Whether the token is rented
*/
async isRented(tokenId: number | bigint): Promise<boolean> {
return await this.contract.isRented(tokenId);
}
/**
* Get rental information for a token
* @param tokenId The ID of the token to get rental info for
* @returns The rental information
*/
async getRentalInfo(tokenId: number | bigint): Promise<RentalInfo> {
const [renter, startTime, endTime, rentalPrice, active] = await this.contract.getRentalInfo(tokenId);
return { renter, startTime, endTime, rentalPrice, active };
}
/**
* Check if a user has any active rentals
* @param user The user address to check
* @returns Whether the user has active rentals
*/
async hasActiveRentals(user: string): Promise<boolean> {
return await this.contract.hasActiveRentals(user);
}
/**
* Get the USDC token address
* @returns The address of the USDC token contract
*/
async getUsdcTokenAddress(): Promise<string> {
return await this.contract.usdcToken();
}
/**
* Get the platform commission percentage
* @returns The platform commission percentage in basis points (e.g., 500 = 5%)
*/
async getPlatformCommissionPercentage(): Promise<bigint> {
return await this.contract.platformCommissionPercentage();
}
/**
* Get the platform address
* @returns The address that receives platform commission
*/
async getPlatformAddress(): Promise<string> {
return await this.contract.platformAddress();
}
/**
* Check if the contract is paused
* @returns Whether the contract is paused
*/
async paused(): Promise<boolean> {
return await this.contract.paused();
}
/**
* Pause the contract (requires OWNER_ROLE)
* @returns The transaction
*/
async pause() {
return await this.contract.pause();
}
/**
* Unpause the contract (requires OWNER_ROLE)
* @returns The transaction
*/
async unpause() {
return await this.contract.unpause();
}
}