diff --git a/packages/core-sdk/package.json b/packages/core-sdk/package.json index 5bbbb09e..3af853cf 100644 --- a/packages/core-sdk/package.json +++ b/packages/core-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@story-protocol/core-sdk", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "description": "Story Protocol Core SDK", "main": "dist/story-protocol-core-sdk.cjs.js", "module": "dist/story-protocol-core-sdk.esm.js", diff --git a/packages/core-sdk/src/abi/generated.ts b/packages/core-sdk/src/abi/generated.ts index 4b514ca9..958a71b5 100644 --- a/packages/core-sdk/src/abi/generated.ts +++ b/packages/core-sdk/src/abi/generated.ts @@ -2492,6 +2492,621 @@ export const licenseRegistryConfig = { abi: licenseRegistryAbi, } as const; +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// LicenseToken +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * + */ +export const licenseTokenAbi = [ + { type: "constructor", inputs: [], stateMutability: "nonpayable" }, + { + type: "error", + inputs: [{ name: "target", internalType: "address", type: "address" }], + name: "AddressEmptyCode", + }, + { + type: "error", + inputs: [{ name: "implementation", internalType: "address", type: "address" }], + name: "ERC1967InvalidImplementation", + }, + { type: "error", inputs: [], name: "ERC1967NonPayable" }, + { type: "error", inputs: [], name: "ERC721EnumerableForbiddenBatchMint" }, + { + type: "error", + inputs: [ + { name: "sender", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + { name: "owner", internalType: "address", type: "address" }, + ], + name: "ERC721IncorrectOwner", + }, + { + type: "error", + inputs: [ + { name: "operator", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + ], + name: "ERC721InsufficientApproval", + }, + { + type: "error", + inputs: [{ name: "approver", internalType: "address", type: "address" }], + name: "ERC721InvalidApprover", + }, + { + type: "error", + inputs: [{ name: "operator", internalType: "address", type: "address" }], + name: "ERC721InvalidOperator", + }, + { + type: "error", + inputs: [{ name: "owner", internalType: "address", type: "address" }], + name: "ERC721InvalidOwner", + }, + { + type: "error", + inputs: [{ name: "receiver", internalType: "address", type: "address" }], + name: "ERC721InvalidReceiver", + }, + { + type: "error", + inputs: [{ name: "sender", internalType: "address", type: "address" }], + name: "ERC721InvalidSender", + }, + { + type: "error", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "ERC721NonexistentToken", + }, + { + type: "error", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "index", internalType: "uint256", type: "uint256" }, + ], + name: "ERC721OutOfBoundsIndex", + }, + { type: "error", inputs: [], name: "FailedInnerCall" }, + { type: "error", inputs: [], name: "Governance__InconsistentState" }, + { type: "error", inputs: [], name: "Governance__OnlyProtocolAdmin" }, + { + type: "error", + inputs: [{ name: "interfaceName", internalType: "string", type: "string" }], + name: "Governance__UnsupportedInterface", + }, + { type: "error", inputs: [], name: "Governance__ZeroAddress" }, + { type: "error", inputs: [], name: "InvalidInitialization" }, + { + type: "error", + inputs: [ + { name: "licenseTemplate", internalType: "address", type: "address" }, + { + name: "anotherLicenseTemplate", + internalType: "address", + type: "address", + }, + ], + name: "LicenseToken__AllLicenseTokensMustFromSameLicenseTemplate", + }, + { type: "error", inputs: [], name: "LicenseToken__CallerNotLicensingModule" }, + { + type: "error", + inputs: [ + { name: "tokenId", internalType: "uint256", type: "uint256" }, + { name: "expiredAt", internalType: "uint256", type: "uint256" }, + { name: "currentTimestamp", internalType: "uint256", type: "uint256" }, + ], + name: "LicenseToken__LicenseTokenExpired", + }, + { + type: "error", + inputs: [ + { name: "tokenId", internalType: "uint256", type: "uint256" }, + { name: "iPowner", internalType: "address", type: "address" }, + { name: "tokenOwner", internalType: "address", type: "address" }, + ], + name: "LicenseToken__NotLicenseTokenOwner", + }, + { type: "error", inputs: [], name: "LicenseToken__NotTransferable" }, + { + type: "error", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "LicenseToken__RevokedLicense", + }, + { type: "error", inputs: [], name: "LicenseToken__ZeroDisputeModule" }, + { type: "error", inputs: [], name: "LicenseToken__ZeroLicensingModule" }, + { type: "error", inputs: [], name: "NotInitializing" }, + { + type: "error", + inputs: [ + { name: "value", internalType: "uint256", type: "uint256" }, + { name: "length", internalType: "uint256", type: "uint256" }, + ], + name: "StringsInsufficientHexLength", + }, + { type: "error", inputs: [], name: "UUPSUnauthorizedCallContext" }, + { + type: "error", + inputs: [{ name: "slot", internalType: "bytes32", type: "bytes32" }], + name: "UUPSUnsupportedProxiableUUID", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "owner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "approved", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "tokenId", + internalType: "uint256", + type: "uint256", + indexed: true, + }, + ], + name: "Approval", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "owner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "operator", + internalType: "address", + type: "address", + indexed: true, + }, + { name: "approved", internalType: "bool", type: "bool", indexed: false }, + ], + name: "ApprovalForAll", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "_fromTokenId", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + { + name: "_toTokenId", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "BatchMetadataUpdate", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "newGovernance", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "GovernanceUpdated", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "version", + internalType: "uint64", + type: "uint64", + indexed: false, + }, + ], + name: "Initialized", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "minter", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "receiver", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "tokenId", + internalType: "uint256", + type: "uint256", + indexed: true, + }, + ], + name: "LicenseTokenMinted", + }, + { + type: "event", + anonymous: false, + inputs: [ + { name: "from", internalType: "address", type: "address", indexed: true }, + { name: "to", internalType: "address", type: "address", indexed: true }, + { + name: "tokenId", + internalType: "uint256", + type: "uint256", + indexed: true, + }, + ], + name: "Transfer", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "implementation", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "Upgraded", + }, + { + type: "function", + inputs: [], + name: "UPGRADE_INTERFACE_VERSION", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "to", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "owner", internalType: "address", type: "address" }], + name: "balanceOf", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "holder", internalType: "address", type: "address" }, + { name: "tokenIds", internalType: "uint256[]", type: "uint256[]" }, + ], + name: "burnLicenseTokens", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getApproved", + outputs: [{ name: "", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getExpirationTime", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "getGovernance", + outputs: [{ name: "", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getLicenseTemplate", + outputs: [{ name: "", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getLicenseTermsId", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getLicenseTokenMetadata", + outputs: [ + { + name: "", + internalType: "struct ILicenseToken.LicenseTokenMetadata", + type: "tuple", + components: [ + { name: "licensorIpId", internalType: "address", type: "address" }, + { name: "licenseTemplate", internalType: "address", type: "address" }, + { name: "licenseTermsId", internalType: "uint256", type: "uint256" }, + { name: "transferable", internalType: "bool", type: "bool" }, + { name: "mintedAt", internalType: "uint256", type: "uint256" }, + { name: "expiresAt", internalType: "uint256", type: "uint256" }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "getLicensorIpId", + outputs: [{ name: "", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "governance", internalType: "address", type: "address" }, + { name: "imageUrl", internalType: "string", type: "string" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "operator", internalType: "address", type: "address" }, + ], + name: "isApprovedForAll", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "isLicenseTokenRevoked", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "licensingModule", + outputs: [{ name: "", internalType: "contract ILicensingModule", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "licensorIpId", internalType: "address", type: "address" }, + { name: "licenseTemplate", internalType: "address", type: "address" }, + { name: "licenseTermsId", internalType: "uint256", type: "uint256" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + { name: "minter", internalType: "address", type: "address" }, + { name: "receiver", internalType: "address", type: "address" }, + ], + name: "mintLicenseTokens", + outputs: [{ name: "startLicenseTokenId", internalType: "uint256", type: "uint256" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "name", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], + name: "ownerOf", + outputs: [{ name: "", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "proxiableUUID", + outputs: [{ name: "", internalType: "bytes32", type: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "to", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "to", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + { name: "data", internalType: "bytes", type: "bytes" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "operator", internalType: "address", type: "address" }, + { name: "approved", internalType: "bool", type: "bool" }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "newDisputeModule", internalType: "address", type: "address" }], + name: "setDisputeModule", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "newGovernance", internalType: "address", type: "address" }], + name: "setGovernance", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "url", internalType: "string", type: "string" }], + name: "setLicensingImageUrl", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "newLicensingModule", internalType: "address", type: "address" }], + name: "setLicensingModule", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "interfaceId", internalType: "bytes4", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "symbol", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "index", internalType: "uint256", type: "uint256" }], + name: "tokenByIndex", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "index", internalType: "uint256", type: "uint256" }, + ], + name: "tokenOfOwnerByIndex", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [{ name: "id", internalType: "uint256", type: "uint256" }], + name: "tokenURI", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "totalMintedTokens", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "totalSupply", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "to", internalType: "address", type: "address" }, + { name: "tokenId", internalType: "uint256", type: "uint256" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "newImplementation", internalType: "address", type: "address" }, + { name: "data", internalType: "bytes", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "childIpId", internalType: "address", type: "address" }, + { name: "childIpOwner", internalType: "address", type: "address" }, + { name: "tokenIds", internalType: "uint256[]", type: "uint256[]" }, + ], + name: "validateLicenseTokensForDerivative", + outputs: [ + { name: "licenseTemplate", internalType: "address", type: "address" }, + { name: "licensorIpIds", internalType: "address[]", type: "address[]" }, + { name: "licenseTermsIds", internalType: "uint256[]", type: "uint256[]" }, + ], + stateMutability: "view", + }, +] as const; + +/** + * + */ +export const licenseTokenAddress = { + 1513: "0xD40b7bCA204f96a346021e31c9ad54FF495226e7", +} as const; + +/** + * + */ +export const licenseTokenConfig = { + address: licenseTokenAddress, + abi: licenseTokenAbi, +} as const; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // LicensingModule ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -5130,6 +5745,19 @@ export class IpAssetRegistryClient extends IpAssetRegistryReadOnlyClient { // Contract IpRoyaltyVaultImpl ============================================================= +/** + * IpRoyaltyVaultImplRevenueTokenClaimedEvent + * + * @param claimer address + * @param token address + * @param amount uint256 + */ +export type IpRoyaltyVaultImplRevenueTokenClaimedEvent = { + claimer: Address; + token: Address; + amount: bigint; +}; + /** * IpRoyaltyVaultImplRoyaltyTokensCollectedEvent * @@ -5214,6 +5842,47 @@ export class IpRoyaltyVaultImplEventClient { this.rpcClient = rpcClient; } + /** + * event RevenueTokenClaimed for contract IpRoyaltyVaultImpl + */ + public watchRevenueTokenClaimedEvent( + onLogs: (txHash: Hex, ev: Partial) => void, + ): WatchContractEventReturnType { + return this.rpcClient.watchContractEvent({ + abi: ipRoyaltyVaultImplAbi, + address: this.address, + eventName: "RevenueTokenClaimed", + onLogs: (evs) => { + evs.forEach((it) => onLogs(it.transactionHash, it.args)); + }, + }); + } + + /** + * parse tx receipt event RevenueTokenClaimed for contract IpRoyaltyVaultImpl + */ + public parseTxRevenueTokenClaimedEvent( + txReceipt: TransactionReceipt, + ): Array { + const targetLogs: Array = []; + for (const log of txReceipt.logs) { + try { + const event = decodeEventLog({ + abi: ipRoyaltyVaultImplAbi, + eventName: "RevenueTokenClaimed", + data: log.data, + topics: log.topics, + }); + if (event.eventName === "RevenueTokenClaimed") { + targetLogs.push(event.args); + } + } catch (e) { + /* empty */ + } + } + return targetLogs; + } + /** * event RoyaltyTokensCollected for contract IpRoyaltyVaultImpl */ @@ -6791,6 +7460,47 @@ export class LicenseRegistryClient extends LicenseRegistryReadOnlyClient { } } +// Contract LicenseToken ============================================================= + +/** + * LicenseTokenOwnerOfRequest + * + * @param tokenId uint256 + */ +export type LicenseTokenOwnerOfRequest = { + tokenId: bigint; +}; + +export type LicenseTokenOwnerOfResponse = Address; + +/** + * contract LicenseToken readonly method + */ +export class LicenseTokenReadOnlyClient { + protected readonly rpcClient: PublicClient; + public readonly address: Address; + + constructor(rpcClient: PublicClient, address?: Address) { + this.address = address || getAddress(licenseTokenAddress, rpcClient.chain?.id); + this.rpcClient = rpcClient; + } + + /** + * method ownerOf for contract LicenseToken + * + * @param request LicenseTokenOwnerOfRequest + * @return Promise + */ + public async ownerOf(request: LicenseTokenOwnerOfRequest): Promise { + return await this.rpcClient.readContract({ + abi: licenseTokenAbi, + address: this.address, + functionName: "ownerOf", + args: [request.tokenId], + }); + } +} + // Contract LicensingModule ============================================================= /** diff --git a/packages/core-sdk/src/client.ts b/packages/core-sdk/src/client.ts index 4e57e978..98ccc76a 100644 --- a/packages/core-sdk/src/client.ts +++ b/packages/core-sdk/src/client.ts @@ -141,7 +141,7 @@ export class StoryClient { */ public get license(): LicenseClient { if (this._license === null) { - this._license = new LicenseClient(this.rpcClient, this.wallet, this.storyClient); + this._license = new LicenseClient(this.rpcClient, this.wallet); } return this._license; diff --git a/packages/core-sdk/src/resources/ipAsset.ts b/packages/core-sdk/src/resources/ipAsset.ts index b385c41a..1deaccf8 100644 --- a/packages/core-sdk/src/resources/ipAsset.ts +++ b/packages/core-sdk/src/resources/ipAsset.ts @@ -14,34 +14,37 @@ import { import { IpAssetRegistryClient, LicenseRegistryReadOnlyClient, + LicenseTokenReadOnlyClient, LicensingModuleClient, PiLicenseTemplateClient, SimpleWalletClient, } from "../abi/generated"; export class IPAssetClient { - private readonly rpcClient: PublicClient; - private readonly chainId: SupportedChainIds; - public ipAssetRegistryClient: IpAssetRegistryClient; public licensingModuleClient: LicensingModuleClient; + public ipAssetRegistryClient: IpAssetRegistryClient; public licenseTemplateClient: PiLicenseTemplateClient; - private licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; + public licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; + public licenseTokenReadOnlyClient: LicenseTokenReadOnlyClient; + private readonly rpcClient: PublicClient; + private readonly chainId: SupportedChainIds; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: SupportedChainIds) { + this.licensingModuleClient = new LicensingModuleClient(rpcClient, wallet); + this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); + this.licenseTemplateClient = new PiLicenseTemplateClient(rpcClient, wallet); + this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(rpcClient); + this.licenseTokenReadOnlyClient = new LicenseTokenReadOnlyClient(rpcClient); this.rpcClient = rpcClient; this.chainId = chainId; - this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); - this.licensingModuleClient = new LicensingModuleClient(this.rpcClient, wallet); - this.licenseTemplateClient = new PiLicenseTemplateClient(this.rpcClient, wallet); - this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(this.rpcClient); } /** * Registers an NFT as IP, creating a corresponding IP record. * @param request The request object that contains all data needed to register IP. - * @param request.tokenContract The address of the NFT. - * @param request.tokenId The token identifier of the NFT. - * @param request.txOptions [Optional] The transaction options. + * @param request.tokenContract The address of the NFT. + * @param request.tokenId The token identifier of the NFT. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash and optional IP ID if waitForTxn is set to true. * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, resolverAddr, metadataProviderAddress, metadata) */ @@ -75,26 +78,28 @@ export class IPAssetClient { * All IPs attached default license terms by default. * The derivative IP owner must be the caller or an authorized operator. * @param request The request object that contains all data needed to register derivative IP. - * @param request.childIpId The derivative IP ID. - * @param request.parentIpIds The parent IP IDs. - * @param request.licenseTermsIds The IDs of the license terms that the parent IP supports. - * @param request.txOptions [Optional] The transaction options. + * @param request.childIpId The derivative IP ID. + * @param request.parentIpIds The parent IP IDs. + * @param request.licenseTermsIds The IDs of the license terms that the parent IP supports. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash. */ public async registerDerivative( request: RegisterDerivativeRequest, ): Promise { try { - if (!(await this.isIpIdRegistered(request.childIpId))) { - throw new Error("IP asset must be registered before registering derivative"); + const isChildIpIdRegistered = await this.isRegistered(request.childIpId); + if (!isChildIpIdRegistered) { + throw new Error(`The child IP with id ${request.childIpId} is not registered.`); } for (const parentId of request.parentIpIds) { - if (!(await this.isIpIdRegistered(parentId))) { - throw new Error("Parent IP asset must be registered before registering derivative"); + const isParentIpIdRegistered = await this.isRegistered(parentId); + if (!isParentIpIdRegistered) { + throw new Error(`The parent IP with id ${parentId} is not registered.`); } } if (request.parentIpIds.length !== request.licenseTermsIds.length) { - throw new Error("Parent IP IDs and License terms IDs must be provided in pairs"); + throw new Error("Parent IP IDs and License terms IDs must be provided in pairs."); } for (let i = 0; i < request.parentIpIds.length; i++) { const isAttachedLicenseTerms = @@ -105,7 +110,7 @@ export class IPAssetClient { }); if (!isAttachedLicenseTerms) { throw new Error( - "License terms must be attached to the parent ipId before registering derivative", + `License terms id ${request.licenseTermsIds[i]} must be attached to the parent ipId ${request.parentIpIds[i]} before registering derivative.`, ); } } @@ -134,19 +139,26 @@ export class IPAssetClient { * the license terms of the parent IPs issued with license tokens are attached to the derivative IP. * the caller must be the derivative IP owner or an authorized operator. * @param request The request object that contains all data needed to register derivative license tokens. - * @param request.childIpId The derivative IP ID. - * @param request.licenseTokenIds The IDs of the license tokens. - * @param request.txOptions [Optional] The transaction options. + * @param request.childIpId The derivative IP ID. + * @param request.licenseTokenIds The IDs of the license tokens. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash. */ public async registerDerivativeWithLicenseTokens( request: RegisterDerivativeWithLicenseTokensRequest, ): Promise { try { - if (!(await this.isIpIdRegistered(request.childIpId))) { - throw new Error( - "IP asset must be registered before registering derivative with license tokens", - ); + const isChildIpIdRegistered = await this.isRegistered(request.childIpId); + if (!isChildIpIdRegistered) { + throw new Error(`The child IP with id ${request.childIpId} is not registered.`); + } + for (const licenseTokenId of request.licenseTokenIds) { + const tokenOwnerAddress = await this.licenseTokenReadOnlyClient.ownerOf({ + tokenId: BigInt(licenseTokenId), + }); + if (!tokenOwnerAddress) { + throw new Error(`License token id ${licenseTokenId} must be owned by the caller.`); + } } const txHash = await this.licensingModuleClient.registerDerivativeWithLicenseTokens({ childIpId: request.childIpId, @@ -174,7 +186,7 @@ export class IPAssetClient { return isRegistered ? ipId : "0x"; } - private async isIpIdRegistered(ipId: Hex): Promise { + private async isRegistered(ipId: Hex): Promise { return await this.ipAssetRegistryClient.isRegistered({ id: ipId }); } } diff --git a/packages/core-sdk/src/resources/license.ts b/packages/core-sdk/src/resources/license.ts index 854f99d5..e837806f 100644 --- a/packages/core-sdk/src/resources/license.ts +++ b/packages/core-sdk/src/resources/license.ts @@ -1,12 +1,12 @@ import { PublicClient, zeroAddress } from "viem"; -import { StoryAPIClient } from "../clients/storyAPI"; import { IpAssetRegistryClient, LicenseRegistryEventClient, LicenseRegistryReadOnlyClient, LicensingModuleClient, PiLicenseTemplateClient, + PiLicenseTemplateReadOnlyClient, RoyaltyPolicyLapClient, SimpleWalletClient, } from "../abi/generated"; @@ -24,36 +24,36 @@ import { import { handleError } from "../utils/errors"; export class LicenseClient { - private readonly wallet: SimpleWalletClient; - private readonly rpcClient: PublicClient; - private readonly storyClient: StoryAPIClient; public licenseRegistryClient: LicenseRegistryEventClient; public licensingModuleClient: LicensingModuleClient; - private licenseTemplateClient: PiLicenseTemplateClient; - private royaltyPolicyLAPClient: RoyaltyPolicyLapClient; - private licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; public ipAssetRegistryClient: IpAssetRegistryClient; + public piLicenseTemplateReadOnlyClient: PiLicenseTemplateReadOnlyClient; + public licenseTemplateClient: PiLicenseTemplateClient; + public royaltyPolicyLAPClient: RoyaltyPolicyLapClient; + public licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; + private readonly rpcClient: PublicClient; + private readonly wallet: SimpleWalletClient; - constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, storyClient: StoryAPIClient) { - this.wallet = wallet; - this.rpcClient = rpcClient; - this.storyClient = storyClient; - this.licenseRegistryClient = new LicenseRegistryEventClient(this.rpcClient); - this.licenseTemplateClient = new PiLicenseTemplateClient(this.rpcClient, this.wallet); - this.licensingModuleClient = new LicensingModuleClient(this.rpcClient, this.wallet); - this.royaltyPolicyLAPClient = new RoyaltyPolicyLapClient(this.rpcClient, this.wallet); - this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(this.rpcClient); + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { + this.licensingModuleClient = new LicensingModuleClient(rpcClient, wallet); + this.licenseRegistryClient = new LicenseRegistryEventClient(rpcClient); + this.piLicenseTemplateReadOnlyClient = new PiLicenseTemplateReadOnlyClient(rpcClient); + this.licenseTemplateClient = new PiLicenseTemplateClient(rpcClient, wallet); + this.royaltyPolicyLAPClient = new RoyaltyPolicyLapClient(rpcClient, wallet); + this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(rpcClient); this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); + this.rpcClient = rpcClient; + this.wallet = wallet; } /** * Convenient function to register a PIL non commercial social remix license to the registry - * @param request The request object that contains all data needed to register a PIL non commercial social remix license. - * @param request.txOptions [Optional] The transaction options. + * @param request [Optional] The request object that contains all data needed to register a PIL non commercial social remix license. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the optional transaction hash and optional license terms Id. * @emits LicenseTermsRegistered (licenseTermsId, licenseTemplate, licenseTerms); */ public async registerNonComSocialRemixingPIL( - request: RegisterNonComSocialRemixingPILRequest, + request?: RegisterNonComSocialRemixingPILRequest, ): Promise { try { const licenseTerms: LicenseTerms = { @@ -79,11 +79,9 @@ export class LicenseClient { return { licenseTermsId: licenseTermsId.toString() }; } const txHash = await this.licenseTemplateClient.registerLicenseTerms({ terms: licenseTerms }); - - if (request.txOptions?.waitForTransaction) { + if (request?.txOptions?.waitForTransaction) { const txReceipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); const targetLogs = this.licenseTemplateClient.parseTxLicenseTermsRegisteredEvent(txReceipt); - return { txHash: txHash, licenseTermsId: targetLogs[0].licenseTermsId.toString() }; } else { return { txHash: txHash }; @@ -95,11 +93,10 @@ export class LicenseClient { /** * Convenient function to register a PIL commercial use license to the registry. * @param request The request object that contains all data needed to register a PIL commercial use license. - * @param request.mintingFee The fee to be paid when minting a license. - * @param request.currency The ERC20 token to be used to pay the minting fee. - * the token must be registered in story protocol. - * @param request.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.txOptions [Optional] The transaction options. + * @param request.mintingFee The fee to be paid when minting a license. + * @param request.currency The ERC20 token to be used to pay the minting fee and the token must be registered in story protocol. + * @param request.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the optional transaction hash and optional license terms Id. * @emits LicenseTermsRegistered (licenseTermsId, licenseTemplate, licenseTerms); */ @@ -129,9 +126,7 @@ export class LicenseClient { if (licenseTermsId !== 0) { return { licenseTermsId: licenseTermsId.toString() }; } - const txHash = await this.licenseTemplateClient.registerLicenseTerms({ terms: licenseTerms }); - if (request.txOptions?.waitForTransaction) { const txReceipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); const targetLogs = this.licenseTemplateClient.parseTxLicenseTermsRegisteredEvent(txReceipt); @@ -146,11 +141,11 @@ export class LicenseClient { /** * Convenient function to register a PIL commercial Remix license to the registry. * @param request The request object that contains all data needed to register license. - * @param request.mintingFee The fee to be paid when minting a license. - * @param request.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.txOptions [Optional] The transaction options. + * @param request.mintingFee The fee to be paid when minting a license. + * @param request.commercialRevShare Percentage of revenue that must be shared with the licensor. + * @param request.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. + * @param request.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the optional transaction hash and optional license terms Id. * @emits LicenseTermsRegistered (licenseTermsId, licenseTemplate, licenseTerms); */ @@ -181,7 +176,6 @@ export class LicenseClient { return { licenseTermsId: licenseTermsId.toString() }; } const txHash = await this.licenseTemplateClient.registerLicenseTerms({ terms: licenseTerms }); - if (request.txOptions?.waitForTransaction) { const txReceipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); const targetLogs = this.licenseTemplateClient.parseTxLicenseTermsRegisteredEvent(txReceipt); @@ -197,40 +191,48 @@ export class LicenseClient { /** * Attaches license terms to an IP. * @param request The request object that contains all data needed to attach license terms. - @param request.ipId The IP ID. - @param request.tokenAddress The address of the NFT. - @param request.licenseTemplate The address of the license template. - @param request.licenseTermsId The ID of the license terms. - @param request.ipId The address of the IP to which the license terms are attached. - @param request.licenseTemplate The address of the license template. - @param request.licenseTermsId The ID of the license terms. - * @param request.txOptions [Optional] The transaction options. + * @param request.ipId The address of the IP to which the license terms are attached. + * @param request.licenseTemplate The address of the license template. + * @param request.licenseTermsId The ID of the license terms. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash. */ public async attachLicenseTerms(request: AttachLicenseTermsRequest) { - const isRegistered = await this.ipAssetRegistryClient.isRegistered({ id: request.ipId }); - if (!isRegistered) { - throw new Error("IP asset must be registered before attaching license terms"); - } - const isAttachedLicenseTerms = - await this.licenseRegistryReadOnlyClient.hasIpAttachedLicenseTerms({ + try { + const isRegistered = await this.ipAssetRegistryClient.isRegistered({ id: request.ipId }); + if (!isRegistered) { + throw new Error(`The IP with id ${request.ipId} is not registered.`); + } + const isExisted = await this.piLicenseTemplateReadOnlyClient.exists({ + licenseTermsId: BigInt(request.licenseTermsId), + }); + if (!isExisted) { + throw new Error(`License terms id ${request.licenseTermsId} do not exist.`); + } + const isAttachedLicenseTerms = + await this.licenseRegistryReadOnlyClient.hasIpAttachedLicenseTerms({ + ipId: request.ipId, + licenseTemplate: request.licenseTemplate || this.licenseTemplateClient.address, + licenseTermsId: BigInt(request.licenseTermsId), + }); + if (isAttachedLicenseTerms) { + throw new Error( + `License terms id ${request.licenseTermsId} is already attached to the IP with id ${request.ipId}.`, + ); + } + const txHash = await this.licensingModuleClient.attachLicenseTerms({ ipId: request.ipId, licenseTemplate: request.licenseTemplate || this.licenseTemplateClient.address, licenseTermsId: BigInt(request.licenseTermsId), }); - if (isAttachedLicenseTerms) { - throw new Error("License terms are already attached to the IP"); - } - const txHash = await this.licensingModuleClient.attachLicenseTerms({ - ipId: request.ipId, - licenseTemplate: request.licenseTemplate || this.licenseTemplateClient.address, - licenseTermsId: BigInt(request.licenseTermsId), - }); - if (request.txOptions?.waitForTransaction) { - await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); - return { txHash: txHash }; - } else { - return { txHash: txHash }; + if (request.txOptions?.waitForTransaction) { + await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); + return { txHash: txHash }; + } else { + return { txHash: txHash }; + } + } catch (error) { + handleError(error, "Failed to attach license terms"); } } @@ -247,12 +249,12 @@ export class LicenseClient { * IP owners can configure the minting fee of their IPs or * configure the minting fee module to determine the minting fee. * @param request The request object that contains all data needed to mint license tokens. - * @param request.licensorIpId The licensor IP ID. - * @param request.licenseTemplate The address of the license template. - * @param request.licenseTermsId The ID of the license terms within the license template. - * @param request.amount The amount of license tokens to mint. - * @param request.receiver The address of the receiver. - * @param request.txOptions [Optional] The transaction options. + * @param request.licensorIpId The licensor IP ID. + * @param request.licenseTemplate The address of the license template. + * @param request.licenseTermsId The ID of the license terms within the license template. + * @param request.amount The amount of license tokens to mint. + * @param request.receiver The address of the receiver. + * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash and optional license token ID if waitForTxn is set to true. * @emits LicenseTokensMinted (msg.sender, licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, startLicenseTokenId); */ @@ -260,6 +262,18 @@ export class LicenseClient { request: MintLicenseTokensRequest, ): Promise { try { + const isLicenseIpIdRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: request.licensorIpId, + }); + if (!isLicenseIpIdRegistered) { + throw new Error(`The licensor IP with id ${request.licensorIpId} is not registered.`); + } + const isExisted = await this.piLicenseTemplateReadOnlyClient.exists({ + licenseTermsId: BigInt(request.licenseTermsId), + }); + if (!isExisted) { + throw new Error(`License terms id ${request.licenseTermsId} do not exist.`); + } const isAttachedLicenseTerms = await this.licenseRegistryReadOnlyClient.hasIpAttachedLicenseTerms({ ipId: request.licensorIpId, @@ -267,7 +281,9 @@ export class LicenseClient { licenseTermsId: BigInt(request.licenseTermsId), }); if (!isAttachedLicenseTerms) { - throw new Error("License terms are not attached to the IP"); + throw new Error( + `License terms id ${request.licenseTermsId} is not attached to the IP with id ${request.licensorIpId}.`, + ); } const txHash = await this.licensingModuleClient.mintLicenseTokens({ licensorIpId: request.licensorIpId, diff --git a/packages/core-sdk/src/resources/royalty.ts b/packages/core-sdk/src/resources/royalty.ts index dbb13d1e..39026470 100644 --- a/packages/core-sdk/src/resources/royalty.ts +++ b/packages/core-sdk/src/resources/royalty.ts @@ -11,8 +11,11 @@ import { RoyaltyVaultAddress, SnapshotRequest, SnapshotResponse, + claimRevenueRequest, + claimRevenueResponse, } from "../types/resources/royalty"; import { + IpAssetRegistryClient, IpRoyaltyVaultImplClient, RoyaltyModuleClient, RoyaltyPolicyLapClient, @@ -20,24 +23,24 @@ import { } from "../abi/generated"; export class RoyaltyClient { - private readonly wallet: SimpleWalletClient; - private readonly rpcClient: PublicClient; - public royaltyVaultImplClient: IpRoyaltyVaultImplClient; - public royaltyPolicyLAPClient: RoyaltyPolicyLapClient; + public royaltyPolicyLapClient: RoyaltyPolicyLapClient; public royaltyModuleClient: RoyaltyModuleClient; + public ipAssetRegistryClient: IpAssetRegistryClient; + private readonly rpcClient: PublicClient; + private readonly wallet: SimpleWalletClient; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { + this.royaltyPolicyLapClient = new RoyaltyPolicyLapClient(rpcClient, wallet); + this.royaltyModuleClient = new RoyaltyModuleClient(rpcClient, wallet); + this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); this.rpcClient = rpcClient; this.wallet = wallet; - this.royaltyVaultImplClient = new IpRoyaltyVaultImplClient(this.rpcClient, this.wallet); - this.royaltyPolicyLAPClient = new RoyaltyPolicyLapClient(this.rpcClient, this.wallet); - this.royaltyModuleClient = new RoyaltyModuleClient(this.rpcClient, this.wallet); } /** * Allows ancestors to claim the royalty tokens and any accrued revenue tokens * @param request - The request object that contains all data needed to collect royalty tokens. - * @param request.ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to. + * @param request.parentIpId The ip id of the ancestor to whom the royalty tokens belong to. * @param request.royaltyVaultIpId The id of the royalty vault. * @param request.txOptions [Optional] The transaction options. * @returns A Promise that resolves to an object containing the transaction hash and optional the amount of royalty tokens collected if waitForTxn is set to true. @@ -47,6 +50,12 @@ export class RoyaltyClient { request: CollectRoyaltyTokensRequest, ): Promise { try { + const isParentIpIdRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: request.parentIpId, + }); + if (!isParentIpIdRegistered) { + throw new Error(`The parent IP with id ${request.parentIpId} is not registered.`); + } const proxyAddress = await this.getRoyaltyVaultProxyAddress(request.royaltyVaultIpId); const ipRoyaltyVault = new IpRoyaltyVaultImplClient( this.rpcClient, @@ -54,7 +63,7 @@ export class RoyaltyClient { proxyAddress, ); const txHash = await ipRoyaltyVault.collectRoyaltyTokens({ - ancestorIpId: request.ancestorIpId, + ancestorIpId: request.parentIpId, }); if (request.txOptions?.waitForTransaction) { const txReceipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); @@ -85,6 +94,18 @@ export class RoyaltyClient { request: PayRoyaltyOnBehalfRequest, ): Promise { try { + const isReceiverRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: request.receiverIpId, + }); + if (!isReceiverRegistered) { + throw new Error(`The receiver IP with id ${request.receiverIpId} is not registered`); + } + const isPayerRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: request.payerIpId, + }); + if (!isPayerRegistered) { + throw new Error(`The payer IP with id ${request.payerIpId} is not registered`); + } const txHash = await this.royaltyModuleClient.payRoyaltyOnBehalf({ receiverIpId: request.receiverIpId, payerIpId: request.payerIpId, @@ -131,6 +152,40 @@ export class RoyaltyClient { handleError(error, "Failed to calculate claimable revenue"); } } + + /** + * Allows token holders to claim by a list of snapshot ids based on the token balance at certain snapshot + * @param request - The request object that contains all data needed to claim revenue. + * @param request.snapshotIds The list of snapshot ids. + * @param request.royaltyVaultIpId The id of the royalty vault. + * @param request.token The revenue token to claim. + * @param request.txOptions [Optional] The transaction options. + * @returns A Promise that resolves to an object containing the transaction hash and optional claimableToken if waitForTxn is set to true. + * @emits RevenueTokenClaimed (claimer, token, amount). + */ + public async claimRevenue(request: claimRevenueRequest): Promise { + try { + const proxyAddress = await this.getRoyaltyVaultProxyAddress(request.royaltyVaultIpId); + const ipRoyaltyVault = new IpRoyaltyVaultImplClient( + this.rpcClient, + this.wallet, + proxyAddress, + ); + const txHash = await ipRoyaltyVault.claimRevenueBySnapshotBatch({ + snapshotIds: request.snapshotIds.map((id) => BigInt(id)), + token: request.token, + }); + if (request.txOptions?.waitForTransaction) { + const txReceipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); + const targetLogs = ipRoyaltyVault.parseTxRevenueTokenClaimedEvent(txReceipt); + return { txHash, claimableToken: targetLogs[0].amount }; + } else { + return { txHash }; + } + } catch (error) { + handleError(error, "Failed to claim revenue"); + } + } /** * Snapshots the claimable revenue and royalty token amounts. * @param request - The request object that contains all data needed to snapshot. @@ -161,9 +216,18 @@ export class RoyaltyClient { } private async getRoyaltyVaultProxyAddress(royaltyVaultIpId: Hex): Promise { - const data = await this.royaltyPolicyLAPClient.getRoyaltyData({ + const isRoyaltyVaultIpIdRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: royaltyVaultIpId, + }); + if (!isRoyaltyVaultIpIdRegistered) { + throw new Error(`The royalty vault IP with id ${royaltyVaultIpId} is not registered.`); + } + const data = await this.royaltyPolicyLapClient.getRoyaltyData({ ipId: royaltyVaultIpId, }); + if (!data[1] || data[1] === "0x") { + throw new Error(`The royalty vault IP with id ${royaltyVaultIpId} address is not set.`); + } return data[1]; } } diff --git a/packages/core-sdk/src/types/resources/royalty.ts b/packages/core-sdk/src/types/resources/royalty.ts index 753dd6a6..2303e370 100644 --- a/packages/core-sdk/src/types/resources/royalty.ts +++ b/packages/core-sdk/src/types/resources/royalty.ts @@ -22,7 +22,7 @@ export type RoyaltyContext = { }; export type CollectRoyaltyTokensRequest = { - ancestorIpId: Hex; + parentIpId: Hex; royaltyVaultIpId: Hex; txOptions?: TxOptions; }; @@ -67,6 +67,18 @@ export type SnapshotRequest = { txOptions?: TxOptions; }; +export type claimRevenueRequest = { + snapshotIds: string[]; + token: Hex; + royaltyVaultIpId: Hex; + txOptions?: TxOptions; +}; + +export type claimRevenueResponse = { + txHash: string; + claimableToken?: bigint; +}; + export type SnapshotResponse = { txHash: string; snapshotId?: bigint; diff --git a/packages/core-sdk/test/integration/storyTestNet/royalty.storyTest.test.ts b/packages/core-sdk/test/integration/storyTestNet/royalty.storyTest.test.ts index 4744605d..db0be8c8 100644 --- a/packages/core-sdk/test/integration/storyTestNet/royalty.storyTest.test.ts +++ b/packages/core-sdk/test/integration/storyTestNet/royalty.storyTest.test.ts @@ -15,7 +15,7 @@ import { MockERC721, MockERC20, getTokenId } from "./util"; chai.use(chaiAsPromised); const expect = chai.expect; -let startTokenId = 180; +let startTokenId = 198; let snapshotId: bigint; describe.skip("Test royalty Functions", () => { let client: StoryClient; @@ -43,7 +43,6 @@ describe.skip("Test royalty Functions", () => { let ipId1: Hex; let ipId2: Hex; const getIpId = async (): Promise => { - console.log("startTokenId", startTokenId); const tokenId = await getTokenId(startTokenId++); const response = await client.ipAsset.register({ tokenContract: MockERC721, @@ -63,7 +62,6 @@ describe.skip("Test royalty Functions", () => { waitForTransaction: true, }, }); - console.log("licenseTermsId", response.licenseTermsId); return response.licenseTermsId!; }; @@ -80,7 +78,6 @@ describe.skip("Test royalty Functions", () => { before(async () => { ipId1 = await getIpId(); ipId2 = await getIpId(); - console.log("ipId1", ipId1, "ipId2", ipId2); const licenseTermsId = await getCommercialPolicyId(); await attachLicenseTerms(ipId1, licenseTermsId); await client.ipAsset.registerDerivative({ @@ -125,7 +122,7 @@ describe.skip("Test royalty Functions", () => { abi: abi, address: MockERC20, functionName: "approve", - args: [client.royalty.royaltyPolicyLAPClient.address, BigInt(100)], + args: [client.royalty.royaltyPolicyLapClient.address, BigInt(100)], account: walletClient.account, }); const approveHash = await walletClient.writeContract(call); @@ -181,12 +178,11 @@ describe.skip("Test royalty Functions", () => { expect(response.txHash).to.be.a("string").not.empty; expect(response.snapshotId).to.be.a("bigint"); snapshotId = response.snapshotId!; - console.log("snapshot", snapshotId); }); it("should not throw error when collect royalty tokens", async () => { const response = await client.royalty.collectRoyaltyTokens({ - ancestorIpId: ipId1, + parentIpId: ipId1, royaltyVaultIpId: ipId2, txOptions: { waitForTransaction: true, @@ -208,5 +204,18 @@ describe.skip("Test royalty Functions", () => { }); expect(response).to.be.a("bigint"); }); + + it("should not throw error when claim revenue", async () => { + const response = await client.royalty.claimRevenue({ + royaltyVaultIpId: ipId2, + snapshotIds: [snapshotId.toString()], + token: "0xA36F2A4A02f5C215d1b3630f71A4Ff55B5492AAE", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(response.claimableToken).to.be.a("bigint"); + }); }); }); diff --git a/packages/core-sdk/test/unit/client.test.ts b/packages/core-sdk/test/unit/client.test.ts index f49b30de..3cc93148 100644 --- a/packages/core-sdk/test/unit/client.test.ts +++ b/packages/core-sdk/test/unit/client.test.ts @@ -71,7 +71,7 @@ describe("Test StoryClient", function () { }); it("should return client license", () => { - const license = new LicenseClient(rpcClient, wallet, storyAPIClient); + const license = new LicenseClient(rpcClient, wallet); expect(client.license).to.not.equal(null); expect(client.license).to.not.equal(undefined); }); diff --git a/packages/core-sdk/test/unit/resources/ipAsset.test.ts b/packages/core-sdk/test/unit/resources/ipAsset.test.ts index 40ede5d1..837fcf9b 100644 --- a/packages/core-sdk/test/unit/resources/ipAsset.test.ts +++ b/packages/core-sdk/test/unit/resources/ipAsset.test.ts @@ -26,79 +26,59 @@ describe("Test IpAssetClient", function () { }); describe("Test ipAssetClient.register", async function () { - it("should throw readContract error if readContract throws an error", async () => { - rpcMock.readContract = sinon.stub().rejects(new Error("readContract error")); - try { - await ipAssetClient.register({ - tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", - tokenId: "3", - txOptions: { - waitForTransaction: false, - }, - }); - } catch (err) { - expect((err as Error).message).includes("readContract error"); - } - }); - it("should not throw error when register", async function () { - const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; - rpcMock.readContract = sinon.stub().resolves(); - rpcMock.simulateContract = sinon.stub().resolves({ request: null }); - walletMock.writeContract = sinon.stub().resolves(txHash); + it("should return ipId when register given tokenId have registered", async function () { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "ipId") + .resolves("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(true); const res = await ipAssetClient.register({ tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", tokenId: "3", - txOptions: { - waitForTransaction: false, - }, }); - expect(res.txHash).equal(txHash); + expect(res.ipId).equal("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); + expect(res.txHash).to.be.undefined; }); - it("should return ipId if contract is successful read", async function () { - const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; - rpcMock.readContract = sinon.stub().resolves(txHash); - rpcMock.simulateContract = sinon.stub().resolves({ request: null }); - walletMock.writeContract = sinon.stub().resolves(txHash); + it("should return txHash when register given tokenId have no registered", async function () { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "ipId") + .resolves("0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(false); + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "register") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); const res = await ipAssetClient.register({ tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", tokenId: "3", - txOptions: { - waitForTransaction: false, - }, }); - expect(res.ipId).equal(txHash); + expect(res.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); }); - it("should not throw error when creating a IP and wait for transaction confirmed", async function () { - const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; - rpcMock.readContract = sinon.stub().resolves(); - rpcMock.simulateContract = sinon.stub().resolves({ request: null }); - walletMock.writeContract = sinon.stub().resolves(txHash); - rpcMock.waitForTransactionReceipt = sinon.stub().resolves({ - logs: [ - { - address: "0x30c89bcb41277f09b18df0375b9438909e193bf0", - topics: [ - "0x02ad3a2e0356b65fdfe4a73c825b78071ae469db35162978518b8c258abb3767", - "0x00000000000000000000000000000000000000000000000000000000000005e9", - "0x0000000000000000000000007c0004c6d352bc0a0531aad46d33a03d9d51ab1d", - "0x000000000000000000000000000000000000000000000000000000000000000d", - ], - data: "0x000000000000000000000000d142822dc1674154eaf4ddf38bbf7ef8f0d8ece4000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000660cc2100000000000000000000000000000000000000000000000000000000000000014313531333a204d6f636b45524337323120233133000000000000000000000000000000000000000000000000000000000000000000000000000000000000002368747470733a2f2f73746f727970726f746f636f6c2e78797a2f6572633732312f31330000000000000000000000000000000000000000000000000000000000", - blockNumber: 478104n, - transactionHash: "0x10b563fc5722b9648ad95389280e71a16e10fab8cbd0aff1da18516c35704562", - transactionIndex: 1, - blockHash: "0xe9a24656b3c6c4ea66ad467300e39b63ae89af97481744d737d63ebe91857e34", - logIndex: 2, - removed: false, - }, - ], - }); + it("should return ipId and txHash when register a IP and given waitForTransaction of true and tokenId is not registered ", async function () { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "ipId") + .resolves("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(false); + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "register") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "parseTxIpRegisteredEvent").returns([ + { + ipId: "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + chainId: 0n, + tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + tokenId: 0n, + name: "", + uri: "", + registrationDate: 0n, + }, + ]); const response = await ipAssetClient.register({ tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -107,34 +87,255 @@ describe("Test IpAssetClient", function () { waitForTransaction: true, }, }); - expect(response.txHash).equal(txHash); - expect(response.ipId).equals("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); - const response2 = await ipAssetClient.register({ - tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", - tokenId: "3", - txOptions: { - waitForTransaction: true, - }, - }); - expect(response2.txHash).equal(txHash); - expect(response2.ipId).equals("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); + expect(response.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); + expect(response.ipId).equals("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); }); it("should throw error when request fails", async function () { - rpcMock.simulateContract = sinon.stub().resolves({ request: null }); - rpcMock.readContract = sinon.stub().resolves(); - walletMock.writeContract = sinon.stub().rejects(new Error("http 500")); - - await expect( - ipAssetClient.register({ + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "ipId") + .resolves("0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(false); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "register").throws(new Error("revert error")); + try { + await ipAssetClient.register({ tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", tokenId: "3", txOptions: { waitForTransaction: true, }, - }), - ).to.be.rejectedWith("http 500"); + }); + } catch (err) { + expect((err as Error).message).equal("Failed to register IP: revert error"); + } + }); + }); + + describe("Test ipAssetClient.registerDerivative", async function () { + it("should throw childIpId error when registerDerivative given childIpId is not registered", async () => { + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(false); + try { + await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative: The child IP with id 0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c is not registered.", + ); + } + }); + + it("should throw parentIpId error when registerDerivative given parentIpId is not registered", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(false); + + try { + await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0x1daAE3197Bc469Cb97B917aa460a12dD95c6627a"], + licenseTermsIds: ["1"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative: The parent IP with id 0x1daAE3197Bc469Cb97B917aa460a12dD95c6627a is not registered.", + ); + } + }); + + it("should throw not match error when registerDerivative given parentIds'length is not equal licenseTermsIds'length", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + + try { + await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1", "2"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative: Parent IP IDs and License terms IDs must be provided in pairs.", + ); + } + }); + + it("should throw not attach error when registerDerivative given licenseTermsIds is not attached parentIpIds", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(false); + + try { + await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative: License terms id 1 must be attached to the parent ipId 0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4 before registering derivative.", + ); + } + }); + + it("should return txHash when registerDerivative given childIpId and parentIpIds are registered, and parentIpIds match License terms ids", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon + .stub(ipAssetClient.licensingModuleClient, "registerDerivative") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + + const res = await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1"], + }); + + expect(res.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); + }); + + it("should return txHash when registerDerivative given correct childIpId, parentIpId, licenseTermsIds and waitForTransaction of true ", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon + .stub(ipAssetClient.licensingModuleClient, "registerDerivative") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + + const res = await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1"], + txOptions: { + waitForTransaction: true, + }, + }); + + expect(res.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); + }); + }); + + describe("Test ipAssetClient.registerDerivativeWithLicenseTokens", async function () { + it("should throw childIpId error when registerDerivativeWithLicenseTokens given childIpId is not registered", async () => { + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(false); + + try { + await ipAssetClient.registerDerivativeWithLicenseTokens({ + childIpId: "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + licenseTokenIds: ["1"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative with license tokens: The child IP with id 0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4 is not registered.", + ); + } + }); + + it("should throw own error when registerDerivativeWithLicenseTokens given licenseTokenIds is not belongs caller", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon.stub(ipAssetClient.licenseTokenReadOnlyClient, "ownerOf").resolves(undefined); + + try { + await ipAssetClient.registerDerivativeWithLicenseTokens({ + childIpId: "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + licenseTokenIds: ["1"], + }); + } catch (err) { + expect((err as Error).message).equal( + "Failed to register derivative with license tokens: License token id 1 must be owned by the caller.", + ); + } + }); + + it("should return txHash when registerDerivativeWithLicenseTokens given correct args", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseTokenReadOnlyClient, "ownerOf") + .resolves("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); + sinon + .stub(ipAssetClient.licensingModuleClient, "registerDerivativeWithLicenseTokens") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + + const res = await ipAssetClient.registerDerivativeWithLicenseTokens({ + childIpId: "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + licenseTokenIds: ["1"], + }); + + expect(res.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); + }); + + it("should return txHash when registerDerivativeWithLicenseTokens given correct args and waitForTransaction of true", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseTokenReadOnlyClient, "ownerOf") + .resolves("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); + sinon + .stub(ipAssetClient.licensingModuleClient, "registerDerivativeWithLicenseTokens") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + + const res = await ipAssetClient.registerDerivativeWithLicenseTokens({ + childIpId: "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + licenseTokenIds: ["1"], + txOptions: { + waitForTransaction: true, + }, + }); + + expect(res.txHash).equal( + "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + ); }); }); }); diff --git a/packages/core-sdk/test/unit/resources/license.test.ts b/packages/core-sdk/test/unit/resources/license.test.ts new file mode 100644 index 00000000..adb828e3 --- /dev/null +++ b/packages/core-sdk/test/unit/resources/license.test.ts @@ -0,0 +1,437 @@ +import chai from "chai"; +import { createMock } from "../testUtils"; +import * as sinon from "sinon"; +import { LicenseClient } from "../../../src"; +import { PublicClient, WalletClient, Account } from "viem"; +import chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; +const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; + +describe("Test LicenseClient", function () { + let licenseClient: LicenseClient; + let rpcMock: PublicClient; + let walletMock: WalletClient; + + beforeEach(function () { + rpcMock = createMock(); + walletMock = createMock(); + const accountMock = createMock(); + accountMock.address = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; + walletMock.account = accountMock; + licenseClient = new LicenseClient(rpcMock, walletMock); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe("Test licenseClient.registerNonComSocialRemixingPIL", async function () { + it("should return licenseTermsId when call registerNonComSocialRemixingPIL given licenseTermsId is registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(1) }); + + const result = await licenseClient.registerNonComSocialRemixingPIL(); + + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return txhash when call registerNonComSocialRemixingPIL given licenseTermsId is not registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + + const result = await licenseClient.registerNonComSocialRemixingPIL(); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txhash when call registerNonComSocialRemixingPIL given licenseTermsId is not registered and waitForTransaction of true", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + sinon + .stub(licenseClient.licenseTemplateClient, "parseTxLicenseTermsRegisteredEvent") + .returns([ + { + licenseTermsId: BigInt(1), + licenseTemplate: "0x", + licenseTerms: "0x", + }, + ]); + + const result = await licenseClient.registerNonComSocialRemixingPIL({ + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return throw error when call registerNonComSocialRemixingPIL given request fail", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon + .stub(licenseClient.licenseTemplateClient, "registerLicenseTerms") + .throws(new Error("request fail.")); + try { + await licenseClient.registerNonComSocialRemixingPIL({ + txOptions: { + waitForTransaction: true, + }, + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to register non commercial social remixing PIL: request fail.", + ); + } + }); + }); + + describe("Test licenseClient.registerCommercialUsePIL", async function () { + it("should return licenseTermsId when call registerCommercialUsePIL given licenseTermsId is registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(1) }); + + const result = await licenseClient.registerCommercialUsePIL({ + mintingFee: "1", + currency: "0x", + }); + + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return txhash when call registerCommercialUsePIL given licenseTermsId is not registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + + const result = await licenseClient.registerCommercialUsePIL({ + mintingFee: "1", + currency: "0x", + }); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txhash when call registerCommercialUsePIL given licenseTermsId is not registered and waitForTransaction of true", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + sinon + .stub(licenseClient.licenseTemplateClient, "parseTxLicenseTermsRegisteredEvent") + .returns([ + { + licenseTermsId: BigInt(1), + licenseTemplate: "0x", + licenseTerms: "0x", + }, + ]); + + const result = await licenseClient.registerCommercialUsePIL({ + mintingFee: "1", + currency: "0x", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return throw error when call registerCommercialUsePIL given request fail", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon + .stub(licenseClient.licenseTemplateClient, "registerLicenseTerms") + .throws(new Error("request fail.")); + + try { + await licenseClient.registerCommercialUsePIL({ + mintingFee: "1", + currency: "0x", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to register commercial use PIL: request fail.", + ); + } + }); + }); + + describe("Test licenseClient.registerCommercialRemixPIL", async function () { + it("should return licenseTermsId when call registerCommercialRemixPIL given licenseTermsId is registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(1) }); + + const result = await licenseClient.registerCommercialRemixPIL({ + mintingFee: "1", + commercialRevShare: 100, + currency: "0x", + }); + + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return txhash when call registerCommercialRemixPIL given licenseTermsId is not registered", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + + const result = await licenseClient.registerCommercialRemixPIL({ + mintingFee: "1", + commercialRevShare: 100, + currency: "0x", + }); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txhash when call registerCommercialRemixPIL given licenseTermsId is not registered and waitForTransaction of true", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); + sinon + .stub(licenseClient.licenseTemplateClient, "parseTxLicenseTermsRegisteredEvent") + .returns([ + { + licenseTermsId: BigInt(1), + licenseTemplate: "0x", + licenseTerms: "0x", + }, + ]); + + const result = await licenseClient.registerCommercialRemixPIL({ + mintingFee: "1", + commercialRevShare: 100, + currency: "0x", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + expect(result.licenseTermsId).to.equal("1"); + }); + + it("should return throw error when call registerCommercialRemixPIL given request fail", async function () { + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon + .stub(licenseClient.licenseTemplateClient, "registerLicenseTerms") + .throws(new Error("request fail.")); + try { + await licenseClient.registerCommercialRemixPIL({ + mintingFee: "1", + commercialRevShare: 100, + currency: "0x", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to register commercial remix PIL: request fail.", + ); + } + }); + }); + + describe("Test licenseClient.attachLicenseTerms", async function () { + it("should throw ipId is not registered when call attachLicenseTerms given ipId is not registered", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(false); + + try { + await licenseClient.attachLicenseTerms({ + ipId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to attach license terms: The IP with id 0x is not registered.", + ); + } + }); + + it("should throw licenseTermsId error when call attachLicenseTerms given licenseTermsId is not exist", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(false); + + try { + await licenseClient.attachLicenseTerms({ + ipId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to attach license terms: License terms id 1 do not exist.", + ); + } + }); + + it("should throw attached error when call attachLicenseTerms given licenseTermsId is already attached", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + + try { + await licenseClient.attachLicenseTerms({ + ipId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to attach license terms: License terms id 1 is already attached to the IP with id 0x.", + ); + } + }); + + it("should return txHash when call attachLicenseTerms given args is correct", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(false); + sinon.stub(licenseClient.licensingModuleClient, "attachLicenseTerms").resolves(txHash); + + const result = await licenseClient.attachLicenseTerms({ + ipId: "0x", + licenseTermsId: "1", + }); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txHash when call attachLicenseTerms given args is correct and waitForTransaction of true", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(false); + sinon.stub(licenseClient.licensingModuleClient, "attachLicenseTerms").resolves(txHash); + + const result = await licenseClient.attachLicenseTerms({ + ipId: "0x", + licenseTermsId: "1", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + }); + }); + + describe("Test licenseClient.mintLicenseTokens", async function () { + it("should throw licensor ipId error when call mintLicenseTokens given licensorIpId is not registered", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(false); + + try { + await licenseClient.mintLicenseTokens({ + licensorIpId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to mint license tokens: The licensor IP with id 0x is not registered.", + ); + } + }); + + it("should throw licenseTermsId error when call mintLicenseTokens given licenseTermsId is not exist", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(false); + + try { + await licenseClient.mintLicenseTokens({ + licensorIpId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to mint license tokens: License terms id 1 do not exist.", + ); + } + }); + + it("should throw attached error when call mintLicenseTokens given licenseTermsId is not attached", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(false); + + try { + await licenseClient.mintLicenseTokens({ + licensorIpId: "0x", + licenseTermsId: "1", + }); + } catch (error) { + expect((error as Error).message).equal( + "Failed to mint license tokens: License terms id 1 is not attached to the IP with id 0x.", + ); + } + }); + + it("should return txHash when call mintLicenseTokens given args is correct", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon.stub(licenseClient.licensingModuleClient, "mintLicenseTokens").resolves(txHash); + + const result = await licenseClient.mintLicenseTokens({ + licensorIpId: "0x", + licenseTermsId: "1", + }); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txHash when call mintLicenseTokens given args is correct and waitForTransaction of true", async function () { + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon.stub(licenseClient.licensingModuleClient, "mintLicenseTokens").resolves(txHash); + sinon.stub(licenseClient.licensingModuleClient, "parseTxLicenseTokensMintedEvent").returns([ + { + caller: "0x", + licensorIpId: "0x", + licenseTemplate: "0x", + licenseTermsId: BigInt(1), + amount: BigInt(1), + receiver: "0x", + startLicenseTokenId: BigInt(1), + }, + ]); + + const result = await licenseClient.mintLicenseTokens({ + licensorIpId: "0x", + licenseTermsId: "1", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + expect(result.licenseTokenId).to.equal("1"); + }); + }); +}); diff --git a/packages/core-sdk/test/unit/resources/royalty.test.ts b/packages/core-sdk/test/unit/resources/royalty.test.ts new file mode 100644 index 00000000..da5a44a6 --- /dev/null +++ b/packages/core-sdk/test/unit/resources/royalty.test.ts @@ -0,0 +1,107 @@ +import chai from "chai"; +import { createMock } from "../testUtils"; +import * as sinon from "sinon"; +import { PublicClient, WalletClient, Account } from "viem"; +import chaiAsPromised from "chai-as-promised"; +import { RoyaltyClient } from "../../../src/resources/royalty"; +import { + IpRoyaltyVaultImplClient, + RoyaltyPolicyLapGetRoyaltyDataResponse, +} from "../../../src/abi/generated"; + +chai.use(chaiAsPromised); +const expect = chai.expect; +const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; + +describe("Test RoyaltyClient", function () { + let royaltyClient: RoyaltyClient; + let rpcMock: PublicClient; + let walletMock: WalletClient; + + beforeEach(function () { + rpcMock = createMock(); + walletMock = createMock(); + const accountMock = createMock(); + accountMock.address = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; + walletMock.account = accountMock; + royaltyClient = new RoyaltyClient(rpcMock, walletMock); + sinon.stub(); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe("Test royaltyClient.collectRoyaltyTokens", async function () { + it("should throw parentIpId error when call collectRoyaltyTokens given parentIpId is not registered", async function () { + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(false); + + try { + await royaltyClient.collectRoyaltyTokens({ + parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + royaltyVaultIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to collect royalty tokens: The parent IP with id 0x73fcb515cee99e4991465ef586cfe2b072ebb512 is not registered.", + ); + } + }); + + it("should throw royaltyVaultIpId error when call collectRoyaltyTokens given royaltyVaultIpId is not registered", async function () { + sinon + .stub(royaltyClient.ipAssetRegistryClient, "isRegistered") + .onFirstCall() + .resolves(true) + .onSecondCall() + .resolves(false); + + try { + await royaltyClient.collectRoyaltyTokens({ + parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + royaltyVaultIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to collect royalty tokens: The royalty vault IP with id 0x73fcb515cee99e4991465ef586cfe2b072ebb512 is not registered.", + ); + } + }); + + it("should throw royaltyVaultAddress error when call collectRoyaltyTokens given royalty vault address is empty", async function () { + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.royaltyPolicyLapClient, "getRoyaltyData") + .resolves([] as unknown as RoyaltyPolicyLapGetRoyaltyDataResponse); + + try { + await royaltyClient.collectRoyaltyTokens({ + parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + royaltyVaultIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to collect royalty tokens: The royalty vault IP with id 0x73fcb515cee99e4991465ef586cfe2b072ebb512 address is not set.", + ); + } + }); + + it("should throw royaltyVaultAddress error when call collectRoyaltyTokens given royalty vault address is 0x", async function () { + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.royaltyPolicyLapClient, "getRoyaltyData") + .resolves([true, "0x", 1, ["0x73fcb515cee99e4991465ef586cfe2b072ebb512"], [1]]); + + try { + await royaltyClient.collectRoyaltyTokens({ + parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + royaltyVaultIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to collect royalty tokens: The royalty vault IP with id 0x73fcb515cee99e4991465ef586cfe2b072ebb512 address is not set.", + ); + } + }); + }); +}); diff --git a/packages/core-sdk/test/unit/testUtils.ts b/packages/core-sdk/test/unit/testUtils.ts index 3f6f3b57..9b3f6e8f 100644 --- a/packages/core-sdk/test/unit/testUtils.ts +++ b/packages/core-sdk/test/unit/testUtils.ts @@ -1,5 +1,8 @@ +import sinon from "sinon"; + export function createMock(obj = {}): T { const mockObj: any = obj; + mockObj.waitForTransactionReceipt = sinon.stub().resolves({}); return mockObj; } diff --git a/packages/wagmi-generater/wagmi.config.ts b/packages/wagmi-generater/wagmi.config.ts index 4a35caa2..032da8dc 100644 --- a/packages/wagmi-generater/wagmi.config.ts +++ b/packages/wagmi-generater/wagmi.config.ts @@ -45,6 +45,12 @@ export default defineConfig(async () => { [storyTestnetId]: "0x0c3D467537FAd845a78728CEdc3D9447338c5422", } }, + { + name: "LicenseToken", address: { + // [sepolia.id]: "0x950d766A1a0afDc33c3e653C861A8765cb42DbdC", + [storyTestnetId]: "0xD40b7bCA204f96a346021e31c9ad54FF495226e7", + } + }, { name: "LicensingModule", address: { // [sepolia.id]: "0x950d766A1a0afDc33c3e653C861A8765cb42DbdC", @@ -124,7 +130,8 @@ export default defineConfig(async () => { "ipId", "RoyaltyTokensCollected", "snapshot", - "SnapshotCompleted" + "SnapshotCompleted", + "RevenueTokenClaimed" ], "PiLicenseTemplate": [ "getLicenseTermsId", @@ -148,6 +155,9 @@ export default defineConfig(async () => { "onRoyaltyPayment", "getRoyaltyData", ], + "LicenseToken":[ + "ownerOf" + ] } }), ],