diff --git a/packages/core-sdk/.env.example b/packages/core-sdk/.env.example new file mode 100644 index 00000000..6115f569 --- /dev/null +++ b/packages/core-sdk/.env.example @@ -0,0 +1,2 @@ +TEST_WALLET_ADDRESS = 0x0000000000000000000000000000000000000000 +WALLET_PRIVATE_KEY = 0x0000000000000000000000000000000000000000 \ No newline at end of file diff --git a/packages/core-sdk/package.json b/packages/core-sdk/package.json index 7fe5aa74..5301d49f 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.22", + "version": "1.0.0-rc.23", "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 ccbfb26b..5af6166f 100644 --- a/packages/core-sdk/src/abi/generated.ts +++ b/packages/core-sdk/src/abi/generated.ts @@ -2237,6 +2237,7 @@ export const ipRoyaltyVaultImplAbi = [ { type: "error", inputs: [], name: "IpRoyaltyVault__AlreadyClaimed" }, { type: "error", inputs: [], name: "IpRoyaltyVault__ClaimerNotAnAncestor" }, { type: "error", inputs: [], name: "IpRoyaltyVault__EnforcedPause" }, + { type: "error", inputs: [], name: "IpRoyaltyVault__IpGraphCallFailed" }, { type: "error", inputs: [], name: "IpRoyaltyVault__IpTagged" }, { type: "error", inputs: [], name: "IpRoyaltyVault__NotRoyaltyPolicyLAP" }, { @@ -2693,6 +2694,7 @@ export const licenseRegistryAbi = [ inputs: [ { name: "licensingModule", internalType: "address", type: "address" }, { name: "disputeModule", internalType: "address", type: "address" }, + { name: "ipGraphAcl", internalType: "address", type: "address" }, ], stateMutability: "nonpayable", }, @@ -2727,6 +2729,14 @@ export const licenseRegistryAbi = [ { type: "error", inputs: [], name: "ERC1967NonPayable" }, { type: "error", inputs: [], name: "FailedInnerCall" }, { type: "error", inputs: [], name: "InvalidInitialization" }, + { + type: "error", + inputs: [ + { name: "childIpId", internalType: "address", type: "address" }, + { name: "parentIpIds", internalType: "address[]", type: "address[]" }, + ], + name: "LicenseRegistry__AddParentIpToIPGraphFailed", + }, { type: "error", inputs: [], @@ -2848,6 +2858,7 @@ export const licenseRegistryAbi = [ }, { type: "error", inputs: [], name: "LicenseRegistry__ZeroAccessManager" }, { type: "error", inputs: [], name: "LicenseRegistry__ZeroDisputeModule" }, + { type: "error", inputs: [], name: "LicenseRegistry__ZeroIPGraphACL" }, { type: "error", inputs: [], name: "LicenseRegistry__ZeroLicenseTemplate" }, { type: "error", inputs: [], name: "LicenseRegistry__ZeroLicensingModule" }, { @@ -3009,6 +3020,13 @@ export const licenseRegistryAbi = [ outputs: [{ name: "", internalType: "bytes32", type: "bytes32" }], stateMutability: "view", }, + { + type: "function", + inputs: [], + name: "IP_GRAPH_ACL", + outputs: [{ name: "", internalType: "contract IPGraphACL", type: "address" }], + stateMutability: "view", + }, { type: "function", inputs: [], @@ -6006,6 +6024,7 @@ export const royaltyPolicyLapAbi = [ inputs: [ { name: "royaltyModule", internalType: "address", type: "address" }, { name: "licensingModule", internalType: "address", type: "address" }, + { name: "ipGraphAcl", internalType: "address", type: "address" }, ], stateMutability: "nonpayable", }, @@ -6064,6 +6083,7 @@ export const royaltyPolicyLapAbi = [ { type: "error", inputs: [], name: "RoyaltyPolicyLAP__NotRoyaltyModule" }, { type: "error", inputs: [], name: "RoyaltyPolicyLAP__UnlinkableToParents" }, { type: "error", inputs: [], name: "RoyaltyPolicyLAP__ZeroAccessManager" }, + { type: "error", inputs: [], name: "RoyaltyPolicyLAP__ZeroIPGraphACL" }, { type: "error", inputs: [], @@ -6217,6 +6237,13 @@ export const royaltyPolicyLapAbi = [ ], name: "Upgraded", }, + { + type: "function", + inputs: [], + name: "IP_GRAPH_ACL", + outputs: [{ name: "", internalType: "contract IPGraphACL", type: "address" }], + stateMutability: "view", + }, { type: "function", inputs: [], @@ -6847,6 +6874,13 @@ export const spgAbi = [ ], stateMutability: "nonpayable", }, + { + type: "function", + inputs: [{ name: "data", internalType: "bytes[]", type: "bytes[]" }], + name: "multicall", + outputs: [{ name: "results", internalType: "bytes[]", type: "bytes[]" }], + stateMutability: "nonpayable", + }, { type: "function", inputs: [ @@ -7439,6 +7473,25 @@ export const spgnftImplAbi = [ ], 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, @@ -7452,6 +7505,19 @@ export const spgnftImplAbi = [ ], name: "Initialized", }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "_tokenId", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "MetadataUpdate", + }, { type: "event", anonymous: false, @@ -7618,7 +7684,10 @@ export const spgnftImplAbi = [ }, { type: "function", - inputs: [{ name: "to", internalType: "address", type: "address" }], + inputs: [ + { name: "to", internalType: "address", type: "address" }, + { name: "nftMetadataURI", internalType: "string", type: "string" }, + ], name: "mint", outputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], stateMutability: "nonpayable", @@ -7628,6 +7697,7 @@ export const spgnftImplAbi = [ inputs: [ { name: "to", internalType: "address", type: "address" }, { name: "payer", internalType: "address", type: "address" }, + { name: "nftMetadataURI", internalType: "string", type: "string" }, ], name: "mintBySPG", outputs: [{ name: "tokenId", internalType: "uint256", type: "uint256" }], @@ -9251,6 +9321,15 @@ export type IpAccountImplStateResponse = { result: Hex; }; +/** + * IpAccountImplTokenResponse + * + * @param 0 uint256 + * @param 1 address + * @param 2 uint256 + */ +export type IpAccountImplTokenResponse = readonly [bigint, Address, bigint]; + /** * IpAccountImplExecuteRequest * @@ -9326,6 +9405,20 @@ export class IpAccountImplReadOnlyClient { result: result, }; } + + /** + * method token for contract IPAccountImpl + * + * @param request IpAccountImplTokenRequest + * @return Promise + */ + public async token(): Promise { + return await this.rpcClient.readContract({ + abi: ipAccountImplAbi, + address: this.address, + functionName: "token", + }); + } } /** @@ -10233,6 +10326,8 @@ export type LicenseRegistryDisputeModuleResponse = Address; export type LicenseRegistryExpirationTimeResponse = Hex; +export type LicenseRegistryIpGraphAclResponse = Address; + export type LicenseRegistryIpGraphContractResponse = Address; export type LicenseRegistryLicensingModuleResponse = Address; @@ -10979,6 +11074,20 @@ export class LicenseRegistryReadOnlyClient extends LicenseRegistryEventClient { }); } + /** + * method IP_GRAPH_ACL for contract LicenseRegistry + * + * @param request LicenseRegistryIpGraphAclRequest + * @return Promise + */ + public async ipGraphAcl(): Promise { + return await this.rpcClient.readContract({ + abi: licenseRegistryAbi, + address: this.address, + functionName: "IP_GRAPH_ACL", + }); + } + /** * method IP_GRAPH_CONTRACT for contract LicenseRegistry * @@ -13536,6 +13645,28 @@ export class PiLicenseTemplateClient extends PiLicenseTemplateReadOnlyClient { // Contract RoyaltyModule ============================================================= +/** + * RoyaltyModuleIsWhitelistedRoyaltyPolicyRequest + * + * @param royaltyPolicy address + */ +export type RoyaltyModuleIsWhitelistedRoyaltyPolicyRequest = { + royaltyPolicy: Address; +}; + +export type RoyaltyModuleIsWhitelistedRoyaltyPolicyResponse = boolean; + +/** + * RoyaltyModuleIsWhitelistedRoyaltyTokenRequest + * + * @param token address + */ +export type RoyaltyModuleIsWhitelistedRoyaltyTokenRequest = { + token: Address; +}; + +export type RoyaltyModuleIsWhitelistedRoyaltyTokenResponse = boolean; + /** * RoyaltyModulePayRoyaltyOnBehalfRequest * @@ -13552,16 +13683,60 @@ export type RoyaltyModulePayRoyaltyOnBehalfRequest = { }; /** - * contract RoyaltyModule write method + * contract RoyaltyModule readonly method */ -export class RoyaltyModuleClient { - protected readonly wallet: SimpleWalletClient; +export class RoyaltyModuleReadOnlyClient { protected readonly rpcClient: PublicClient; public readonly address: Address; - constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address?: Address) { + constructor(rpcClient: PublicClient, address?: Address) { this.address = address || getAddress(royaltyModuleAddress, rpcClient.chain?.id); this.rpcClient = rpcClient; + } + + /** + * method isWhitelistedRoyaltyPolicy for contract RoyaltyModule + * + * @param request RoyaltyModuleIsWhitelistedRoyaltyPolicyRequest + * @return Promise + */ + public async isWhitelistedRoyaltyPolicy( + request: RoyaltyModuleIsWhitelistedRoyaltyPolicyRequest, + ): Promise { + return await this.rpcClient.readContract({ + abi: royaltyModuleAbi, + address: this.address, + functionName: "isWhitelistedRoyaltyPolicy", + args: [request.royaltyPolicy], + }); + } + + /** + * method isWhitelistedRoyaltyToken for contract RoyaltyModule + * + * @param request RoyaltyModuleIsWhitelistedRoyaltyTokenRequest + * @return Promise + */ + public async isWhitelistedRoyaltyToken( + request: RoyaltyModuleIsWhitelistedRoyaltyTokenRequest, + ): Promise { + return await this.rpcClient.readContract({ + abi: royaltyModuleAbi, + address: this.address, + functionName: "isWhitelistedRoyaltyToken", + args: [request.token], + }); + } +} + +/** + * contract RoyaltyModule write method + */ +export class RoyaltyModuleClient extends RoyaltyModuleReadOnlyClient { + protected readonly wallet: SimpleWalletClient; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address?: Address) { + super(rpcClient, address); this.wallet = wallet; } @@ -14846,6 +15021,17 @@ export type SpgnftImplApprovalForAllEvent = { approved: boolean; }; +/** + * SpgnftImplBatchMetadataUpdateEvent + * + * @param _fromTokenId uint256 + * @param _toTokenId uint256 + */ +export type SpgnftImplBatchMetadataUpdateEvent = { + _fromTokenId: bigint; + _toTokenId: bigint; +}; + /** * SpgnftImplInitializedEvent * @@ -14855,6 +15041,15 @@ export type SpgnftImplInitializedEvent = { version: bigint; }; +/** + * SpgnftImplMetadataUpdateEvent + * + * @param _tokenId uint256 + */ +export type SpgnftImplMetadataUpdateEvent = { + _tokenId: bigint; +}; + /** * SpgnftImplRoleAdminChangedEvent * @@ -15058,9 +15253,11 @@ export type SpgnftImplInitializeRequest = { * SpgnftImplMintRequest * * @param to address + * @param nftMetadataURI string */ export type SpgnftImplMintRequest = { to: Address; + nftMetadataURI: string; }; /** @@ -15068,10 +15265,12 @@ export type SpgnftImplMintRequest = { * * @param to address * @param payer address + * @param nftMetadataURI string */ export type SpgnftImplMintBySpgRequest = { to: Address; payer: Address; + nftMetadataURI: string; }; /** @@ -15269,6 +15468,47 @@ export class SpgnftImplEventClient { return targetLogs; } + /** + * event BatchMetadataUpdate for contract SPGNFTImpl + */ + public watchBatchMetadataUpdateEvent( + onLogs: (txHash: Hex, ev: Partial) => void, + ): WatchContractEventReturnType { + return this.rpcClient.watchContractEvent({ + abi: spgnftImplAbi, + address: this.address, + eventName: "BatchMetadataUpdate", + onLogs: (evs) => { + evs.forEach((it) => onLogs(it.transactionHash, it.args)); + }, + }); + } + + /** + * parse tx receipt event BatchMetadataUpdate for contract SPGNFTImpl + */ + public parseTxBatchMetadataUpdateEvent( + txReceipt: TransactionReceipt, + ): Array { + const targetLogs: Array = []; + for (const log of txReceipt.logs) { + try { + const event = decodeEventLog({ + abi: spgnftImplAbi, + eventName: "BatchMetadataUpdate", + data: log.data, + topics: log.topics, + }); + if (event.eventName === "BatchMetadataUpdate") { + targetLogs.push(event.args); + } + } catch (e) { + /* empty */ + } + } + return targetLogs; + } + /** * event Initialized for contract SPGNFTImpl */ @@ -15308,6 +15548,47 @@ export class SpgnftImplEventClient { return targetLogs; } + /** + * event MetadataUpdate for contract SPGNFTImpl + */ + public watchMetadataUpdateEvent( + onLogs: (txHash: Hex, ev: Partial) => void, + ): WatchContractEventReturnType { + return this.rpcClient.watchContractEvent({ + abi: spgnftImplAbi, + address: this.address, + eventName: "MetadataUpdate", + onLogs: (evs) => { + evs.forEach((it) => onLogs(it.transactionHash, it.args)); + }, + }); + } + + /** + * parse tx receipt event MetadataUpdate for contract SPGNFTImpl + */ + public parseTxMetadataUpdateEvent( + txReceipt: TransactionReceipt, + ): Array { + const targetLogs: Array = []; + for (const log of txReceipt.logs) { + try { + const event = decodeEventLog({ + abi: spgnftImplAbi, + eventName: "MetadataUpdate", + data: log.data, + topics: log.topics, + }); + if (event.eventName === "MetadataUpdate") { + targetLogs.push(event.args); + } + } catch (e) { + /* empty */ + } + } + return targetLogs; + } + /** * event RoleAdminChanged for contract SPGNFTImpl */ @@ -15843,7 +16124,7 @@ export class SpgnftImplClient extends SpgnftImplReadOnlyClient { address: this.address, functionName: "mint", account: this.wallet.account, - args: [request.to], + args: [request.to, request.nftMetadataURI], }); return await this.wallet.writeContract(call as WriteContractParameters); } @@ -15860,7 +16141,7 @@ export class SpgnftImplClient extends SpgnftImplReadOnlyClient { data: encodeFunctionData({ abi: spgnftImplAbi, functionName: "mint", - args: [request.to], + args: [request.to, request.nftMetadataURI], }), }; } @@ -15877,7 +16158,7 @@ export class SpgnftImplClient extends SpgnftImplReadOnlyClient { address: this.address, functionName: "mintBySPG", account: this.wallet.account, - args: [request.to, request.payer], + args: [request.to, request.payer, request.nftMetadataURI], }); return await this.wallet.writeContract(call as WriteContractParameters); } @@ -15894,7 +16175,7 @@ export class SpgnftImplClient extends SpgnftImplReadOnlyClient { data: encodeFunctionData({ abi: spgnftImplAbi, functionName: "mintBySPG", - args: [request.to, request.payer], + args: [request.to, request.payer, request.nftMetadataURI], }), }; } diff --git a/packages/core-sdk/src/index.ts b/packages/core-sdk/src/index.ts index cc07c082..0a0bd365 100644 --- a/packages/core-sdk/src/index.ts +++ b/packages/core-sdk/src/index.ts @@ -1,6 +1,6 @@ export { StoryClient } from "./client"; export { AddressZero, HashZero } from "./constants/common"; - +export { iliad } from "./utils/chain"; export { IPAssetClient } from "./resources/ipAsset"; export { PermissionClient } from "./resources/permission"; export { LicenseClient } from "./resources/license"; @@ -47,6 +47,7 @@ export type { MintLicenseTokensRequest, MintLicenseTokensResponse, LicenseTermsId, + LicenseTerms, } from "./types/resources/license"; export { PIL_TYPE } from "./types/resources/license"; @@ -87,6 +88,7 @@ export type { IPAccountExecuteWithSigRequest, IPAccountExecuteWithSigResponse, IpAccountStateResponse, + TokenResponse, } from "./types/resources/ipAccount"; export type { diff --git a/packages/core-sdk/src/resources/ipAccount.ts b/packages/core-sdk/src/resources/ipAccount.ts index 189a4c3c..9987780d 100644 --- a/packages/core-sdk/src/resources/ipAccount.ts +++ b/packages/core-sdk/src/resources/ipAccount.ts @@ -1,4 +1,4 @@ -import { PublicClient } from "viem"; +import { Address, PublicClient } from "viem"; import { IPAccountExecuteRequest, @@ -6,6 +6,7 @@ import { IPAccountExecuteWithSigRequest, IPAccountExecuteWithSigResponse, IpAccountStateResponse, + TokenResponse, } from "../types/resources/ipAccount"; import { handleError } from "../utils/errors"; import { IpAccountImplClient, SimpleWalletClient } from "../abi/generated"; @@ -113,15 +114,41 @@ export class IPAccountClient { /** Returns the IPAccount's internal nonce for transaction ordering. * @param ipId The IP ID - * @returns The nonce for transaction ordering. + * @returns A Promise that resolves to the IP Account's nonce. */ - public async getIpAccountNonce(ipId: string): Promise { - const ipAccount = new IpAccountImplClient( - this.rpcClient, - this.wallet, - getAddress(ipId, "ipId"), - ); - const { result: state } = await ipAccount.state(); - return state; + public async getIpAccountNonce(ipId: Address): Promise { + try { + const ipAccount = new IpAccountImplClient( + this.rpcClient, + this.wallet, + getAddress(ipId, "ipId"), + ); + const { result: state } = await ipAccount.state(); + return state; + } catch (error) { + handleError(error, "Failed to get the IP Account nonce"); + } + } + + /** + * Returns the identifier of the non-fungible token which owns the account + * @returns A Promise that resolves to an object containing the chain ID, token contract address, and token ID. + */ + public async getToken(ipId: Address): Promise { + try { + const ipAccount = new IpAccountImplClient( + this.rpcClient, + this.wallet, + getAddress(ipId, "ipId"), + ); + const [chainId, tokenContract, tokenId] = await ipAccount.token(); + return { + chainId, + tokenContract, + tokenId, + }; + } catch (error) { + handleError(error, "Failed to get the token"); + } } } diff --git a/packages/core-sdk/src/resources/license.ts b/packages/core-sdk/src/resources/license.ts index f32b6bab..9a4e84a7 100644 --- a/packages/core-sdk/src/resources/license.ts +++ b/packages/core-sdk/src/resources/license.ts @@ -8,6 +8,7 @@ import { PiLicenseTemplateClient, PiLicenseTemplateGetLicenseTermsResponse, PiLicenseTemplateReadOnlyClient, + RoyaltyModuleReadOnlyClient, RoyaltyPolicyLapClient, SimpleWalletClient, } from "../abi/generated"; @@ -24,6 +25,7 @@ import { PIL_TYPE, AttachLicenseTermsResponse, LicenseTermsId, + RegisterPILTermsRequest, } from "../types/resources/license"; import { handleError } from "../utils/errors"; import { getLicenseTermByType } from "../utils/getLicenseTermsByType"; @@ -36,6 +38,7 @@ export class LicenseClient { public piLicenseTemplateReadOnlyClient: PiLicenseTemplateReadOnlyClient; public licenseTemplateClient: PiLicenseTemplateClient; public royaltyPolicyLAPClient: RoyaltyPolicyLapClient; + public royaltyModuleReadOnlyClient: RoyaltyModuleReadOnlyClient; public licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; @@ -46,12 +49,102 @@ export class LicenseClient { this.piLicenseTemplateReadOnlyClient = new PiLicenseTemplateReadOnlyClient(rpcClient); this.licenseTemplateClient = new PiLicenseTemplateClient(rpcClient, wallet); this.royaltyPolicyLAPClient = new RoyaltyPolicyLapClient(rpcClient, wallet); + this.royaltyModuleReadOnlyClient = new RoyaltyModuleReadOnlyClient(rpcClient); this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(rpcClient); this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); this.rpcClient = rpcClient; this.wallet = wallet; } - + /** + * Registers new license terms and return the ID of the newly registered license terms. + * @param request - The request object that contains all data needed to register a license term. + * @param request.transferable Indicates whether the license is transferable or not. + * @param request.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. + * @param request.mintingFee The fee to be paid when minting a license. + * @param request.expiration The expiration period of the license. + * @param request.commercialUse Indicates whether the work can be used commercially or not. + * @param request.commercialAttribution Whether attribution is required when reproducing the work commercially or not. + * @param request.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. + * @param request.commercializerCheckerData The data to be passed to the commercializer checker contract. + * @param request.commercialRevShare Percentage of revenue that must be shared with the licensor. + * @param request.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. + * @param request.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. + * @param request.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. + * @param request.derivativesApproval Indicates whether the licensor must approve derivatives of the work before they can be linked to the licensor IP ID or not. + * @param request.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. + * @param request.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. + * @param request.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. + * @param request.uri The URI of the license terms, which can be used to fetch the offchain license terms. + * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. + * @returns A Promise that resolves to an object containing the optional transaction hash, optional transaction encodedTxData and optional license terms Id. + * @emits LicenseTermsRegistered (licenseTermsId, licenseTemplate, licenseTerms); + */ + public async registerPILTerms(request: RegisterPILTermsRequest): Promise { + try { + const { royaltyPolicy, currency } = request; + if (getAddress(royaltyPolicy, "request.royaltyPolicy") !== zeroAddress) { + const isWhitelistedArbitrationPolicy = + await this.royaltyModuleReadOnlyClient.isWhitelistedRoyaltyPolicy({ royaltyPolicy }); + if (!isWhitelistedArbitrationPolicy) { + throw new Error("The royalty policy is not whitelisted."); + } + } + if (getAddress(currency, "request.currency") !== zeroAddress) { + const isWhitelistedRoyaltyToken = + await this.royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken({ + token: currency, + }); + if (!isWhitelistedRoyaltyToken) { + throw new Error("The currency token is not whitelisted."); + } + } + if (royaltyPolicy !== zeroAddress && currency === zeroAddress) { + throw new Error("Royalty policy requires currency token."); + } + const object = { + ...request, + defaultMintingFee: BigInt(request.defaultMintingFee), + expiration: BigInt(request.expiration), + commercialRevCeiling: BigInt(request.commercialRevCeiling), + derivativeRevCeiling: BigInt(request.derivativeRevCeiling), + }; + this.verifyCommercialUse(object); + this.verifyDerivatives(object); + if (object.commercialRevShare < 0 || object.commercialRevShare > 100) { + throw new Error("CommercialRevShare should be between 0 and 100."); + } else { + object.commercialRevShare = (object.commercialRevShare / 100) * 100000000; + } + const licenseTermsId = await this.getLicenseTermsId(object); + if (licenseTermsId !== 0n) { + return { licenseTermsId: licenseTermsId }; + } + if (request?.txOptions?.encodedTxDataOnly) { + return { + encodedTxData: this.licenseTemplateClient.registerLicenseTermsEncode({ + terms: object, + }), + }; + } else { + const txHash = await this.licenseTemplateClient.registerLicenseTerms({ + terms: object, + }); + if (request?.txOptions?.waitForTransaction) { + const txReceipt = await this.rpcClient.waitForTransactionReceipt({ + ...request.txOptions, + hash: txHash, + }); + const targetLogs = + this.licenseTemplateClient.parseTxLicenseTermsRegisteredEvent(txReceipt); + return { txHash: txHash, licenseTermsId: targetLogs[0].licenseTermsId }; + } else { + return { txHash: txHash }; + } + } + } catch (error) { + handleError(error, "Failed to register license terms"); + } + } /** * Convenient function to register a PIL non commercial social remix license to the registry * @param request - [Optional] The request object that contains all data needed to register a PIL non commercial social remix license. @@ -97,7 +190,7 @@ 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.defaultMintingFee 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.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. * @returns A Promise that resolves to an object containing the optional transaction hash and optional license terms Id. @@ -108,7 +201,7 @@ export class LicenseClient { ): Promise { try { const licenseTerms = getLicenseTermByType(PIL_TYPE.COMMERCIAL_USE, { - defaultMintingFee: request.mintingFee, + defaultMintingFee: request.defaultMintingFee, currency: request.currency, royaltyPolicyLAPAddress: this.royaltyPolicyLAPClient.address, }); @@ -145,7 +238,7 @@ 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.defaultMintingFee 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.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. @@ -157,7 +250,7 @@ export class LicenseClient { ): Promise { try { const licenseTerms = getLicenseTermByType(PIL_TYPE.COMMERCIAL_REMIX, { - defaultMintingFee: request.mintingFee, + defaultMintingFee: request.defaultMintingFee, currency: request.currency, royaltyPolicyLAPAddress: this.royaltyPolicyLAPClient.address, commercialRevShare: request.commercialRevShare, @@ -365,4 +458,49 @@ export class LicenseClient { const licenseRes = await this.licenseTemplateClient.getLicenseTermsId({ terms: request }); return licenseRes.selectedLicenseTermsId; } + private verifyCommercialUse(terms: LicenseTerms) { + if (!terms.commercialUse) { + if (terms.commercialAttribution) { + throw new Error("Cannot add commercial attribution when commercial use is disabled."); + } + if (terms.commercializerChecker !== zeroAddress) { + throw new Error("Cannot add commercializerChecker when commercial use is disabled."); + } + if (terms.commercialRevShare > 0) { + throw new Error("Cannot add commercial revenue share when commercial use is disabled."); + } + if (terms.commercialRevCeiling > 0) { + throw new Error("Cannot add commercial revenue ceiling when commercial use is disabled."); + } + if (terms.derivativeRevCeiling > 0) { + throw new Error( + "Cannot add derivative revenue ceiling share when commercial use is disabled.", + ); + } + if (terms.royaltyPolicy !== zeroAddress) { + throw new Error("Cannot add commercial royalty policy when commercial use is disabled."); + } + } else { + if (terms.royaltyPolicy === zeroAddress) { + throw new Error("Royalty policy is required when commercial use is enabled."); + } + } + } + + private verifyDerivatives(terms: LicenseTerms) { + if (!terms.derivativesAllowed) { + if (terms.derivativesAttribution) { + throw new Error("Cannot add derivative attribution when derivative use is disabled."); + } + if (terms.derivativesApproval) { + throw new Error("Cannot add derivative approval when derivative use is disabled."); + } + if (terms.derivativesReciprocal) { + throw new Error("Cannot add derivative reciprocal when derivative use is disabled."); + } + if (terms.derivativeRevCeiling > 0) { + throw new Error("Cannot add derivative revenue ceiling when derivative use is disabled."); + } + } + } } diff --git a/packages/core-sdk/src/resources/royalty.ts b/packages/core-sdk/src/resources/royalty.ts index abe7a736..3c1fa78f 100644 --- a/packages/core-sdk/src/resources/royalty.ts +++ b/packages/core-sdk/src/resources/royalty.ts @@ -1,4 +1,4 @@ -import { Address, Hex, PublicClient, encodeFunctionData } from "viem"; +import { Address, Hex, PublicClient, encodeFunctionData, zeroAddress } from "viem"; import { handleError } from "../utils/errors"; import { @@ -108,23 +108,26 @@ export class RoyaltyClient { request: PayRoyaltyOnBehalfRequest, ): Promise { try { + const { receiverIpId, payerIpId, token, amount } = request; const isReceiverRegistered = await this.ipAssetRegistryClient.isRegistered({ - id: getAddress(request.receiverIpId, "request.receiverIpId"), + id: getAddress(receiverIpId, "request.receiverIpId"), }); if (!isReceiverRegistered) { - throw new Error(`The receiver IP with id ${request.receiverIpId} is not registered.`); + throw new Error(`The receiver IP with id ${receiverIpId} is not registered.`); } - const isPayerRegistered = await this.ipAssetRegistryClient.isRegistered({ - id: getAddress(request.payerIpId, "request.payerIpId"), - }); - if (!isPayerRegistered) { - throw new Error(`The payer IP with id ${request.payerIpId} is not registered.`); + if (getAddress(payerIpId, "request.payerIpId") && payerIpId !== zeroAddress) { + const isPayerRegistered = await this.ipAssetRegistryClient.isRegistered({ + id: payerIpId, + }); + if (!isPayerRegistered) { + throw new Error(`The payer IP with id ${request.payerIpId} is not registered.`); + } } const req = { - receiverIpId: request.receiverIpId, - payerIpId: request.payerIpId, - token: getAddress(request.token, "request.token"), - amount: BigInt(request.amount), + receiverIpId: receiverIpId, + payerIpId: payerIpId, + token: getAddress(token, "request.token"), + amount: BigInt(amount), }; if (request.txOptions?.encodedTxDataOnly) { return { encodedTxData: this.royaltyModuleClient.payRoyaltyOnBehalfEncode(req) }; diff --git a/packages/core-sdk/src/types/resources/ipAccount.ts b/packages/core-sdk/src/types/resources/ipAccount.ts index 54c6c718..2ef9280b 100644 --- a/packages/core-sdk/src/types/resources/ipAccount.ts +++ b/packages/core-sdk/src/types/resources/ipAccount.ts @@ -33,3 +33,9 @@ export type IPAccountExecuteWithSigResponse = { }; export type IpAccountStateResponse = Hex; + +export type TokenResponse = { + chainId: bigint; + tokenContract: Address; + tokenId: bigint; +}; diff --git a/packages/core-sdk/src/types/resources/license.ts b/packages/core-sdk/src/types/resources/license.ts index a128ac06..1d2c92e0 100644 --- a/packages/core-sdk/src/types/resources/license.ts +++ b/packages/core-sdk/src/types/resources/license.ts @@ -17,25 +17,56 @@ export type RegisterNonComSocialRemixingPILRequest = { txOptions?: TxOptions; }; +/** + * This structure defines the terms for a Programmable IP License (PIL). These terms can be attached to IP Assets. The legal document of the PIL can be found in this repository. + * @type LicenseTerms + **/ export type LicenseTerms = { - defaultMintingFee: bigint; - expiration: bigint; - commercialRevCeiling: bigint; - derivativeRevCeiling: bigint; - commercializerCheckerData: Address; + /*Indicates whether the license is transferable or not.*/ transferable: boolean; + /*The address of the royalty policy contract which required to StoryProtocol in advance.*/ royaltyPolicy: Address; + /*The default minting fee to be paid when minting a license.*/ + defaultMintingFee: bigint; + /*The expiration period of the license.*/ + expiration: bigint; + /*Indicates whether the work can be used commercially or not.*/ commercialUse: boolean; + /*Whether attribution is required when reproducing the work commercially or not.*/ commercialAttribution: boolean; + /*Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced.*/ commercializerChecker: Address; + /*The data to be passed to the commercializer checker contract.*/ + commercializerCheckerData: Address; + /*Percentage of revenue that must be shared with the licensor.*/ commercialRevShare: number; + /*The maximum revenue that can be generated from the commercial use of the work.*/ + commercialRevCeiling: bigint; + /*Indicates whether the licensee can create derivatives of his work or not.*/ derivativesAllowed: boolean; + /*Indicates whether attribution is required for derivatives of the work or not.*/ derivativesAttribution: boolean; + /*Indicates whether the licensor must approve derivatives of the work before they can be linked to the licensor IP ID or not.*/ derivativesApproval: boolean; + /*Indicates whether the licensee must license derivatives of the work under the same terms or not.*/ derivativesReciprocal: boolean; + /*The maximum revenue that can be generated from the derivative use of the work.*/ + derivativeRevCeiling: bigint; + /*The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol.*/ currency: Address; + /*The URI of the license terms, which can be used to fetch the offchain license terms.*/ uri: string; }; +export type RegisterPILTermsRequest = Omit< + LicenseTerms, + "defaultMintingFee" | "expiration" | "commercialRevCeiling" | "derivativeRevCeiling" +> & { + defaultMintingFee: bigint | string | number; + expiration: bigint | string | number; + commercialRevCeiling: bigint | string | number; + derivativeRevCeiling: bigint | string | number; + txOptions?: TxOptions; +}; export type LicenseTermsIdResponse = bigint; export type RegisterPILResponse = { @@ -45,13 +76,13 @@ export type RegisterPILResponse = { }; export type RegisterCommercialUsePILRequest = { - mintingFee: string | number | bigint; + defaultMintingFee: string | number | bigint; currency: Address; txOptions?: TxOptions; }; export type RegisterCommercialRemixPILRequest = { - mintingFee: string | number | bigint; + defaultMintingFee: string | number | bigint; commercialRevShare: number; currency: Address; txOptions?: TxOptions; diff --git a/packages/core-sdk/src/utils/chain.ts b/packages/core-sdk/src/utils/chain.ts new file mode 100644 index 00000000..0670dfb6 --- /dev/null +++ b/packages/core-sdk/src/utils/chain.ts @@ -0,0 +1,26 @@ +import { defineChain } from "viem/utils"; + +export const iliad = defineChain({ + id: 15_13, + name: "iliad", + nativeCurrency: { name: "IP", symbol: "IP", decimals: 18 }, + rpcUrls: { + default: { + http: ["https://testnet.storyrpc.io"], + webSocket: ["wss://story-network.rpc.caldera.xyz/ws"], + }, + }, + blockExplorers: { + default: { + name: "Explorer", + url: "https://testnet.storyscan.xyz", + }, + }, + contracts: { + multicall3: { + address: "0xcA11bde05977b3631167028862bE2a173976CA11", + blockCreated: 5882, + }, + }, + testnet: true, +}); diff --git a/packages/core-sdk/src/utils/getLicenseTermsByType.ts b/packages/core-sdk/src/utils/getLicenseTermsByType.ts index 58546f49..9f14e657 100644 --- a/packages/core-sdk/src/utils/getLicenseTermsByType.ts +++ b/packages/core-sdk/src/utils/getLicenseTermsByType.ts @@ -35,7 +35,7 @@ export function getLicenseTermByType( return licenseTerms; } else if (type === PIL_TYPE.COMMERCIAL_USE) { if (!term || term.defaultMintingFee === undefined || term.currency === undefined) { - throw new Error("mintingFee currency are required for commercial use PIL."); + throw new Error("MintingFee currency are required for commercial use PIL."); } licenseTerms.royaltyPolicy = getAddress(term.royaltyPolicyLAPAddress); licenseTerms.defaultMintingFee = BigInt(term.defaultMintingFee); @@ -52,11 +52,11 @@ export function getLicenseTermByType( term.commercialRevShare === undefined ) { throw new Error( - "mintingFee, currency and commercialRevShare are required for commercial remix PIL.", + "MintingFee, currency and commercialRevShare are required for commercial remix PIL.", ); } if (term.commercialRevShare < 0 || term.commercialRevShare > 100) { - throw new Error("commercialRevShare should be between 0 and 100."); + throw new Error("CommercialRevShare should be between 0 and 100."); } licenseTerms.royaltyPolicy = getAddress(term.royaltyPolicyLAPAddress); licenseTerms.defaultMintingFee = BigInt(term.defaultMintingFee); diff --git a/packages/core-sdk/src/utils/utils.ts b/packages/core-sdk/src/utils/utils.ts index ba5313d2..ded0ec38 100644 --- a/packages/core-sdk/src/utils/utils.ts +++ b/packages/core-sdk/src/utils/utils.ts @@ -10,10 +10,10 @@ import { isAddress, checksumAddress, Address, - defineChain, } from "viem"; import { SupportedChainIds } from "../types/config"; +import { iliad } from "./chain"; export async function waitTxAndFilterLog< const TAbi extends Abi | readonly unknown[], @@ -76,31 +76,7 @@ export async function waitTx( ...params, }); } -//TODO: Some information is waiting for confirmation about chain -export const iliad = defineChain({ - id: 15_13, - name: "iliad", - nativeCurrency: { name: "IP", symbol: "IP", decimals: 18 }, - rpcUrls: { - default: { - http: ["https://testnet.storyrpc.io"], - webSocket: ["wss://story-network.rpc.caldera.xyz/ws"], - }, - }, - blockExplorers: { - default: { - name: "Explorer", - url: "https://testnet.storyscan.xyz", - }, - }, - contracts: { - multicall3: { - address: "0xcA11bde05977b3631167028862bE2a173976CA11", - blockCreated: 5882, - }, - }, - testnet: true, -}); + export function chainStringToViemChain(chainId: SupportedChainIds): Chain { switch (chainId) { case "1513": diff --git a/packages/core-sdk/test/integration/ipAccount.test.ts b/packages/core-sdk/test/integration/ipAccount.test.ts index dc0a758e..91125567 100644 --- a/packages/core-sdk/test/integration/ipAccount.test.ts +++ b/packages/core-sdk/test/integration/ipAccount.test.ts @@ -54,4 +54,16 @@ describe("Ip Account functions", () => { }); expect(response.txHash).to.be.a("string").and.not.empty; }); + + it("should not throw error when getIpAccountNonce", async () => { + const response = await client.ipAccount.getIpAccountNonce(ipId); + expect(response).to.be.a("string").and.not.empty; + }); + + it("should not throw error when call getToken", async () => { + const response = await client.ipAccount.getToken(ipId); + expect(response.chainId).to.be.a("bigint"); + expect(response.tokenContract).to.be.a("string").and.not.empty; + expect(response.tokenId).to.be.a("bigint"); + }); }); diff --git a/packages/core-sdk/test/integration/license.test.ts b/packages/core-sdk/test/integration/license.test.ts index de9ddde8..e10ef3c7 100644 --- a/packages/core-sdk/test/integration/license.test.ts +++ b/packages/core-sdk/test/integration/license.test.ts @@ -1,6 +1,6 @@ import chai from "chai"; import { StoryClient } from "../../src"; -import { Hex } from "viem"; +import { Hex, zeroAddress } from "viem"; import chaiAsPromised from "chai-as-promised"; import { mockERC721, getStoryClient, getTokenId, iliadChainId } from "./utils/util"; import { MockERC20 } from "./utils/mockERC20"; @@ -16,6 +16,31 @@ describe("License Functions", () => { client = getStoryClient(); }); describe("registering license with different types", async () => { + it("should not throw error when registering license ", async () => { + const result = await client.license.registerPILTerms({ + defaultMintingFee: "1", + currency: MockERC20.address, + transferable: false, + royaltyPolicy: zeroAddress, + commercialUse: false, + commercialAttribution: false, + commercializerChecker: zeroAddress, + commercializerCheckerData: "0x", + commercialRevShare: 0, + derivativesAllowed: false, + derivativesAttribution: false, + derivativesApproval: false, + derivativesReciprocal: false, + uri: "", + expiration: "", + commercialRevCeiling: "", + derivativeRevCeiling: "", + txOptions: { + waitForTransaction: true, + }, + }); + expect(result.licenseTermsId).to.be.a("bigint"); + }); it("should not throw error when registering license with non commercial social remixing PIL", async () => { const result = await client.license.registerNonComSocialRemixingPIL({ txOptions: { @@ -26,7 +51,7 @@ describe("License Functions", () => { }); it("should not throw error when registering license with commercial use", async () => { const result = await client.license.registerCommercialUsePIL({ - mintingFee: "1", + defaultMintingFee: "1", currency: MockERC20.address, txOptions: { waitForTransaction: true, @@ -37,7 +62,7 @@ describe("License Functions", () => { it("should not throw error when registering license with commercial Remix use", async () => { const result = await client.license.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: MockERC20.address, txOptions: { @@ -67,7 +92,7 @@ describe("License Functions", () => { ); ipId = registerResult.ipId!; const registerLicenseResult = await client.license.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: MockERC20.address, txOptions: { diff --git a/packages/core-sdk/test/integration/royalty.test.ts b/packages/core-sdk/test/integration/royalty.test.ts index 0101fae6..e8fc72d7 100644 --- a/packages/core-sdk/test/integration/royalty.test.ts +++ b/packages/core-sdk/test/integration/royalty.test.ts @@ -31,7 +31,7 @@ describe("Test royalty Functions", () => { }; const getCommercialPolicyId = async (): Promise => { const response = await client.license.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", currency: MockERC20.address, commercialRevShare: 100, txOptions: { diff --git a/packages/core-sdk/test/unit/client.test.ts b/packages/core-sdk/test/unit/client.test.ts index f42a6f3c..bbeb7ed9 100644 --- a/packages/core-sdk/test/unit/client.test.ts +++ b/packages/core-sdk/test/unit/client.test.ts @@ -1,8 +1,7 @@ import { expect } from "chai"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { createWalletClient, http, Transport } from "viem"; -import { StoryClient, StoryConfig } from "../../src"; -import { iliad } from "../../src/utils/utils"; +import { StoryClient, StoryConfig, iliad } from "../../src/index"; const rpc = "http://127.0.0.1:8545"; describe("Test StoryClient", () => { diff --git a/packages/core-sdk/test/unit/resources/ipAccount.test.ts b/packages/core-sdk/test/unit/resources/ipAccount.test.ts index a74ffc50..1e299591 100644 --- a/packages/core-sdk/test/unit/resources/ipAccount.test.ts +++ b/packages/core-sdk/test/unit/resources/ipAccount.test.ts @@ -71,6 +71,24 @@ describe("Test IPAccountClient", () => { expect(result.txHash).to.equal(txHash); }); + + it("should return encodedTxData when call execute successfully with encodedTxDataOnly", async () => { + IpAccountImplClient.prototype.execute = sinon.stub().resolves(txHash); + IpAccountImplClient.prototype.executeEncode = sinon + .stub() + .returns("0x11111111111111111111111111111"); + const result = await ipAccountClient.execute({ + ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + to: zeroAddress, + value: 2, + data: "0x11111111111111111111111111111", + txOptions: { + encodedTxDataOnly: true, + }, + }); + + expect(result.encodedTxData).to.equal("0x11111111111111111111111111111"); + }); }); describe("Test executeWithSig", () => { @@ -126,9 +144,39 @@ describe("Test IPAccountClient", () => { expect(result.txHash).to.equal(txHash); }); + + it("should return encodedTxData when call executeWithSig successfully with encodedTxDataOnly", async () => { + IpAccountImplClient.prototype.executeWithSig = sinon.stub().resolves(txHash); + IpAccountImplClient.prototype.executeWithSigEncode = sinon + .stub() + .returns("0x11111111111111111111111111111"); + const result = await ipAccountClient.executeWithSig({ + ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + to: zeroAddress, + value: 2, + data: "0x11111111111111111111111111111", + signer: zeroAddress, + deadline: 20, + signature: zeroAddress, + txOptions: { + encodedTxDataOnly: true, + }, + }); + + expect(result.encodedTxData).to.equal("0x11111111111111111111111111111"); + }); }); describe("Test getIpAccountNonce", () => { + it("should throw invalid address error when call getIpAccountNonce given a wrong ipId", async () => { + try { + await ipAccountClient.getIpAccountNonce("0x123"); + } catch (err) { + expect((err as Error).message).equal( + "Failed to get the IP Account nonce: ipId address is invalid: 0x123, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", + ); + } + }); it("should return the state of the IP Account", async () => { sinon .stub(IpAccountImplClient.prototype, "state") @@ -139,4 +187,22 @@ describe("Test IPAccountClient", () => { expect(state).to.equal("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); }); }); + + describe("Test getToken", () => { + it("should invalid address error error when call getToken given a wrong ipId", async () => { + try { + await ipAccountClient.getToken("0x123"); + } catch (err) { + expect((err as Error).message).equal( + "Failed to get the token: ipId address is invalid: 0x123, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", + ); + } + }); + + it("should return the token information when call getToken with correct args", async () => { + sinon.stub(IpAccountImplClient.prototype, "token").resolves([1513n, zeroAddress, 1n]); + const token = await ipAccountClient.getToken("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); + expect(token).to.deep.equal({ chainId: 1513n, tokenContract: zeroAddress, tokenId: 1n }); + }); + }); }); diff --git a/packages/core-sdk/test/unit/resources/license.test.ts b/packages/core-sdk/test/unit/resources/license.test.ts index 5a850e53..a9544384 100644 --- a/packages/core-sdk/test/unit/resources/license.test.ts +++ b/packages/core-sdk/test/unit/resources/license.test.ts @@ -8,6 +8,8 @@ import { PiLicenseTemplateGetLicenseTermsResponse, RoyaltyPolicyLapClient, } from "../../../src/abi/generated"; +import { LicenseTerms } from "../../../src/types/resources/license"; +import { MockERC20 } from "../../integration/utils/mockERC20"; chai.use(chaiAsPromised); const expect = chai.expect; const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; @@ -34,6 +36,419 @@ describe("Test LicenseClient", () => { afterEach(() => { sinon.restore(); }); + + describe("Test licenseClient.registerPILTerms", async () => { + const licenseTerms: LicenseTerms = { + defaultMintingFee: 1513n, + currency: MockERC20.address, + royaltyPolicy: zeroAddress, + transferable: false, + expiration: 0n, + commercialUse: false, + commercialAttribution: false, + commercializerChecker: zeroAddress, + commercializerCheckerData: "0x", + commercialRevShare: 0, + commercialRevCeiling: 0n, + derivativesAllowed: false, + derivativesAttribution: false, + derivativesApproval: false, + derivativesReciprocal: false, + derivativeRevCeiling: 0n, + uri: "", + }; + + it("should throw royalty error when call registerRILTerms with invalid royalty policy address", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + royaltyPolicy: "0x", + }), + ).to.be.rejectedWith( + "Failed to register license terms: request.royaltyPolicy address is invalid: 0x, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", + ); + }); + + it("should throw royalty whitelist error when call registerRILTerms with invalid royalty whitelist address", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(false); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: The royalty policy is not whitelisted.", + ); + }); + + it("should throw currency error when call registerRILTerms with invalid currency address", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + currency: "0x", + }), + ).to.be.rejectedWith( + "Failed to register license terms: request.currency address is invalid: 0x, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", + ); + }); + + it("should throw currency whitelist error when call registerRILTerms with invalid currency whitelist address", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(false); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + currency: MockERC20.address, + }), + ).to.be.rejectedWith( + "Failed to register license terms: The currency token is not whitelisted.", + ); + }); + + it("should throw royalty policy requires currency token error when call registerRILTerms given royaltyPolicy is not zero address and current is zero address", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + currency: zeroAddress, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Royalty policy requires currency token.", + ); + }); + + describe("verify commercial use", () => { + beforeEach(() => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + }); + it("should throw commercialAttribution error when call registerRILTerms given commercialUse is false and commercialAttribution is true", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + commercialAttribution: true, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add commercial attribution when commercial use is disabled.", + ); + }); + + it("should throw commercializerChecker error when call registerRILTerms given commercialUse is false and commercialChecker is not zero address", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + commercializerChecker: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add commercializerChecker when commercial use is disabled.", + ); + }); + it("should throw commercialRevShare error when call registerRILTerms given commercialUse is false and commercialRevShare is more than 0 ", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + commercializerChecker: zeroAddress, + commercialRevShare: 1, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add commercial revenue share when commercial use is disabled.", + ); + }); + + it("should throw commercialRevCeiling error when call registerRILTerms given commercialUse is false and commercialRevCeiling is more than 0", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + commercialRevCeiling: 1, + commercializerChecker: zeroAddress, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add commercial revenue ceiling when commercial use is disabled.", + ); + }); + + it("should throw derivativeRevCeiling error when call registerRILTerms given commercialUse is false and derivativeRevCeiling is more than 0", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + derivativeRevCeiling: 1, + commercializerChecker: zeroAddress, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add derivative revenue ceiling share when commercial use is disabled.", + ); + }); + + it("should throw royaltyPolicy error when call registerRILTerms given commercialUse is false and royaltyPolicy is not zero address", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: false, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + commercializerChecker: zeroAddress, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add commercial royalty policy when commercial use is disabled.", + ); + }); + + it("should throw royaltyPolicy error when call registerRILTerms given commercialUse is true and royaltyPolicy is zero address", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + royaltyPolicy: zeroAddress, + }), + ).to.be.rejectedWith( + "Failed to register license terms: Royalty policy is required when commercial use is enabled.", + ); + }); + }); + + describe("verify derivatives", () => { + beforeEach(() => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + }); + it("should throw derivativesAttribution error when call registerRILTerms given derivativesAllowed is false and derivativesAttribution is true", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + derivativesAllowed: false, + derivativesAttribution: true, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add derivative attribution when derivative use is disabled.", + ); + }); + + it("should throw derivativesApproval error when call registerRILTerms given derivativesAllowed is false and derivativesApproval is true", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + derivativesAllowed: false, + derivativesApproval: true, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add derivative approval when derivative use is disabled.", + ); + }); + + it("should throw derivativesReciprocal error when call registerRILTerms given derivativesAllowed is false and derivativesReciprocal is true", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + derivativesAllowed: false, + derivativesReciprocal: true, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add derivative reciprocal when derivative use is disabled.", + ); + }); + + it("should throw derivativeRevCeiling error when call registerRILTerms given derivativesAllowed is false and derivativeRevCeiling is more than 0", async () => { + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + derivativesAllowed: false, + derivativeRevCeiling: 1, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: Cannot add derivative revenue ceiling when derivative use is disabled.", + ); + }); + }); + + it("should return directly licenseTermsId when call registerPILTerms given request have already registered", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(1) }); + + const result = await licenseClient.registerPILTerms(licenseTerms); + + expect(result.licenseTermsId).to.equal(1n); + expect(result.txHash).to.equal(undefined); + }); + + it("should throw commercialRevShare error when call registerPILTerms given commercialRevShare is more than 100", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + defaultMintingFee: 1, + currency: MockERC20.address, + commercialRevShare: 101, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: CommercialRevShare should be between 0 and 100.", + ); + }); + it("should throw commercialRevShare error when call registerPILTerms given commercialRevShare is less than 0", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + await expect( + licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + defaultMintingFee: 1, + currency: MockERC20.address, + commercialRevShare: -1, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }), + ).to.be.rejectedWith( + "Failed to register license terms: CommercialRevShare should be between 0 and 100.", + ); + }); + it("should return encodedTxData when call registerPILTerms given txOptions.encodedTxDataOnly of true and args is correct", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + sinon + .stub(licenseClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: BigInt(0) }); + sinon + .stub(licenseClient.licenseTemplateClient, "registerLicenseTermsEncode") + .returns({ to: zeroAddress, data: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" }); + + const result = await licenseClient.registerPILTerms({ + ...licenseTerms, + txOptions: { + encodedTxDataOnly: true, + }, + }); + expect(result.encodedTxData).to.deep.equal({ + to: zeroAddress, + data: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }); + }); + + it("should return txHash when call registerPILTerms given args is correct", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + 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: zeroAddress, + licenseTerms: zeroAddress, + }, + ]); + + const result = await licenseClient.registerPILTerms({ + ...licenseTerms, + }); + + expect(result.txHash).to.equal(txHash); + }); + + it("should return txHash when call registerPILTerms given args is correct and waitForTransaction of true", async () => { + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyPolicy") + .resolves(true); + sinon + .stub(licenseClient.royaltyModuleReadOnlyClient, "isWhitelistedRoyaltyToken") + .resolves(true); + 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: zeroAddress, + licenseTerms: zeroAddress, + }, + ]); + + const result = await licenseClient.registerPILTerms({ + ...licenseTerms, + commercialUse: true, + defaultMintingFee: 1, + currency: MockERC20.address, + commercialRevShare: 90, + royaltyPolicy: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + txOptions: { + waitForTransaction: true, + }, + }); + + expect(result.txHash).to.equal(txHash); + expect(result.licenseTermsId).to.equal(1n); + }); + }); describe("Test licenseClient.registerNonComSocialRemixingPIL", async () => { it("should return licenseTermsId when call registerNonComSocialRemixingPIL given licenseTermsId is registered", async () => { sinon @@ -107,7 +522,7 @@ describe("Test LicenseClient", () => { .resolves({ selectedLicenseTermsId: BigInt(1) }); const result = await licenseClient.registerCommercialUsePIL({ - mintingFee: 1, + defaultMintingFee: 1, currency: zeroAddress, }); @@ -121,7 +536,7 @@ describe("Test LicenseClient", () => { sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); const result = await licenseClient.registerCommercialUsePIL({ - mintingFee: "1", + defaultMintingFee: "1", currency: zeroAddress, }); @@ -144,7 +559,7 @@ describe("Test LicenseClient", () => { ]); const result = await licenseClient.registerCommercialUsePIL({ - mintingFee: "1", + defaultMintingFee: "1", currency: zeroAddress, txOptions: { waitForTransaction: true, @@ -165,7 +580,7 @@ describe("Test LicenseClient", () => { try { await licenseClient.registerCommercialUsePIL({ - mintingFee: "1", + defaultMintingFee: "1", currency: zeroAddress, }); } catch (error) { @@ -183,7 +598,7 @@ describe("Test LicenseClient", () => { .resolves({ selectedLicenseTermsId: BigInt(1) }); const result = await licenseClient.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: zeroAddress, }); @@ -198,7 +613,7 @@ describe("Test LicenseClient", () => { sinon.stub(licenseClient.licenseTemplateClient, "registerLicenseTerms").resolves(txHash); const result = await licenseClient.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: zeroAddress, }); @@ -222,7 +637,7 @@ describe("Test LicenseClient", () => { ]); const result = await licenseClient.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: zeroAddress, txOptions: { @@ -244,7 +659,7 @@ describe("Test LicenseClient", () => { try { await licenseClient.registerCommercialRemixPIL({ - mintingFee: "1", + defaultMintingFee: "1", commercialRevShare: 100, currency: zeroAddress, }); diff --git a/packages/core-sdk/test/unit/utils/getLicenseTermsByType.test.ts b/packages/core-sdk/test/unit/utils/getLicenseTermsByType.test.ts index 948c9475..84ee0f97 100644 --- a/packages/core-sdk/test/unit/utils/getLicenseTermsByType.test.ts +++ b/packages/core-sdk/test/unit/utils/getLicenseTermsByType.test.ts @@ -30,7 +30,7 @@ describe("Get License Terms By Type", () => { describe("Get Commercial License Terms", () => { it("it should throw when call getLicenseTermByType given COMMERCIAL_USE without terms", async () => { expect(() => getLicenseTermByType(PIL_TYPE.COMMERCIAL_USE)).to.throw( - "mintingFee currency are required for commercial use PIL.", + "MintingFee currency are required for commercial use PIL.", ); }); @@ -40,7 +40,7 @@ describe("Get License Terms By Type", () => { currency: zeroAddress, royaltyPolicyLAPAddress: zeroAddress, }), - ).to.throw("mintingFee currency are required for commercial use PIL."); + ).to.throw("MintingFee currency are required for commercial use PIL."); }); it("it should throw when call getLicenseTermByType given COMMERCIAL_USE without currency", async () => { @@ -49,7 +49,7 @@ describe("Get License Terms By Type", () => { royaltyPolicyLAPAddress: zeroAddress, defaultMintingFee: "1", }), - ).to.throw("mintingFee currency are required for commercial use PIL."); + ).to.throw("MintingFee currency are required for commercial use PIL."); }); it("it should throw when call getLicenseTermByType given COMMERCIAL_USE and wrong royaltyAddress", async () => { @@ -93,7 +93,7 @@ describe("Get License Terms By Type", () => { describe("Get Commercial remix License Terms", () => { it("it should throw when call getLicenseTermByType given COMMERCIAL_REMIX without terms", async () => { expect(() => getLicenseTermByType(PIL_TYPE.COMMERCIAL_REMIX)).to.throw( - "mintingFee, currency and commercialRevShare are required for commercial remix PIL.", + "MintingFee, currency and commercialRevShare are required for commercial remix PIL.", ); }); @@ -105,7 +105,7 @@ describe("Get License Terms By Type", () => { commercialRevShare: 100, }), ).to.throw( - "mintingFee, currency and commercialRevShare are required for commercial remix PIL.", + "MintingFee, currency and commercialRevShare are required for commercial remix PIL.", ); }); @@ -117,7 +117,7 @@ describe("Get License Terms By Type", () => { commercialRevShare: 100, }), ).to.throw( - "mintingFee, currency and commercialRevShare are required for commercial remix PIL.", + "MintingFee, currency and commercialRevShare are required for commercial remix PIL.", ); }); @@ -140,7 +140,7 @@ describe("Get License Terms By Type", () => { currency: zeroAddress, }), ).to.throw( - `mintingFee, currency and commercialRevShare are required for commercial remix PIL.`, + `MintingFee, currency and commercialRevShare are required for commercial remix PIL.`, ); }); @@ -171,7 +171,7 @@ describe("Get License Terms By Type", () => { uri: "", }); }); - it("it throw commercialRevShare error when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is less than 0 ", async () => { + it("it throw commercialRevShare error when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is less than 0 ", async () => { expect(() => getLicenseTermByType(PIL_TYPE.COMMERCIAL_REMIX, { royaltyPolicyLAPAddress: zeroAddress, @@ -179,7 +179,7 @@ describe("Get License Terms By Type", () => { currency: zeroAddress, commercialRevShare: -8, }), - ).to.throw(`commercialRevShare should be between 0 and 100.`); + ).to.throw(`CommercialRevShare should be between 0 and 100.`); }); it("it throw commercialRevShare error when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is greater than 100", async () => { @@ -190,7 +190,7 @@ describe("Get License Terms By Type", () => { currency: zeroAddress, commercialRevShare: 105, }), - ).to.throw(`commercialRevShare should be between 0 and 100.`); + ).to.throw(`CommercialRevShare should be between 0 and 100.`); }); it("it get commercialRevShare correct value when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is 10", async () => { diff --git a/packages/core-sdk/test/unit/utils/utils.test.ts b/packages/core-sdk/test/unit/utils/utils.test.ts index 1eb99ed8..a7f5d9b0 100644 --- a/packages/core-sdk/test/unit/utils/utils.test.ts +++ b/packages/core-sdk/test/unit/utils/utils.test.ts @@ -7,10 +7,10 @@ import { chainStringToViemChain, waitTx, getAddress, - iliad, } from "../../../src/utils/utils"; import { createMock } from "../testUtils"; import { licensingModuleAbi } from "../../../src/abi/generated"; +import { iliad } from "../../../src/index"; describe("Test waitTxAndFilterLog", () => { const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; diff --git a/packages/wagmi-generator/wagmi.config.ts b/packages/wagmi-generator/wagmi.config.ts index af99818b..fb2fbea3 100644 --- a/packages/wagmi-generator/wagmi.config.ts +++ b/packages/wagmi-generator/wagmi.config.ts @@ -111,7 +111,7 @@ export default defineConfig(async () => { contracts: [], plugins: [ optimizedBlockExplorer({ - baseUrl: "https://testnet.storyscan.xyz", + baseUrl: "https://testnet.storyscan.xyz/api", name: "iliad", getAddress: await resolveProxyContracts({ baseUrl: "https://testnet.storyrpc.io", @@ -137,7 +137,7 @@ export default defineConfig(async () => { "raiseDispute", "resolveDispute", ], - IPAccountImpl: ["execute", "executeWithSig", "state"], + IPAccountImpl: ["execute", "executeWithSig", "state", "token"], IPAssetRegistry: [ "IPRegistered", "ipId", @@ -169,9 +169,10 @@ export default defineConfig(async () => { "registerDerivative", "getLicenseTerms", "LicenseTermsAttached", + "predictMintingLicenseFee", ], ModuleRegistry: ["isRegistered", "getDefaultLicenseTerms"], - RoyaltyModule: ["payRoyaltyOnBehalf"], + RoyaltyModule: ["payRoyaltyOnBehalf", "isWhitelistedRoyaltyPolicy","isWhitelistedRoyaltyToken"], RoyaltyPolicyLAP: ["onRoyaltyPayment", "getRoyaltyData"], LicenseToken: ["ownerOf"], SPG: [