UNPKG

@paulstinchcombe/kami721c-sdk

Version:

SDK for interacting with KAMI721C NFT contracts

556 lines (497 loc) 17.2 kB
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(); } }