diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1bab326f9..db924382a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @LeoHChen @DonFungible @edisonz0718 @jacob-tucker @AndyBoWu @allenchuang @bpolania +* @LeoHChen @DonFungible @edisonz0718 @jacob-tucker @AndyBoWu @allenchuang @bpolania @bonnie57 @DracoLi diff --git a/.github/workflows/pr-internal.yml b/.github/workflows/pr-internal.yml index 792ca0aa7..2a95f1412 100644 --- a/.github/workflows/pr-internal.yml +++ b/.github/workflows/pr-internal.yml @@ -24,6 +24,7 @@ jobs: secrets: WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }} TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} + JUDGE_PRIVATE_KEY: ${{ secrets.JUDGE_PRIVATE_KEY }} push_build_and_test: if: github.event_name == 'push' @@ -34,3 +35,4 @@ jobs: secrets: WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }} TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} + JUDGE_PRIVATE_KEY: ${{ secrets.JUDGE_PRIVATE_KEY }} diff --git a/packages/core-sdk/package.json b/packages/core-sdk/package.json index 8d257095d..7bc59fce7 100644 --- a/packages/core-sdk/package.json +++ b/packages/core-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@story-protocol/core-sdk", - "version": "1.3.0-beta.3", + "version": "1.3.0.rc.1", "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 73cd03f3c..21eab3787 100644 --- a/packages/core-sdk/src/abi/generated.ts +++ b/packages/core-sdk/src/abi/generated.ts @@ -2745,6 +2745,204 @@ export const disputeModuleConfig = { abi: disputeModuleAbi, } as const; +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ERC20 +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + +*/ +export const erc20Abi = [ + { type: "constructor", inputs: [], stateMutability: "nonpayable" }, + { + type: "error", + inputs: [ + { name: "spender", internalType: "address", type: "address" }, + { name: "allowance", internalType: "uint256", type: "uint256" }, + { name: "needed", internalType: "uint256", type: "uint256" }, + ], + name: "ERC20InsufficientAllowance", + }, + { + type: "error", + inputs: [ + { name: "sender", internalType: "address", type: "address" }, + { name: "balance", internalType: "uint256", type: "uint256" }, + { name: "needed", internalType: "uint256", type: "uint256" }, + ], + name: "ERC20InsufficientBalance", + }, + { + type: "error", + inputs: [{ name: "approver", internalType: "address", type: "address" }], + name: "ERC20InvalidApprover", + }, + { + type: "error", + inputs: [{ name: "receiver", internalType: "address", type: "address" }], + name: "ERC20InvalidReceiver", + }, + { + type: "error", + inputs: [{ name: "sender", internalType: "address", type: "address" }], + name: "ERC20InvalidSender", + }, + { + type: "error", + inputs: [{ name: "spender", internalType: "address", type: "address" }], + name: "ERC20InvalidSpender", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "owner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "spender", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "value", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "Approval", + }, + { + type: "event", + anonymous: false, + inputs: [ + { name: "from", internalType: "address", type: "address", indexed: true }, + { name: "to", internalType: "address", type: "address", indexed: true }, + { + name: "value", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "Transfer", + }, + { + type: "function", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "spender", internalType: "address", type: "address" }, + ], + name: "allowance", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "spender", internalType: "address", type: "address" }, + { name: "value", internalType: "uint256", type: "uint256" }, + ], + name: "approve", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "account", internalType: "address", type: "address" }], + name: "balanceOf", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "decimals", + outputs: [{ name: "", internalType: "uint8", type: "uint8" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "to", internalType: "address", type: "address" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "name", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "symbol", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "totalSupply", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "to", internalType: "address", type: "address" }, + { name: "value", internalType: "uint256", type: "uint256" }, + ], + name: "transfer", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "to", internalType: "address", type: "address" }, + { name: "value", internalType: "uint256", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, +] as const; + +/** + +*/ +export const erc20Address = { + 1315: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", + 1514: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", +} as const; + +/** + +*/ +export const erc20Config = { address: erc20Address, abi: erc20Abi } as const; + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // EvenSplitGroupPool ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -5593,6 +5791,16 @@ export const ipRoyaltyVaultImplAbi = [ ], stateMutability: "nonpayable", }, + { + type: "error", + inputs: [{ name: "target", internalType: "address", type: "address" }], + name: "AddressEmptyCode", + }, + { + type: "error", + inputs: [{ name: "account", internalType: "address", type: "address" }], + name: "AddressInsufficientBalance", + }, { type: "error", inputs: [ @@ -5631,6 +5839,7 @@ export const ipRoyaltyVaultImplAbi = [ inputs: [{ name: "spender", internalType: "address", type: "address" }], name: "ERC20InvalidSpender", }, + { type: "error", inputs: [], name: "FailedInnerCall" }, { type: "error", inputs: [], name: "InvalidInitialization" }, { type: "error", inputs: [], name: "IpRoyaltyVault__EnforcedPause" }, { @@ -9648,251 +9857,50 @@ export const licensingModuleConfig = { } as const; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// MockERC20 +// ModuleRegistry ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** */ -export const mockErc20Abi = [ +export const moduleRegistryAbi = [ { type: "constructor", inputs: [], stateMutability: "nonpayable" }, { type: "error", - inputs: [ - { name: "spender", internalType: "address", type: "address" }, - { name: "allowance", internalType: "uint256", type: "uint256" }, - { name: "needed", internalType: "uint256", type: "uint256" }, - ], - name: "ERC20InsufficientAllowance", + inputs: [{ name: "authority", internalType: "address", type: "address" }], + name: "AccessManagedInvalidAuthority", }, { type: "error", inputs: [ - { name: "sender", internalType: "address", type: "address" }, - { name: "balance", internalType: "uint256", type: "uint256" }, - { name: "needed", internalType: "uint256", type: "uint256" }, + { name: "caller", internalType: "address", type: "address" }, + { name: "delay", internalType: "uint32", type: "uint32" }, ], - name: "ERC20InsufficientBalance", + name: "AccessManagedRequiredDelay", }, { type: "error", - inputs: [{ name: "approver", internalType: "address", type: "address" }], - name: "ERC20InvalidApprover", + inputs: [{ name: "caller", internalType: "address", type: "address" }], + name: "AccessManagedUnauthorized", }, { type: "error", - inputs: [{ name: "receiver", internalType: "address", type: "address" }], - name: "ERC20InvalidReceiver", + inputs: [{ name: "target", internalType: "address", type: "address" }], + name: "AddressEmptyCode", }, { type: "error", - inputs: [{ name: "sender", internalType: "address", type: "address" }], - name: "ERC20InvalidSender", + inputs: [{ name: "implementation", internalType: "address", type: "address" }], + name: "ERC1967InvalidImplementation", }, + { type: "error", inputs: [], name: "ERC1967NonPayable" }, + { type: "error", inputs: [], name: "FailedCall" }, + { type: "error", inputs: [], name: "InvalidInitialization" }, + { type: "error", inputs: [], name: "ModuleRegistry__InterfaceIdZero" }, { type: "error", - inputs: [{ name: "spender", internalType: "address", type: "address" }], - name: "ERC20InvalidSpender", - }, - { - type: "event", - anonymous: false, - inputs: [ - { - name: "owner", - internalType: "address", - type: "address", - indexed: true, - }, - { - name: "spender", - internalType: "address", - type: "address", - indexed: true, - }, - { - name: "value", - internalType: "uint256", - type: "uint256", - indexed: false, - }, - ], - name: "Approval", - }, - { - type: "event", - anonymous: false, - inputs: [ - { name: "from", internalType: "address", type: "address", indexed: true }, - { name: "to", internalType: "address", type: "address", indexed: true }, - { - name: "value", - internalType: "uint256", - type: "uint256", - indexed: false, - }, - ], - name: "Transfer", - }, - { - type: "function", - inputs: [ - { name: "owner", internalType: "address", type: "address" }, - { name: "spender", internalType: "address", type: "address" }, - ], - name: "allowance", - outputs: [{ name: "", internalType: "uint256", type: "uint256" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [ - { name: "spender", internalType: "address", type: "address" }, - { name: "value", internalType: "uint256", type: "uint256" }, - ], - name: "approve", - outputs: [{ name: "", internalType: "bool", type: "bool" }], - stateMutability: "nonpayable", - }, - { - type: "function", - inputs: [{ name: "account", internalType: "address", type: "address" }], - name: "balanceOf", - outputs: [{ name: "", internalType: "uint256", type: "uint256" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [ - { name: "from", internalType: "address", type: "address" }, - { name: "amount", internalType: "uint256", type: "uint256" }, - ], - name: "burn", - outputs: [], - stateMutability: "nonpayable", - }, - { - type: "function", - inputs: [], - name: "decimals", - outputs: [{ name: "", internalType: "uint8", type: "uint8" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [ - { name: "to", internalType: "address", type: "address" }, - { name: "amount", internalType: "uint256", type: "uint256" }, - ], - name: "mint", - outputs: [], - stateMutability: "nonpayable", - }, - { - type: "function", - inputs: [], - name: "name", - outputs: [{ name: "", internalType: "string", type: "string" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [], - name: "symbol", - outputs: [{ name: "", internalType: "string", type: "string" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [], - name: "totalSupply", - outputs: [{ name: "", internalType: "uint256", type: "uint256" }], - stateMutability: "view", - }, - { - type: "function", - inputs: [ - { name: "to", internalType: "address", type: "address" }, - { name: "value", internalType: "uint256", type: "uint256" }, - ], - name: "transfer", - outputs: [{ name: "", internalType: "bool", type: "bool" }], - stateMutability: "nonpayable", - }, - { - type: "function", - inputs: [ - { name: "from", internalType: "address", type: "address" }, - { name: "to", internalType: "address", type: "address" }, - { name: "value", internalType: "uint256", type: "uint256" }, - ], - name: "transferFrom", - outputs: [{ name: "", internalType: "bool", type: "bool" }], - stateMutability: "nonpayable", - }, -] as const; - -/** - -*/ -export const mockErc20Address = { - 1315: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", - 1514: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", -} as const; - -/** - -*/ -export const mockErc20Config = { - address: mockErc20Address, - abi: mockErc20Abi, -} as const; - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// ModuleRegistry -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - -*/ -export const moduleRegistryAbi = [ - { type: "constructor", inputs: [], stateMutability: "nonpayable" }, - { - type: "error", - inputs: [{ name: "authority", internalType: "address", type: "address" }], - name: "AccessManagedInvalidAuthority", - }, - { - type: "error", - inputs: [ - { name: "caller", internalType: "address", type: "address" }, - { name: "delay", internalType: "uint32", type: "uint32" }, - ], - name: "AccessManagedRequiredDelay", - }, - { - type: "error", - inputs: [{ name: "caller", internalType: "address", type: "address" }], - name: "AccessManagedUnauthorized", - }, - { - 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: "FailedCall" }, - { type: "error", inputs: [], name: "InvalidInitialization" }, - { type: "error", inputs: [], name: "ModuleRegistry__InterfaceIdZero" }, - { - type: "error", - inputs: [], - name: "ModuleRegistry__ModuleAddressNotContract", + inputs: [], + name: "ModuleRegistry__ModuleAddressNotContract", }, { type: "error", @@ -14473,7 +14481,7 @@ export const spgnftImplAbi = [ }, { type: "error", inputs: [], name: "InvalidInitialization" }, { type: "error", inputs: [], name: "NotInitializing" }, - { type: "error", inputs: [], name: "SPGNFT__CallerNotFeeRecipientOrAdmin" }, + { type: "error", inputs: [], name: "SPGNFT__CallerNotFeeRecipient" }, { type: "error", inputs: [], name: "SPGNFT__CallerNotPeripheryContract" }, { type: "error", @@ -14489,11 +14497,6 @@ export const spgnftImplAbi = [ { type: "error", inputs: [], name: "SPGNFT__MintingDenied" }, { type: "error", inputs: [], name: "SPGNFT__ZeroAddressParam" }, { type: "error", inputs: [], name: "SPGNFT__ZeroMaxSupply" }, - { - type: "error", - inputs: [{ name: "token", internalType: "address", type: "address" }], - name: "SafeERC20FailedOperation", - }, { type: "event", anonymous: false, @@ -15202,7 +15205,7 @@ export const wrappedIpAbi = [ inputs: [], name: "name", outputs: [{ name: "", internalType: "string", type: "string" }], - stateMutability: "view", + stateMutability: "pure", }, { type: "function", @@ -15231,7 +15234,7 @@ export const wrappedIpAbi = [ inputs: [], name: "symbol", outputs: [{ name: "", internalType: "string", type: "string" }], - stateMutability: "view", + stateMutability: "pure", }, { type: "function", @@ -15558,6 +15561,17 @@ export class AccessControllerClient extends AccessControllerEventClient { // Contract ArbitrationPolicyUMA ============================================================= +/** + * ArbitrationPolicyUmaDisputeIdToAssertionIdRequest + * + * @param disputeId uint256 + */ +export type ArbitrationPolicyUmaDisputeIdToAssertionIdRequest = { + disputeId: bigint; +}; + +export type ArbitrationPolicyUmaDisputeIdToAssertionIdResponse = Hex; + /** * ArbitrationPolicyUmaMaxBondsRequest * @@ -15573,6 +15587,17 @@ export type ArbitrationPolicyUmaMaxLivenessResponse = bigint; export type ArbitrationPolicyUmaMinLivenessResponse = bigint; +/** + * ArbitrationPolicyUmaDisputeAssertionRequest + * + * @param assertionId bytes32 + * @param counterEvidenceHash bytes32 + */ +export type ArbitrationPolicyUmaDisputeAssertionRequest = { + assertionId: Hex; + counterEvidenceHash: Hex; +}; + /** * contract ArbitrationPolicyUMA readonly method */ @@ -15585,6 +15610,23 @@ export class ArbitrationPolicyUmaReadOnlyClient { this.rpcClient = rpcClient; } + /** + * method disputeIdToAssertionId for contract ArbitrationPolicyUMA + * + * @param request ArbitrationPolicyUmaDisputeIdToAssertionIdRequest + * @return Promise + */ + public async disputeIdToAssertionId( + request: ArbitrationPolicyUmaDisputeIdToAssertionIdRequest, + ): Promise { + return await this.rpcClient.readContract({ + abi: arbitrationPolicyUmaAbi, + address: this.address, + functionName: "disputeIdToAssertionId", + args: [request.disputeId], + }); + } + /** * method maxBonds for contract ArbitrationPolicyUMA * @@ -15631,6 +15673,56 @@ export class ArbitrationPolicyUmaReadOnlyClient { } } +/** + * contract ArbitrationPolicyUMA write method + */ +export class ArbitrationPolicyUmaClient extends ArbitrationPolicyUmaReadOnlyClient { + protected readonly wallet: SimpleWalletClient; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address?: Address) { + super(rpcClient, address); + this.wallet = wallet; + } + + /** + * method disputeAssertion for contract ArbitrationPolicyUMA + * + * @param request ArbitrationPolicyUmaDisputeAssertionRequest + * @return Promise + */ + public async disputeAssertion( + request: ArbitrationPolicyUmaDisputeAssertionRequest, + ): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: arbitrationPolicyUmaAbi, + address: this.address, + functionName: "disputeAssertion", + account: this.wallet.account, + args: [request.assertionId, request.counterEvidenceHash], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method disputeAssertion for contract ArbitrationPolicyUMA with only encode + * + * @param request ArbitrationPolicyUmaDisputeAssertionRequest + * @return EncodedTxData + */ + public disputeAssertionEncode( + request: ArbitrationPolicyUmaDisputeAssertionRequest, + ): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: arbitrationPolicyUmaAbi, + functionName: "disputeAssertion", + args: [request.assertionId, request.counterEvidenceHash], + }), + }; + } +} + // Contract CoreMetadataModule ============================================================= /** @@ -16964,6 +17056,17 @@ export type DisputeModuleResolveDisputeRequest = { data: Hex; }; +/** + * DisputeModuleTagIfRelatedIpInfringedRequest + * + * @param ipIdToTag address + * @param infringerDisputeId uint256 + */ +export type DisputeModuleTagIfRelatedIpInfringedRequest = { + ipIdToTag: Address; + infringerDisputeId: bigint; +}; + /** * contract DisputeModule event */ @@ -17247,12 +17350,268 @@ export class DisputeModuleClient extends DisputeModuleReadOnlyClient { }), }; } -} -// Contract EvenSplitGroupPool ============================================================= - -/** - * EvenSplitGroupPoolAuthorityUpdatedEvent + /** + * method tagIfRelatedIpInfringed for contract DisputeModule + * + * @param request DisputeModuleTagIfRelatedIpInfringedRequest + * @return Promise + */ + public async tagIfRelatedIpInfringed( + request: DisputeModuleTagIfRelatedIpInfringedRequest, + ): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: disputeModuleAbi, + address: this.address, + functionName: "tagIfRelatedIpInfringed", + account: this.wallet.account, + args: [request.ipIdToTag, request.infringerDisputeId], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method tagIfRelatedIpInfringed for contract DisputeModule with only encode + * + * @param request DisputeModuleTagIfRelatedIpInfringedRequest + * @return EncodedTxData + */ + public tagIfRelatedIpInfringedEncode( + request: DisputeModuleTagIfRelatedIpInfringedRequest, + ): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: disputeModuleAbi, + functionName: "tagIfRelatedIpInfringed", + args: [request.ipIdToTag, request.infringerDisputeId], + }), + }; + } +} + +// Contract ERC20 ============================================================= + +/** + * Erc20AllowanceRequest + * + * @param owner address + * @param spender address + */ +export type Erc20AllowanceRequest = { + owner: Address; + spender: Address; +}; + +export type Erc20AllowanceResponse = bigint; + +/** + * Erc20BalanceOfRequest + * + * @param account address + */ +export type Erc20BalanceOfRequest = { + account: Address; +}; + +export type Erc20BalanceOfResponse = bigint; + +/** + * Erc20ApproveRequest + * + * @param spender address + * @param value uint256 + */ +export type Erc20ApproveRequest = { + spender: Address; + value: bigint; +}; + +/** + * Erc20MintRequest + * + * @param to address + * @param amount uint256 + */ +export type Erc20MintRequest = { + to: Address; + amount: bigint; +}; + +/** + * Erc20TransferFromRequest + * + * @param from address + * @param to address + * @param value uint256 + */ +export type Erc20TransferFromRequest = { + from: Address; + to: Address; + value: bigint; +}; + +/** + * contract ERC20 readonly method + */ +export class Erc20ReadOnlyClient { + protected readonly rpcClient: PublicClient; + public readonly address: Address; + + constructor(rpcClient: PublicClient, address?: Address) { + this.address = address || getAddress(erc20Address, rpcClient.chain?.id); + this.rpcClient = rpcClient; + } + + /** + * method allowance for contract ERC20 + * + * @param request Erc20AllowanceRequest + * @return Promise + */ + public async allowance(request: Erc20AllowanceRequest): Promise { + return await this.rpcClient.readContract({ + abi: erc20Abi, + address: this.address, + functionName: "allowance", + args: [request.owner, request.spender], + }); + } + + /** + * method balanceOf for contract ERC20 + * + * @param request Erc20BalanceOfRequest + * @return Promise + */ + public async balanceOf(request: Erc20BalanceOfRequest): Promise { + return await this.rpcClient.readContract({ + abi: erc20Abi, + address: this.address, + functionName: "balanceOf", + args: [request.account], + }); + } +} + +/** + * contract ERC20 write method + */ +export class Erc20Client extends Erc20ReadOnlyClient { + protected readonly wallet: SimpleWalletClient; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address?: Address) { + super(rpcClient, address); + this.wallet = wallet; + } + + /** + * method approve for contract ERC20 + * + * @param request Erc20ApproveRequest + * @return Promise + */ + public async approve(request: Erc20ApproveRequest): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: erc20Abi, + address: this.address, + functionName: "approve", + account: this.wallet.account, + args: [request.spender, request.value], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method approve for contract ERC20 with only encode + * + * @param request Erc20ApproveRequest + * @return EncodedTxData + */ + public approveEncode(request: Erc20ApproveRequest): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [request.spender, request.value], + }), + }; + } + + /** + * method mint for contract ERC20 + * + * @param request Erc20MintRequest + * @return Promise + */ + public async mint(request: Erc20MintRequest): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: erc20Abi, + address: this.address, + functionName: "mint", + account: this.wallet.account, + args: [request.to, request.amount], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method mint for contract ERC20 with only encode + * + * @param request Erc20MintRequest + * @return EncodedTxData + */ + public mintEncode(request: Erc20MintRequest): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "mint", + args: [request.to, request.amount], + }), + }; + } + + /** + * method transferFrom for contract ERC20 + * + * @param request Erc20TransferFromRequest + * @return Promise + */ + public async transferFrom(request: Erc20TransferFromRequest): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: erc20Abi, + address: this.address, + functionName: "transferFrom", + account: this.wallet.account, + args: [request.from, request.to, request.value], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method transferFrom for contract ERC20 with only encode + * + * @param request Erc20TransferFromRequest + * @return EncodedTxData + */ + public transferFromEncode(request: Erc20TransferFromRequest): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "transferFrom", + args: [request.from, request.to, request.value], + }), + }; + } +} + +// Contract EvenSplitGroupPool ============================================================= + +/** + * EvenSplitGroupPoolAuthorityUpdatedEvent * * @param authority address */ @@ -23453,542 +23812,55 @@ export class LicensingModuleClient extends LicensingModuleReadOnlyClient { } } -// Contract MockERC20 ============================================================= +// Contract ModuleRegistry ============================================================= /** - * MockErc20ApprovalEvent + * ModuleRegistryIsRegisteredRequest * - * @param owner address - * @param spender address - * @param value uint256 + * @param moduleAddress address */ -export type MockErc20ApprovalEvent = { - owner: Address; - spender: Address; - value: bigint; +export type ModuleRegistryIsRegisteredRequest = { + moduleAddress: Address; }; -/** - * MockErc20TransferEvent - * - * @param from address - * @param to address - * @param value uint256 - */ -export type MockErc20TransferEvent = { - from: Address; - to: Address; - value: bigint; -}; +export type ModuleRegistryIsRegisteredResponse = boolean; /** - * MockErc20AllowanceRequest - * - * @param owner address - * @param spender address + * contract ModuleRegistry readonly method */ -export type MockErc20AllowanceRequest = { - owner: Address; - spender: Address; -}; +export class ModuleRegistryReadOnlyClient { + protected readonly rpcClient: PublicClient; + public readonly address: Address; + + constructor(rpcClient: PublicClient, address?: Address) { + this.address = address || getAddress(moduleRegistryAddress, rpcClient.chain?.id); + this.rpcClient = rpcClient; + } + + /** + * method isRegistered for contract ModuleRegistry + * + * @param request ModuleRegistryIsRegisteredRequest + * @return Promise + */ + public async isRegistered( + request: ModuleRegistryIsRegisteredRequest, + ): Promise { + return await this.rpcClient.readContract({ + abi: moduleRegistryAbi, + address: this.address, + functionName: "isRegistered", + args: [request.moduleAddress], + }); + } +} -export type MockErc20AllowanceResponse = bigint; +// Contract Multicall3 ============================================================= /** - * MockErc20BalanceOfRequest + * Multicall3Aggregate3Request * - * @param account address - */ -export type MockErc20BalanceOfRequest = { - account: Address; -}; - -export type MockErc20BalanceOfResponse = bigint; - -export type MockErc20DecimalsResponse = number; - -export type MockErc20NameResponse = string; - -export type MockErc20SymbolResponse = string; - -export type MockErc20TotalSupplyResponse = bigint; - -/** - * MockErc20ApproveRequest - * - * @param spender address - * @param value uint256 - */ -export type MockErc20ApproveRequest = { - spender: Address; - value: bigint; -}; - -/** - * MockErc20BurnRequest - * - * @param from address - * @param amount uint256 - */ -export type MockErc20BurnRequest = { - from: Address; - amount: bigint; -}; - -/** - * MockErc20MintRequest - * - * @param to address - * @param amount uint256 - */ -export type MockErc20MintRequest = { - to: Address; - amount: bigint; -}; - -/** - * MockErc20TransferRequest - * - * @param to address - * @param value uint256 - */ -export type MockErc20TransferRequest = { - to: Address; - value: bigint; -}; - -/** - * MockErc20TransferFromRequest - * - * @param from address - * @param to address - * @param value uint256 - */ -export type MockErc20TransferFromRequest = { - from: Address; - to: Address; - value: bigint; -}; - -/** - * contract MockERC20 event - */ -export class MockErc20EventClient { - protected readonly rpcClient: PublicClient; - public readonly address: Address; - - constructor(rpcClient: PublicClient, address?: Address) { - this.address = address || getAddress(mockErc20Address, rpcClient.chain?.id); - this.rpcClient = rpcClient; - } - - /** - * event Approval for contract MockERC20 - */ - public watchApprovalEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: mockErc20Abi, - address: this.address, - eventName: "Approval", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Approval for contract MockERC20 - */ - public parseTxApprovalEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: mockErc20Abi, - eventName: "Approval", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Approval") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } - - /** - * event Transfer for contract MockERC20 - */ - public watchTransferEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: mockErc20Abi, - address: this.address, - eventName: "Transfer", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Transfer for contract MockERC20 - */ - public parseTxTransferEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: mockErc20Abi, - eventName: "Transfer", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Transfer") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } -} - -/** - * contract MockERC20 readonly method - */ -export class MockErc20ReadOnlyClient extends MockErc20EventClient { - constructor(rpcClient: PublicClient, address?: Address) { - super(rpcClient, address); - } - - /** - * method allowance for contract MockERC20 - * - * @param request MockErc20AllowanceRequest - * @return Promise - */ - public async allowance(request: MockErc20AllowanceRequest): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "allowance", - args: [request.owner, request.spender], - }); - } - - /** - * method balanceOf for contract MockERC20 - * - * @param request MockErc20BalanceOfRequest - * @return Promise - */ - public async balanceOf(request: MockErc20BalanceOfRequest): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "balanceOf", - args: [request.account], - }); - } - - /** - * method decimals for contract MockERC20 - * - * @param request MockErc20DecimalsRequest - * @return Promise - */ - public async decimals(): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "decimals", - }); - } - - /** - * method name for contract MockERC20 - * - * @param request MockErc20NameRequest - * @return Promise - */ - public async name(): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "name", - }); - } - - /** - * method symbol for contract MockERC20 - * - * @param request MockErc20SymbolRequest - * @return Promise - */ - public async symbol(): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "symbol", - }); - } - - /** - * method totalSupply for contract MockERC20 - * - * @param request MockErc20TotalSupplyRequest - * @return Promise - */ - public async totalSupply(): Promise { - return await this.rpcClient.readContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "totalSupply", - }); - } -} - -/** - * contract MockERC20 write method - */ -export class MockErc20Client extends MockErc20ReadOnlyClient { - protected readonly wallet: SimpleWalletClient; - - constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address?: Address) { - super(rpcClient, address); - this.wallet = wallet; - } - - /** - * method approve for contract MockERC20 - * - * @param request MockErc20ApproveRequest - * @return Promise - */ - public async approve(request: MockErc20ApproveRequest): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "approve", - account: this.wallet.account, - args: [request.spender, request.value], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method approve for contract MockERC20 with only encode - * - * @param request MockErc20ApproveRequest - * @return EncodedTxData - */ - public approveEncode(request: MockErc20ApproveRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: mockErc20Abi, - functionName: "approve", - args: [request.spender, request.value], - }), - }; - } - - /** - * method burn for contract MockERC20 - * - * @param request MockErc20BurnRequest - * @return Promise - */ - public async burn(request: MockErc20BurnRequest): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "burn", - account: this.wallet.account, - args: [request.from, request.amount], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method burn for contract MockERC20 with only encode - * - * @param request MockErc20BurnRequest - * @return EncodedTxData - */ - public burnEncode(request: MockErc20BurnRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: mockErc20Abi, - functionName: "burn", - args: [request.from, request.amount], - }), - }; - } - - /** - * method mint for contract MockERC20 - * - * @param request MockErc20MintRequest - * @return Promise - */ - public async mint(request: MockErc20MintRequest): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "mint", - account: this.wallet.account, - args: [request.to, request.amount], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method mint for contract MockERC20 with only encode - * - * @param request MockErc20MintRequest - * @return EncodedTxData - */ - public mintEncode(request: MockErc20MintRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: mockErc20Abi, - functionName: "mint", - args: [request.to, request.amount], - }), - }; - } - - /** - * method transfer for contract MockERC20 - * - * @param request MockErc20TransferRequest - * @return Promise - */ - public async transfer(request: MockErc20TransferRequest): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "transfer", - account: this.wallet.account, - args: [request.to, request.value], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method transfer for contract MockERC20 with only encode - * - * @param request MockErc20TransferRequest - * @return EncodedTxData - */ - public transferEncode(request: MockErc20TransferRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: mockErc20Abi, - functionName: "transfer", - args: [request.to, request.value], - }), - }; - } - - /** - * method transferFrom for contract MockERC20 - * - * @param request MockErc20TransferFromRequest - * @return Promise - */ - public async transferFrom( - request: MockErc20TransferFromRequest, - ): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: mockErc20Abi, - address: this.address, - functionName: "transferFrom", - account: this.wallet.account, - args: [request.from, request.to, request.value], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method transferFrom for contract MockERC20 with only encode - * - * @param request MockErc20TransferFromRequest - * @return EncodedTxData - */ - public transferFromEncode(request: MockErc20TransferFromRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: mockErc20Abi, - functionName: "transferFrom", - args: [request.from, request.to, request.value], - }), - }; - } -} - -// Contract ModuleRegistry ============================================================= - -/** - * ModuleRegistryIsRegisteredRequest - * - * @param moduleAddress address - */ -export type ModuleRegistryIsRegisteredRequest = { - moduleAddress: Address; -}; - -export type ModuleRegistryIsRegisteredResponse = boolean; - -/** - * contract ModuleRegistry readonly method - */ -export class ModuleRegistryReadOnlyClient { - protected readonly rpcClient: PublicClient; - public readonly address: Address; - - constructor(rpcClient: PublicClient, address?: Address) { - this.address = address || getAddress(moduleRegistryAddress, rpcClient.chain?.id); - this.rpcClient = rpcClient; - } - - /** - * method isRegistered for contract ModuleRegistry - * - * @param request ModuleRegistryIsRegisteredRequest - * @return Promise - */ - public async isRegistered( - request: ModuleRegistryIsRegisteredRequest, - ): Promise { - return await this.rpcClient.readContract({ - abi: moduleRegistryAbi, - address: this.address, - functionName: "isRegistered", - args: [request.moduleAddress], - }); - } -} - -// Contract Multicall3 ============================================================= - -/** - * Multicall3Aggregate3Request - * - * @param calls tuple[] + * @param calls tuple[] */ export type Multicall3Aggregate3Request = { calls: { @@ -27519,6 +27391,15 @@ export type RoyaltyWorkflowsClaimAllRevenueRequest = { currencyTokens: readonly Address[]; }; +/** + * RoyaltyWorkflowsMulticallRequest + * + * @param data bytes[] + */ +export type RoyaltyWorkflowsMulticallRequest = { + data: readonly Hex[]; +}; + /** * contract RoyaltyWorkflows write method */ @@ -27580,6 +27461,42 @@ export class RoyaltyWorkflowsClient { }), }; } + + /** + * method multicall for contract RoyaltyWorkflows + * + * @param request RoyaltyWorkflowsMulticallRequest + * @return Promise + */ + public async multicall( + request: RoyaltyWorkflowsMulticallRequest, + ): Promise { + const { request: call } = await this.rpcClient.simulateContract({ + abi: royaltyWorkflowsAbi, + address: this.address, + functionName: "multicall", + account: this.wallet.account, + args: [request.data], + }); + return await this.wallet.writeContract(call as WriteContractParameters); + } + + /** + * method multicall for contract RoyaltyWorkflows with only encode + * + * @param request RoyaltyWorkflowsMulticallRequest + * @return EncodedTxData + */ + public multicallEncode(request: RoyaltyWorkflowsMulticallRequest): EncodedTxData { + return { + to: this.address, + data: encodeFunctionData({ + abi: royaltyWorkflowsAbi, + functionName: "multicall", + args: [request.data], + }), + }; + } } // Contract SPGNFTBeacon ============================================================= @@ -29788,63 +29705,6 @@ export class SpgnftImplClient extends SpgnftImplReadOnlyClient { // Contract WrappedIP ============================================================= -/** - * WrappedIpApprovalEvent - * - * @param owner address - * @param spender address - * @param amount uint256 - */ -export type WrappedIpApprovalEvent = { - owner: Address; - spender: Address; - amount: bigint; -}; - -/** - * WrappedIpDepositEvent - * - * @param from address - * @param amount uint256 - */ -export type WrappedIpDepositEvent = { - from: Address; - amount: bigint; -}; - -/** - * WrappedIpTransferEvent - * - * @param from address - * @param to address - * @param amount uint256 - */ -export type WrappedIpTransferEvent = { - from: Address; - to: Address; - amount: bigint; -}; - -/** - * WrappedIpWithdrawalEvent - * - * @param to address - * @param amount uint256 - */ -export type WrappedIpWithdrawalEvent = { - to: Address; - amount: bigint; -}; - -/** - * WrappedIpDomainSeparatorResponse - * - * @param result bytes32 - */ -export type WrappedIpDomainSeparatorResponse = { - result: Hex; -}; - /** * WrappedIpAllowanceRequest * @@ -29861,58 +29721,25 @@ export type WrappedIpAllowanceRequest = { * * @param result uint256 */ -export type WrappedIpAllowanceResponse = { - result: bigint; -}; - -/** - * WrappedIpBalanceOfRequest - * - * @param owner address - */ -export type WrappedIpBalanceOfRequest = { - owner: Address; -}; - -/** - * WrappedIpBalanceOfResponse - * - * @param result uint256 - */ -export type WrappedIpBalanceOfResponse = { - result: bigint; -}; - -export type WrappedIpDecimalsResponse = number; - -export type WrappedIpNameResponse = string; - -/** - * WrappedIpNoncesRequest - * - * @param owner address - */ -export type WrappedIpNoncesRequest = { - owner: Address; -}; - -/** - * WrappedIpNoncesResponse - * - * @param result uint256 - */ -export type WrappedIpNoncesResponse = { +export type WrappedIpAllowanceResponse = { result: bigint; }; -export type WrappedIpSymbolResponse = string; +/** + * WrappedIpBalanceOfRequest + * + * @param owner address + */ +export type WrappedIpBalanceOfRequest = { + owner: Address; +}; /** - * WrappedIpTotalSupplyResponse + * WrappedIpBalanceOfResponse * * @param result uint256 */ -export type WrappedIpTotalSupplyResponse = { +export type WrappedIpBalanceOfResponse = { result: bigint; }; @@ -29927,27 +29754,6 @@ export type WrappedIpApproveRequest = { amount: bigint; }; -/** - * WrappedIpPermitRequest - * - * @param owner address - * @param spender address - * @param value uint256 - * @param deadline uint256 - * @param v uint8 - * @param r bytes32 - * @param s bytes32 - */ -export type WrappedIpPermitRequest = { - owner: Address; - spender: Address; - value: bigint; - deadline: bigint; - v: number; - r: Hex; - s: Hex; -}; - /** * WrappedIpTransferRequest * @@ -29982,9 +29788,9 @@ export type WrappedIpWithdrawRequest = { }; /** - * contract WrappedIP event + * contract WrappedIP readonly method */ -export class WrappedIpEventClient { +export class WrappedIpReadOnlyClient { protected readonly rpcClient: PublicClient; public readonly address: Address; @@ -29993,188 +29799,6 @@ export class WrappedIpEventClient { this.rpcClient = rpcClient; } - /** - * event Approval for contract WrappedIP - */ - public watchApprovalEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: wrappedIpAbi, - address: this.address, - eventName: "Approval", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Approval for contract WrappedIP - */ - public parseTxApprovalEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: wrappedIpAbi, - eventName: "Approval", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Approval") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } - - /** - * event Deposit for contract WrappedIP - */ - public watchDepositEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: wrappedIpAbi, - address: this.address, - eventName: "Deposit", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Deposit for contract WrappedIP - */ - public parseTxDepositEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: wrappedIpAbi, - eventName: "Deposit", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Deposit") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } - - /** - * event Transfer for contract WrappedIP - */ - public watchTransferEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: wrappedIpAbi, - address: this.address, - eventName: "Transfer", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Transfer for contract WrappedIP - */ - public parseTxTransferEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: wrappedIpAbi, - eventName: "Transfer", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Transfer") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } - - /** - * event Withdrawal for contract WrappedIP - */ - public watchWithdrawalEvent( - onLogs: (txHash: Hex, ev: Partial) => void, - ): WatchContractEventReturnType { - return this.rpcClient.watchContractEvent({ - abi: wrappedIpAbi, - address: this.address, - eventName: "Withdrawal", - onLogs: (evs) => { - evs.forEach((it) => onLogs(it.transactionHash, it.args)); - }, - }); - } - - /** - * parse tx receipt event Withdrawal for contract WrappedIP - */ - public parseTxWithdrawalEvent(txReceipt: TransactionReceipt): Array { - const targetLogs: Array = []; - for (const log of txReceipt.logs) { - try { - const event = decodeEventLog({ - abi: wrappedIpAbi, - eventName: "Withdrawal", - data: log.data, - topics: log.topics, - }); - if (event.eventName === "Withdrawal") { - targetLogs.push(event.args); - } - } catch (e) { - /* empty */ - } - } - return targetLogs; - } -} - -/** - * contract WrappedIP readonly method - */ -export class WrappedIpReadOnlyClient extends WrappedIpEventClient { - constructor(rpcClient: PublicClient, address?: Address) { - super(rpcClient, address); - } - - /** - * method DOMAIN_SEPARATOR for contract WrappedIP - * - * @param request WrappedIpDomainSeparatorRequest - * @return Promise - */ - public async domainSeparator(): Promise { - const result = await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "DOMAIN_SEPARATOR", - }); - return { - result: result, - }; - } - /** * method allowance for contract WrappedIP * @@ -30210,83 +29834,6 @@ export class WrappedIpReadOnlyClient extends WrappedIpEventClient { result: result, }; } - - /** - * method decimals for contract WrappedIP - * - * @param request WrappedIpDecimalsRequest - * @return Promise - */ - public async decimals(): Promise { - return await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "decimals", - }); - } - - /** - * method name for contract WrappedIP - * - * @param request WrappedIpNameRequest - * @return Promise - */ - public async name(): Promise { - return await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "name", - }); - } - - /** - * method nonces for contract WrappedIP - * - * @param request WrappedIpNoncesRequest - * @return Promise - */ - public async nonces(request: WrappedIpNoncesRequest): Promise { - const result = await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "nonces", - args: [request.owner], - }); - return { - result: result, - }; - } - - /** - * method symbol for contract WrappedIP - * - * @param request WrappedIpSymbolRequest - * @return Promise - */ - public async symbol(): Promise { - return await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "symbol", - }); - } - - /** - * method totalSupply for contract WrappedIP - * - * @param request WrappedIpTotalSupplyRequest - * @return Promise - */ - public async totalSupply(): Promise { - const result = await this.rpcClient.readContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "totalSupply", - }); - return { - result: result, - }; - } } /** @@ -30366,56 +29913,6 @@ export class WrappedIpClient extends WrappedIpReadOnlyClient { }; } - /** - * method permit for contract WrappedIP - * - * @param request WrappedIpPermitRequest - * @return Promise - */ - public async permit(request: WrappedIpPermitRequest): Promise { - const { request: call } = await this.rpcClient.simulateContract({ - abi: wrappedIpAbi, - address: this.address, - functionName: "permit", - account: this.wallet.account, - args: [ - request.owner, - request.spender, - request.value, - request.deadline, - request.v, - request.r, - request.s, - ], - }); - return await this.wallet.writeContract(call as WriteContractParameters); - } - - /** - * method permit for contract WrappedIP with only encode - * - * @param request WrappedIpPermitRequest - * @return EncodedTxData - */ - public permitEncode(request: WrappedIpPermitRequest): EncodedTxData { - return { - to: this.address, - data: encodeFunctionData({ - abi: wrappedIpAbi, - functionName: "permit", - args: [ - request.owner, - request.spender, - request.value, - request.deadline, - request.v, - request.r, - request.s, - ], - }), - }; - } - /** * method transfer for contract WrappedIP * diff --git a/packages/core-sdk/src/client.ts b/packages/core-sdk/src/client.ts index 2dcca20d8..15708a8f6 100644 --- a/packages/core-sdk/src/client.ts +++ b/packages/core-sdk/src/client.ts @@ -170,7 +170,7 @@ export class StoryClient { */ public get ipAccount(): IPAccountClient { if (this._ipAccount === null) { - this._ipAccount = new IPAccountClient(this.rpcClient, this.wallet); + this._ipAccount = new IPAccountClient(this.rpcClient, this.wallet, this.chainId); } return this._ipAccount; diff --git a/packages/core-sdk/src/index.ts b/packages/core-sdk/src/index.ts index d98b24615..f314622da 100644 --- a/packages/core-sdk/src/index.ts +++ b/packages/core-sdk/src/index.ts @@ -9,9 +9,12 @@ export { NftClient } from "./resources/nftClient"; export { IPAccountClient } from "./resources/ipAccount"; export { RoyaltyClient } from "./resources/royalty"; export { GroupClient } from "./resources/group"; +export { WipClient } from "./resources/wip"; export type { StoryConfig, SupportedChainIds } from "./types/config"; +export type { LicensingConfig } from "./types/common"; + export type { RegisterRequest, RegisterIpResponse, @@ -27,16 +30,6 @@ export type { RegisterIpAndAttachPilTermsResponse, MintAndRegisterIpAndMakeDerivativeRequest, MintAndRegisterIpAndMakeDerivativeResponse, - GenerateCreatorMetadataParam, - IpCreator, - GenerateIpMetadataParam, - IpMetadata, - IpRelationship, - IpAttribute, - IpCreatorSocial, - IpMedia, - IPRobotTerms, - StoryProtocolApp, MintAndRegisterIpRequest, RegisterPilTermsAndAttachRequest, RegisterPilTermsAndAttachResponse, @@ -59,8 +52,11 @@ export type { MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensResponse, MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest, MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensResponse, + LicenseTermsData, } from "./types/resources/ipAsset"; +export * from "./types/resources/ipMetadata"; + export type { RegisterNonComSocialRemixingPILRequest, RegisterCommercialUsePILRequest, @@ -83,18 +79,12 @@ export { PIL_TYPE } from "./types/resources/license"; export type { PayRoyaltyOnBehalfRequest, PayRoyaltyOnBehalfResponse, - SnapshotRequest, - SnapshotResponse, ClaimableRevenueRequest, ClaimableRevenueResponse, - SnapshotAndClaimBySnapshotBatchRequest, - SnapshotAndClaimBySnapshotBatchResponse, - SnapshotAndClaimByTokenBatchRequest, - SnapshotAndClaimByTokenBatchResponse, - TransferToVaultAndSnapshotAndClaimBySnapshotBatchRequest, - TransferToVaultAndSnapshotAndClaimBySnapshotBatchResponse, - TransferToVaultAndSnapshotAndClaimByTokenBatchRequest, - TransferToVaultAndSnapshotAndClaimByTokenBatchResponse, + ClaimAllRevenueRequest, + ClaimAllRevenueResponse, + BatchClaimAllRevenueRequest, + BatchClaimAllRevenueResponse, } from "./types/resources/royalty"; export type { @@ -154,8 +144,18 @@ export type { LicensingModulePredictMintingLicenseFeeResponse, } from "./abi/generated"; +export { royaltyPolicyLapAddress, royaltyPolicyLrpAddress } from "./abi/generated"; + +export type { + DepositRequest, + WithdrawRequest, + ApproveRequest, + TransferRequest, + TransferFromRequest, +} from "./types/resources/wip"; + export { getPermissionSignature, getSignature } from "./utils/sign"; export { convertCIDtoHashIPFS, convertHashIPFStoCID } from "./utils/ipfs"; -export type { TxOptions } from "./types/options"; +export type { TxOptions, TransactionResponse, WipOptions, ERC20Options } from "./types/options"; diff --git a/packages/core-sdk/src/resources/dispute.ts b/packages/core-sdk/src/resources/dispute.ts index 9a9f804b6..510df0f7d 100644 --- a/packages/core-sdk/src/resources/dispute.ts +++ b/packages/core-sdk/src/resources/dispute.ts @@ -1,52 +1,54 @@ -import { PublicClient, encodeAbiParameters, stringToHex } from "viem"; +import { Hex, PublicClient, encodeAbiParameters, stringToHex } from "viem"; import { handleError } from "../utils/errors"; import { CancelDisputeRequest, CancelDisputeResponse, + DisputeAssertionRequest, RaiseDisputeRequest, RaiseDisputeResponse, ResolveDisputeRequest, ResolveDisputeResponse, + TagIfRelatedIpInfringedRequest, } from "../types/resources/dispute"; import { - ArbitrationPolicyUmaReadOnlyClient, + ArbitrationPolicyUmaClient, DisputeModuleClient, + DisputeModuleTagIfRelatedIpInfringedRequest, + IpAccountImplClient, + Multicall3Client, SimpleWalletClient, wrappedIpAddress, } from "../abi/generated"; -import { chain, getAddress } from "../utils/utils"; +import { chain, validateAddress } from "../utils/utils"; import { convertCIDtoHashIPFS } from "../utils/ipfs"; import { ChainIds } from "../types/config"; +import { handleTxOptions } from "../utils/txOptions"; +import { TransactionResponse } from "../types/options"; export class DisputeClient { public disputeModuleClient: DisputeModuleClient; - public arbitrationPolicyUmaReadOnlyClient: ArbitrationPolicyUmaReadOnlyClient; + public arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient; + public multicall3Client: Multicall3Client; private readonly rpcClient: PublicClient; private readonly chainId: ChainIds; + private readonly wallet: SimpleWalletClient; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: ChainIds) { this.rpcClient = rpcClient; this.disputeModuleClient = new DisputeModuleClient(rpcClient, wallet); - this.arbitrationPolicyUmaReadOnlyClient = new ArbitrationPolicyUmaReadOnlyClient(rpcClient); + this.arbitrationPolicyUmaClient = new ArbitrationPolicyUmaClient(rpcClient, wallet); + this.multicall3Client = new Multicall3Client(rpcClient, wallet); this.chainId = chainId; + this.wallet = wallet; } /** - * Raises a dispute on a given ipId - * @param request - The request object containing necessary data to raise a dispute. - * @param request.targetIpId The IP ID that is the target of the dispute. - * @param request.targetTag The target tag of the dispute. - * @param request.cid CID (Content Identifier) is a unique identifier in IPFS, including CID v0 (base58) and CID v1 (base32). - * @param request.liveness The liveness time. - * @param request.bond The bond size. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a RaiseDisputeResponse containing the transaction hash. - * @throws `NotRegisteredIpId` if targetIpId is not registered in the IPA Registry. - * @throws `NotWhitelistedDisputeTag` if targetTag is not whitelisted. - * @throws `ZeroLinkToDisputeEvidence` if linkToDisputeEvidence is empty - * @calls raiseDispute(address _targetIpId, string memory _linkToDisputeEvidence, bytes32 _targetTag, bytes calldata _data) external nonReentrant returns (uint256) { - * @emits DisputeRaised (disputeId_, targetIpId, msg.sender, arbitrationPolicy, linkToDisputeEvidence, targetTag, calldata); + * Raises a dispute on a given ipId. + * + * Submits a {@link DisputeRaised} event. + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/dispute/IDisputeModule.sol#L64 | IDisputeModule.sol} + * for a list of on-chain events emitted when a dispute is raised. */ public async raiseDispute(request: RaiseDisputeRequest): Promise { try { @@ -54,8 +56,8 @@ export class DisputeClient { const bonds = BigInt(request.bond); const tokenAddress = wrappedIpAddress[chain[this.chainId]]; const [minLiveness, maxLiveness] = await Promise.all([ - this.arbitrationPolicyUmaReadOnlyClient.minLiveness(), - this.arbitrationPolicyUmaReadOnlyClient.maxLiveness(), + this.arbitrationPolicyUmaClient.minLiveness(), + this.arbitrationPolicyUmaClient.maxLiveness(), ]); const tag = stringToHex(request.targetTag, { size: 32 }); @@ -63,7 +65,7 @@ export class DisputeClient { throw new Error(`Liveness must be between ${minLiveness} and ${maxLiveness}.`); } - const maxBonds = await this.arbitrationPolicyUmaReadOnlyClient.maxBonds({ + const maxBonds = await this.arbitrationPolicyUmaClient.maxBonds({ token: tokenAddress, }); if (bonds > maxBonds) { @@ -84,7 +86,7 @@ export class DisputeClient { throw new Error(`The target tag ${request.targetTag} is not whitelisted.`); } const req = { - targetIpId: getAddress(request.targetIpId, "request.targetIpId"), + targetIpId: validateAddress(request.targetIpId), targetTag: tag, disputeEvidenceHash: convertCIDtoHashIPFS(request.cid), data, @@ -185,4 +187,96 @@ export class DisputeClient { handleError(error, "Failed to resolve dispute"); } } + /** + * Tags a derivative if a parent has been tagged with an infringement tag + * or a group ip if a group member has been tagged with an infringement tag. + * + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/dispute/IDisputeModule.sol#L93 | IDisputeModule.sol} + * for a list of on-chain events emitted when a derivative is tagged on an infringement. + */ + public async tagIfRelatedIpInfringed( + request: TagIfRelatedIpInfringedRequest, + ): Promise { + try { + const objects: DisputeModuleTagIfRelatedIpInfringedRequest[] = request.infringementTags.map( + (arg) => ({ + ipIdToTag: validateAddress(arg.ipId), + infringerDisputeId: BigInt(arg.disputeId), + }), + ); + let txHashes: Hex[] = []; + if ( + request.options?.useMulticallWhenPossible !== false && + request.infringementTags.length > 1 + ) { + const calls = objects.map((object) => ({ + target: this.disputeModuleClient.address, + allowFailure: false, + callData: this.disputeModuleClient.tagIfRelatedIpInfringedEncode(object).data, + })); + const txHash = await this.multicall3Client.aggregate3({ calls }); + txHashes.push(txHash); + } else { + txHashes = await Promise.all( + objects.map((object) => this.disputeModuleClient.tagIfRelatedIpInfringed(object)), + ); + } + return await Promise.all( + txHashes.map((txHash) => + handleTxOptions({ + txHash, + txOptions: request.txOptions, + rpcClient: this.rpcClient, + }), + ), + ); + } catch (error) { + handleError(error, "Failed to tag related ip infringed"); + } + } + + /** + * Counters a dispute that was raised by another party on an IP using counter evidence. + * + * This method can only be called by the IP's owner to counter a dispute by providing + * counter evidence. The counter evidence (e.g., documents, images) should be + * uploaded to IPFS, and its corresponding CID is converted to a hash for the request. + * + * If you only have a `disputeId`, call {@link disputeIdToAssertionId} to get the `assertionId` needed here. + */ + public async disputeAssertion(request: DisputeAssertionRequest): Promise { + try { + const ipAccount = new IpAccountImplClient( + this.rpcClient, + this.wallet, + validateAddress(request.ipId), + ); + const counterEvidenceHash = convertCIDtoHashIPFS(request.counterEvidenceCID); + const encodedData = this.arbitrationPolicyUmaClient.disputeAssertionEncode({ + assertionId: request.assertionId, + counterEvidenceHash, + }); + const txHash = await ipAccount.execute({ + to: encodedData.to, + value: 0n, + data: encodedData.data, + operation: 0, + }); + + return handleTxOptions({ + txHash, + txOptions: request.txOptions, + rpcClient: this.rpcClient, + }); + } catch (e) { + handleError(e, "Failed to dispute assertion"); + } + } + + public async disputeIdToAssertionId(disputeId: number | bigint): Promise { + const assertionId = await this.arbitrationPolicyUmaClient.disputeIdToAssertionId({ + disputeId: BigInt(disputeId), + }); + return assertionId; + } } diff --git a/packages/core-sdk/src/resources/group.ts b/packages/core-sdk/src/resources/group.ts index 7e64ed193..e1797edd2 100644 --- a/packages/core-sdk/src/resources/group.ts +++ b/packages/core-sdk/src/resources/group.ts @@ -27,7 +27,7 @@ import { getPermissionSignature, getDeadline } from "../utils/sign"; import { chain, getAddress } from "../utils/utils"; import { ChainIds } from "../types/config"; import { - InnerLicenseData, + ValidatedLicenseData, LicenseData, MintAndRegisterIpAndAttachLicenseAndAddToGroupRequest, MintAndRegisterIpAndAttachLicenseAndAddToGroupResponse, @@ -44,6 +44,7 @@ import { getFunctionSignature } from "../utils/getFunctionSignature"; import { validateLicenseConfig } from "../utils/validateLicenseConfig"; import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow"; import { getRevenueShare } from "../utils/licenseTermsHelper"; +import { RevShareType } from "../types/common"; export class GroupClient { public groupingWorkflowsClient: GroupingWorkflowsClient; @@ -107,36 +108,9 @@ export class GroupClient { handleError(error, "Failed to register group"); } } - /** Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach license terms to the registered IP, and add it to a group IP. - * @param request - The request object containing necessary data to mint and register Ip and attach license and add to group. - * @param request.nftContract The address of the NFT collection. - * @param request.groupId The ID of the group IP to add the newly registered IP. - * @param request.maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. - * @param {Array} request.licenseData licenseData The data of the license and its configuration to be attached to the new group IP. - * @param request.licenseData.licenseTermsId The ID of the registered license terms that will be attached to the new group IP. - * @param request.licenseData.licenseTemplate [Optional] The address of the license template to be attached to the new group IP, default value is Programmable IP License. - * @param request.licenseData.licensingConfig The licensing configuration for the IP. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param request.allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. - * @param request.recipient [Optional] The address of the recipient of the minted NFT,default value is your wallet address. - * . @param request.deadline [Optional] The deadline for the signature in seconds, default value is 1000s. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes IP ID, token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, resolverAddr, metadataProviderAddress, metadata) + /** Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach license terms to the registered IP, and add it to a group IP. + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * for a list of on-chain events emitted when an IP is minted and registered, license terms are attached to an IP, and it is added to a group. */ public async mintAndRegisterIpAndAttachLicenseAndAddToGroup( request: MintAndRegisterIpAndAttachLicenseAndAddToGroupRequest, @@ -171,10 +145,13 @@ export class GroupClient { }); const object: GroupingWorkflowsMintAndRegisterIpAndAttachLicenseAndAddToGroupRequest = { ...request, + allowDuplicates: request.allowDuplicates || true, spgNftContract: getAddress(spgNftContract, "request.spgNftContract"), recipient: (recipient && getAddress(recipient, "request.recipient")) || this.wallet.account!.address, - maxAllowedRewardShare: BigInt(getRevenueShare(request.maxAllowedRewardShare)), + maxAllowedRewardShare: BigInt( + getRevenueShare(request.maxAllowedRewardShare, RevShareType.MAX_ALLOWED_REWARD_SHARE), + ), licensesData: this.getLicenseData(request.licenseData), ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), sigAddToGroup: { @@ -209,34 +186,8 @@ export class GroupClient { } /** Register an NFT as IP with metadata, attach license terms to the registered IP, and add it to a group IP. - * @param request - The request object containing necessary data to register ip and attach license and add to group. - * @param request.spgNftContract The address of the NFT collection. - * @param request.tokenId The ID of the NFT. - * @param request.maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. - * @param request.groupId The ID of the group IP to add the newly registered IP. - * @param {Array} request.licenseData licenseData The data of the license and its configuration to be attached to the new group IP. - * @param request.licenseData.licenseTermsId The ID of the registered license terms that will be attached to the new group IP. - * @param request.licenseData.licenseTemplate [Optional] The address of the license template to be attached to the new group IP, default value is Programmable IP License. - * @param request.licenseData.licensingConfig The licensing configuration for the IP. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * . @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes IP ID, token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, resolverAddr, metadataProviderAddress, metadata) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * for a list of on-chain events emitted when an IP is registered, license terms are attached to an IP, and it is added to a group. */ public async registerIpAndAttachLicenseAndAddToGroup( request: RegisterIpAndAttachLicenseAndAddToGroupRequest, @@ -310,7 +261,9 @@ export class GroupClient { licensesData: this.getLicenseData(request.licenseData), ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), tokenId: BigInt(request.tokenId), - maxAllowedRewardShare: BigInt(getRevenueShare(request.maxAllowedRewardShare)), + maxAllowedRewardShare: BigInt( + getRevenueShare(request.maxAllowedRewardShare, RevShareType.MAX_ALLOWED_REWARD_SHARE), + ), sigAddToGroup: { signer: getAddress(this.wallet.account!.address, "wallet.account.address"), deadline: calculatedDeadline, @@ -345,25 +298,8 @@ export class GroupClient { } } /** Register a group IP with a group reward pool and attach license terms to the group IP. - * @param request - The request object containing necessary data to register group and attach license. - * @param request.groupPool The address specifying how royalty will be split amongst the pool of IPs in the group. - * @param {Object} request.licenseData licenseData The data of the license and its configuration to be attached to the new group IP. - * @param request.licenseData.licenseTermsId The ID of the registered license terms that will be attached to the new group IP. - * @param request.licenseData.licenseTemplate [Optional] The address of the license template to be attached to the new group IP, default value is Programmable IP License. - * @param request.licenseData.licensingConfig The licensing configuration for the IP. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param request.txOptions [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes group id. - * @emits PGroupRegistered (groupId, groupPool); + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/grouping/IGroupingModule.sol#L14 | IGroupingModule} + * for a list of on-chain events emitted when a group IP is registered, license terms are attached to a group IP . */ public async registerGroupAndAttachLicense( request: RegisterGroupAndAttachLicenseRequest, @@ -394,27 +330,8 @@ export class GroupClient { } } /** Register a group IP with a group reward pool, attach license terms to the group IP, and add individual IPs to the group IP. - * @param request - The request object containing necessary data to register group and attach license and add ips. - * @param request.ipIds The IP IDs of the IPs to be added to the group. - * @param request.groupPool The address specifying how royalty will be split amongst the pool of IPs in the group. - * @param request.maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. - * @param {Object} request.licenseData licenseData The data of the license and its configuration to be attached to the new group IP. - * @param request.licenseData.licenseTermsId The ID of the registered license terms that will be attached to the new group IP. - * @param request.licenseData.licenseTemplate [Optional] The address of the license template to be attached to the new group IP, default value is Programmable IP License. - * @param request.licenseData.licensingConfig The licensing configuration for the IP. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param request.txOptions [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes group id. - * @emits PGroupRegistered (groupId, groupPool); + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/grouping/IGroupingModule.sol#L14 | IGroupingModule} + * for a list of on-chain events emitted when a group IP is registered, license terms are attached to a group IP, and individual IPs are added to a group. */ public async registerGroupAndAttachLicenseAndAddIps( request: RegisterGroupAndAttachLicenseAndAddIpsRequest, @@ -472,7 +389,7 @@ export class GroupClient { } } - private getLicenseData(licenseData: LicenseData[] | LicenseData): InnerLicenseData[] { + private getLicenseData(licenseData: LicenseData[] | LicenseData): ValidatedLicenseData[] { const isArray = Array.isArray(licenseData); if ((isArray && licenseData.length === 0) || !licenseData) { throw new Error("License data is required."); diff --git a/packages/core-sdk/src/resources/ipAccount.ts b/packages/core-sdk/src/resources/ipAccount.ts index 0f3910bfc..c4b29fc7d 100644 --- a/packages/core-sdk/src/resources/ipAccount.ts +++ b/packages/core-sdk/src/resources/ipAccount.ts @@ -1,4 +1,4 @@ -import { Address, PublicClient } from "viem"; +import { Address, Hex, encodeFunctionData, PublicClient } from "viem"; import { IPAccountExecuteRequest, @@ -6,19 +6,27 @@ import { IPAccountExecuteWithSigRequest, IPAccountExecuteWithSigResponse, IpAccountStateResponse, + SetIpMetadataRequest, TokenResponse, } from "../types/resources/ipAccount"; import { handleError } from "../utils/errors"; -import { IpAccountImplClient, SimpleWalletClient } from "../abi/generated"; -import { getAddress } from "../utils/utils"; +import { + coreMetadataModuleAbi, + coreMetadataModuleAddress, + IpAccountImplClient, + SimpleWalletClient, +} from "../abi/generated"; +import { getAddress, validateAddress } from "../utils/utils"; +import { ChainIds } from "../types/config"; export class IPAccountClient { private readonly wallet: SimpleWalletClient; private readonly rpcClient: PublicClient; - - constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { + private readonly chainId: ChainIds; + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: ChainIds) { this.wallet = wallet; this.rpcClient = rpcClient; + this.chainId = chainId; } /** Executes a transaction from the IP Account. @@ -150,4 +158,34 @@ export class IPAccountClient { handleError(error, "Failed to get the token"); } } + /** + * Sets the metadataURI for an IP asset. + */ + public async setIpMetadata({ + ipId, + metadataURI, + metadataHash, + txOptions, + }: SetIpMetadataRequest): Promise { + try { + const data = encodeFunctionData({ + abi: coreMetadataModuleAbi, + functionName: "setMetadataURI", + args: [validateAddress(ipId), metadataURI, metadataHash], + }); + const { txHash } = await this.execute({ + ipId: ipId, + to: coreMetadataModuleAddress[this.chainId], + data: data, + value: 0, + txOptions: { + ...txOptions, + encodedTxDataOnly: false, + }, + }); + return txHash!; + } catch (error) { + handleError(error, "Failed to set the IP metadata"); + } + } } diff --git a/packages/core-sdk/src/resources/ipAsset.ts b/packages/core-sdk/src/resources/ipAsset.ts index ba2980724..494cc4e41 100644 --- a/packages/core-sdk/src/resources/ipAsset.ts +++ b/packages/core-sdk/src/resources/ipAsset.ts @@ -23,10 +23,6 @@ import { BatchRegisterResponse, MintAndRegisterIpAssetWithPilTermsRequest, MintAndRegisterIpAssetWithPilTermsResponse, - GenerateCreatorMetadataParam, - GenerateIpMetadataParam, - IpCreator, - IpMetadata, MintAndRegisterIpAndMakeDerivativeRequest, MintAndRegisterIpAndMakeDerivativeWithLicenseTokensRequest, MintAndRegisterIpRequest, @@ -59,7 +55,9 @@ import { InternalDerivativeData, LicenseTermsData, DerivativeData, - CommonRegistrationHandlerParams, + CommonRegistrationTxResponse, + CommonRegistrationParams, + ValidatedLicenseTermsData, } from "../types/resources/ipAsset"; import { AccessControllerClient, @@ -100,23 +98,20 @@ import { import { getRevenueShare, validateLicenseTerms } from "../utils/licenseTermsHelper"; import { getDeadline, getPermissionSignature, getSignature } from "../utils/sign"; import { AccessPermission } from "../types/resources/permission"; -import { - InnerLicensingConfig, - LicenseTerms, - RegisterPILTermsRequest, -} from "../types/resources/license"; +import { LicenseTerms, RegisterPILTermsRequest } from "../types/resources/license"; import { MAX_ROYALTY_TOKEN, royaltySharesTotalSupply } from "../constants/common"; import { getFunctionSignature } from "../utils/getFunctionSignature"; -import { LicensingConfig } from "../types/common"; +import { LicensingConfig, RevShareType } from "../types/common"; import { validateLicenseConfig } from "../utils/validateLicenseConfig"; import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow"; import { calculateLicenseWipMintFee, calculateSPGWipMintFee, - contractCallWithWipFees, -} from "../utils/wipFeeUtils"; -import { WipSpender } from "../types/utils/wip"; + contractCallWithFees, +} from "../utils/feeUtils"; +import { Erc20Spender } from "../types/utils/wip"; import { ChainIds } from "../types/config"; +import { IpCreator, IpMetadata } from "../types/resources/ipMetadata"; export class IPAssetClient { public licensingModuleClient: LicensingModuleClient; @@ -165,111 +160,12 @@ export class IPAssetClient { this.walletAddress = this.wallet.account!.address; } - /** - * Create a new `IpCreator` object with the specified details. - * @param params - The parameters required to create the `IpCreator` object. - * @param params.name The name of the creator. - * @param params.address The wallet address of the creator. - * @param params.description [Optional] A description of the creator. - * @param params.image [Optional] The URL or path to an image representing the creator. - * @param {Array} params.socialMedia [Optional] An array of social media profiles associated with the creator. - * @param params.socialMedia[].platform The name of the social media platform. - * @param params.socialMedia[].url The URL to the creator's profile on the platform. - * @param params.contributionPercent The percentage of contribution by the creator, must add up to 100. - * @param params.role [Optional] The role of the creator in relation to the IP. - * @returns An `IpCreator` object containing the provided details. - */ - public generateCreatorMetadata(param: GenerateCreatorMetadataParam): IpCreator { - const { - name, - address, - description = "", - image = "", - socialMedia = [], - contributionPercent, - role = "", - } = param; - return { - name, - address, - description, - image, - socialMedia, - contributionPercent, - role, - }; + public generateCreatorMetadata(creator: IpCreator): IpCreator { + return creator; } - /** - * Create a new `IpMetadata` object with the specified details. - * @param params - The parameters required to create the `IpMetadata` object. - * @param params.title [Optional] The title of the IP. - * @param params.description [Optional] A description of the IP. - * @param params.ipType [Optional] The type of the IP asset (e.g., "character", "chapter"). - * @param {Array} params.relationships [Optional] An array of relationships between this IP and its parent IPs. - * @param params.relationships[].ipId The ID of the parent IP. - * @param params.relationships[].type The type of relationship (e.g., "APPEARS_IN"). - * @param params.createdAt [Optional] The creation date and time of the IP in ISO 8601 format. - * @param params.watermarkImg [Optional] The URL or path to an image used as a watermark for the IP. - * @param {Array} params.creators [Optional] An array of creators associated with the IP. - * @param params.creators[].name The name of the creator. - * @param params.creators[].address The address of the creator. - * @param params.creators[].description [Optional] A description of the creator. - * @param params.creators[].image [Optional] The URL or path to an image representing the creator. - * @param params.creators[].socialMedia [Optional] An array of social media profiles for the creator. - * @param params.creators[].socialMedia[].platform The social media platform name. - * @param params.creators[].socialMedia[].url The URL to the creator's profile. - * @param params.creators[].role [Optional] The role of the creator in relation to the IP. - * @param params.creators[].contributionPercent The percentage of contribution by the creator. - * @param {Array} params.media [Optional] An array of media related to the IP. - * @param params.media[].name The name of the media. - * @param params.media[].url The URL to the media. - * @param params.media[].mimeType The MIME type of the media. - * @param {Array} params.attributes [Optional] An array of key-value pairs providing additional metadata. - * @param params.attributes[].key The key for the attribute. - * @param params.attributes[].value The value for the attribute, can be a string or number. - * @param {Object} params.app [Optional] Information about the application associated with the IP. - * @param params.app.id The ID of the application. - * @param params.app.name The name of the application. - * @param params.app.website The website URL of the application. - * @param {Array} params.tags [Optional] An array of tags associated with the IP. - * @param {Object} params.robotTerms [Optional] Robot terms for the IP, specifying access rules. - * @param params.robotTerms.userAgent The user agent for which the rules apply. - * @param params.robotTerms.allow The rules allowing access. - * @param params.additionalProperties [Optional] Any additional key-value pairs to include in the metadata. - * @returns An `IpMetadata` object containing the provided details and any additional properties. - */ - public generateIpMetadata(param: GenerateIpMetadataParam): IpMetadata { - const { - title = "", - description = "", - ipType = "", - relationships = [], - createdAt = "", - watermarkImg = "", - creators = [], - media = [], - attributes = [], - app, - tags = [], - robotTerms, - ...additionalProperties - } = param; - return { - title, - description, - ipType, - relationships, - createdAt, - watermarkImg, - creators, - media, - attributes, - app, - tags, - robotTerms, - ...additionalProperties, - }; + public generateIpMetadata(metadata: IpMetadata): IpMetadata { + return metadata; } /** @@ -458,16 +354,6 @@ export class IPAssetClient { * The license terms must be attached to the parent IP before calling this function. * 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 {Array} request.parentIpIds The parent IP IDs. - * @param {Array} request.licenseTermsIds The IDs of the license terms that the parent IP supports. - * @param request.maxMintingFee The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. - * @param request.maxRts The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). - * @param request.maxRevenueShare The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100,000,000 (where 100,000,000 represents 100%). - * @param request.licenseTemplate [Optional] The license template address, default value is Programmable IP License. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data. */ public async registerDerivative( request: RegisterDerivativeRequest, @@ -482,19 +368,25 @@ export class IPAssetClient { childIpId: request.childIpId, ...derivativeData, }; + const encodedTxData = this.licensingModuleClient.registerDerivativeEncode(object); if (request.txOptions?.encodedTxDataOnly) { - return { encodedTxData: this.licensingModuleClient.registerDerivativeEncode(object) }; + return { encodedTxData }; } else { - const txHash = await this.licensingModuleClient.registerDerivative(object); - if (request.txOptions?.waitForTransaction) { - await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - return { txHash }; - } else { - return { txHash }; - } + const contractCall = () => { + return this.licensingModuleClient.registerDerivative(object); + }; + return this.handleRegistrationWithFees({ + sender: this.walletAddress, + derivData: object, + contractCall, + txOptions: request.txOptions, + encodedTxs: [encodedTxData], + spgSpenderAddress: this.royaltyTokenDistributionWorkflowsClient.address, + wipOptions: { + ...request.wipOptions, + useMulticallWhenPossible: false, + }, + }); } } catch (error) { handleError(error, "Failed to register derivative"); @@ -553,9 +445,9 @@ export class IPAssetClient { arg.licenseTermsIds.map((id) => BigInt(id)), arg.licenseTemplate || this.licenseTemplateClient.address, zeroAddress, - BigInt(arg.maxMintingFee), - Number(arg.maxRts), - getRevenueShare(arg.maxRevenueShare), + BigInt(arg.maxMintingFee || 0), + Number(arg.maxRts || MAX_ROYALTY_TOKEN), + getRevenueShare(arg.maxRevenueShare || 100, RevShareType.MAX_REVENUE_SHARE), ], }); const { result: state } = await ipAccount.state(); @@ -652,48 +544,10 @@ export class IPAssetClient { } /** * Mint an NFT from a collection and register it as an IP. - * @param request - The request object that contains all data needed to mint and register ip. - * @param request.spgNftContract The address of the NFT collection. - * @param request.allowDuplicates Indicates whether the license terms can be attached to the same IP ID or not. - * @param {Array} request.licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. - * @param {Object} request.licenseTermsData.terms The PIL terms to be used for the licensing. - * @param request.licenseTermsData.terms.transferable Indicates whether the license is transferable or not. - * @param request.licenseTermsData.terms.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.licenseTermsData.terms.mintingFee The fee to be paid when minting a license. - * @param request.licenseTermsData.terms.expiration The expiration period of the license. - * @param request.licenseTermsData.terms.commercialUse Indicates whether the work can be used commercially or not, Commercial use is required to deploy a royalty vault. - * @param request.licenseTermsData.terms.commercialAttribution Whether attribution is required when reproducing the work commercially or not. - * @param request.licenseTermsData.terms.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. - * @param request.licenseTermsData.terms.commercializerCheckerData The data to be passed to the commercializer checker contract. - * @param request.licenseTermsData.terms.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.licenseTermsData.terms.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. - * @param request.licenseTermsData.terms.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. - * @param request.licenseTermsData.terms.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. - * @param request.licenseTermsData.terms.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.licenseTermsData.terms.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. - * @param request.licenseTermsData.terms.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. - * @param request.licenseTermsData.terms.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.licenseTermsData.terms.uri The URI of the license terms, which can be used to fetch the offchain license terms. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.recipient [Optional] The address of the recipient of the minted NFT,default value is your wallet address. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, including IP ID, Token ID, License Terms Ids. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) - * @emits LicenseTermsAttached (caller, ipId, licenseTemplate, licenseTermsId) + * it emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate). + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/licensing/ILicensingModule.sol#L19 | ILicensingModule} + * for a list of on-chain events emitted when an IP is minted and registered, and license terms are attached to an IP. */ public async mintAndRegisterIpAssetWithPilTerms( request: MintAndRegisterIpAssetWithPilTermsRequest, @@ -708,7 +562,7 @@ export class IPAssetClient { (request.recipient && getAddress(request.recipient, "request.recipient")) || this.wallet.account!.address, licenseTermsData, - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), }; @@ -720,7 +574,7 @@ export class IPAssetClient { const contractCall = () => { return this.licenseAttachmentWorkflowsClient.mintAndRegisterIpAndAttachPilTerms(object); }; - const rsp = await this.commonRegistrationHandler({ + const rsp = await this.handleRegistrationWithFees({ wipOptions: request.wipOptions, sender: this.walletAddress, spgNftContract: object.spgNftContract, @@ -768,13 +622,13 @@ export class IPAssetClient { * @param {Object} request.args.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. * @param request.args.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. * @param request.args.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.args.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none + * @param request.args.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or zero address if none * @param request.args.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. * @param request.args.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. * @param request.args.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.args.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. + * @param request.args.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group's reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.args.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. + * @param request.args.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or zero address if the IP does not want to be added to any group. * @param {Object} request.args.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. * @param request.args.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. * @param request.args.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. @@ -839,48 +693,9 @@ export class IPAssetClient { } } /** - * Register a given NFT as an IP and attach Programmable IP License Terms.R. - * @param request - The request object that contains all data needed to mint and register ip. - * @param request.nftContract The address of the NFT collection. - * @param request.tokenId The ID of the NFT. - * @param {Array} request.licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. - * @param {Object} request.licenseTermsData.terms The PIL terms to be used for the licensing. - * @param request.licenseTermsData.terms.transferable Indicates whether the license is transferable or not. - * @param request.licenseTermsData.terms.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.licenseTermsData.terms.mintingFee The fee to be paid when minting a license. - * @param request.licenseTermsData.terms.expiration The expiration period of the license. - * @param request.licenseTermsData.terms.commercialUse Indicates whether the work can be used commercially or not, Commercial use is required to deploy a royalty vault. - * @param request.licenseTermsData.terms.commercialAttribution Whether attribution is required when reproducing the work commercially or not. - * @param request.licenseTermsData.terms.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. - * @param request.licenseTermsData.terms.commercializerCheckerData The data to be passed to the commercializer checker contract. - * @param request.licenseTermsData.terms.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.licenseTermsData.terms.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. - * @param request.licenseTermsData.terms.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. - * @param request.licenseTermsData.terms.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. - * @param request.licenseTermsData.terms.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.licenseTermsData.terms.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. - * @param request.licenseTermsData.terms.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. - * @param request.licenseTermsData.terms.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.licenseTermsData.terms.uri The URI of the license terms, which can be used to fetch the offchain license terms. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, if waitForTransaction is true, including IP ID, token ID and License terms IDs. - * @emits LicenseTermsAttached (caller, ipId, licenseTemplate, licenseTermsId) + * Register a given NFT as an IP and attach Programmable IP License Terms. + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/licensing/ILicensingModule.sol#L19 | ILicensingModule} + * for a list of on-chain events emitted when an ip is registered and license terms are attached to it. */ public async registerIpAndAttachPilTerms( request: RegisterIpAndAttachPilTermsRequest, @@ -974,22 +789,8 @@ export class IPAssetClient { } /** * Register the given NFT as a derivative IP with metadata without using license tokens. - * @param request - The request object that contains all data needed to register derivative IP. - * @param request.nftContract The address of the NFT collection. - * @param request.tokenId The ID of the NFT. - * @param {Object} request.derivData The derivative data to be used for registerDerivative. - * @param {Array} request.derivData.parentIpIds The IDs of the parent IPs to link the registered derivative IP. - * @param {Array} request.derivData.licenseTermsIds The IDs of the license terms to be used for the linking. - * @param request.derivData.licenseTemplate [Optional] The address of the license template to be used for the linking, default value is Programmable IP License. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, included IP ID, Token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * for a list of on-chain events emitted when a derivative IP is registered. */ public async registerDerivativeIp( request: RegisterIpAndMakeDerivativeRequest, @@ -1045,7 +846,7 @@ export class IPAssetClient { const contractCall = () => { return this.derivativeWorkflowsClient.registerIpAndMakeDerivative(object); }; - return this.commonRegistrationHandler({ + return this.handleRegistrationWithFees({ wipOptions: { ...request.wipOptions, useMulticallWhenPossible: false, @@ -1063,26 +864,8 @@ export class IPAssetClient { } /** * Mint an NFT from a collection and register it as a derivative IP without license tokens. - * @param request - The request object that contains all data needed to mint and register ip and make derivative. - * @param request.spgNftContract The address of the NFT collection. - * @param request.allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. - * @param {Object} request.derivData The derivative data to be used for registerDerivative. - * @param {Array} request.derivData.parentIpIds The IDs of the parent IPs to link the registered derivative IP. - * @param {Array} request.derivData.licenseTermsIds The IDs of the license terms to be used for the linking. - * @param request.derivData.licenseTemplate [Optional] The address of the license template to be used for the linking, default value is Programmable IP License. - * @param request.derivData.royaltyContext The address of the royalty context to be used for the linking, default value is zero address. - * @param request.derivData.maxMintingFee The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. - * @param request.derivData.maxRts The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). - * @param request.derivData.maxRevenueShare The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100,000,000 (where 100,000,000 represents 100%). - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param request.recipient [Optional] The address of the recipient of the minted NFT,default value is your wallet address. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes child IP ID and token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * for a list of on-chain events emitted when a derivative IP is minted and registered. */ public async mintAndRegisterIpAndMakeDerivative( request: MintAndRegisterIpAndMakeDerivativeRequest, @@ -1098,7 +881,7 @@ export class IPAssetClient { derivData, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), recipient, - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, spgNftContract, }; const encodedTxData = @@ -1109,7 +892,7 @@ export class IPAssetClient { const contractCall = () => { return this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivative(object); }; - return this.commonRegistrationHandler({ + return this.handleRegistrationWithFees({ wipOptions: request.wipOptions, sender: this.walletAddress, spgSpenderAddress: this.derivativeWorkflowsClient.address, @@ -1201,63 +984,34 @@ export class IPAssetClient { (request.recipient && getAddress(request.recipient, "request.recipient")) || this.wallet.account!.address, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, }; + const encodedTxData = this.registrationWorkflowsClient.mintAndRegisterIpEncode(object); if (request.txOptions?.encodedTxDataOnly) { - return { encodedTxData: this.registrationWorkflowsClient.mintAndRegisterIpEncode(object) }; - } else { - const txHash = await this.registrationWorkflowsClient.mintAndRegisterIp(object); - if (request.txOptions?.waitForTransaction) { - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const log = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0]; - return { txHash, ...log }; - } - return { txHash }; + return { encodedTxData }; } + const contractCall = () => { + return this.registrationWorkflowsClient.mintAndRegisterIp(object); + }; + return this.handleRegistrationWithFees({ + sender: this.walletAddress, + spgSpenderAddress: this.registrationWorkflowsClient.address, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + wipOptions: { + ...request.wipOptions, + useMulticallWhenPossible: false, + }, + }); } catch (error) { handleError(error, "Failed to mint and register IP"); } } /** * Register Programmable IP License Terms (if unregistered) and attach it to IP. - * @param request - The request object that contains all data needed to attach license terms. - * @param request.ipId The ID of the IP. - * @param {Array} request.licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. - * @param {Object} request.licenseTermsData.terms The PIL terms to be used for the licensing. - * @param request.licenseTermsData.terms.transferable Indicates whether the license is transferable or not. - * @param request.licenseTermsData.terms.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.licenseTermsData.terms.mintingFee The fee to be paid when minting a license. - * @param request.licenseTermsData.terms.expiration The expiration period of the license. - * @param request.licenseTermsData.terms.commercialUse Indicates whether the work can be used commercially or not, Commercial use is required to deploy a royalty vault. - * @param request.licenseTermsData.terms.commercialAttribution Whether attribution is required when reproducing the work commercially or not. - * @param request.licenseTermsData.terms.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. - * @param request.licenseTermsData.terms.commercializerCheckerData The data to be passed to the commercializer checker contract. - * @param request.licenseTermsData.terms.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.licenseTermsData.terms.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. - * @param request.licenseTermsData.terms.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. - * @param request.licenseTermsData.terms.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. - * @param request.licenseTermsData.terms.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.licenseTermsData.terms.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. - * @param request.licenseTermsData.terms.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. - * @param request.licenseTermsData.terms.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.licenseTermsData.terms.uri The URI of the license terms, which can be used to fetch the offchain license terms. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param request.deadline [Optional] The deadline for the signature in milliseconds, default is 1000s. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, returns an array containing the license terms ID. - * @emits LicenseTermsAttached (caller, ipId, licenseTemplate, licenseTermsId) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/licensing/ILicensingModule.sol#L19 | ILicensingModule} + * for a list of on-chain events emitted when a license terms is attached to an IP. */ public async registerPilTermsAndAttach( request: RegisterPilTermsAndAttachRequest, @@ -1369,7 +1123,7 @@ export class IPAssetClient { licenseTokenIds: licenseTokenIds, royaltyContext: zeroAddress, maxRts: Number(request.maxRts), - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, }; this.validateMaxRts(object.maxRts); @@ -1385,7 +1139,7 @@ export class IPAssetClient { object, ); }; - return this.commonRegistrationHandler({ + return this.handleRegistrationWithFees({ wipOptions: { ...request.wipOptions, // need to disable multicall to avoid needing to transfer the license @@ -1495,51 +1249,9 @@ export class IPAssetClient { /** * Register the given NFT and attach license terms and distribute royalty tokens. In order to successfully distribute royalty tokens, the first license terms attached to the IP must be * a commercial license. - * @param request - The request object that contains all data needed to register ip and attach license terms and distribute royalty tokens. - * @param request.nftContract The address of the NFT collection. - * @param request.tokenId The ID of the NFT. - * @param {Array} request.licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. - * @param {Object} request.licenseTermsData.terms The PIL terms to be used for the licensing. - * @param request.licenseTermsData.terms.transferable Indicates whether the license is transferable or not. - * @param request.licenseTermsData.terms.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.licenseTermsData.terms.mintingFee The fee to be paid when minting a license. - * @param request.licenseTermsData.terms.expiration The expiration period of the license. - * @param request.licenseTermsData.terms.commercialUse Indicates whether the work can be used commercially or not, Commercial use is required to deploy a royalty vault. - * @param request.licenseTermsData.terms.commercialAttribution Whether attribution is required when reproducing the work commercially or not. - * @param request.licenseTermsData.terms.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. - * @param request.licenseTermsData.terms.commercializerCheckerData The data to be passed to the commercializer checker contract. - * @param request.licenseTermsData.terms.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.licenseTermsData.terms.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. - * @param request.licenseTermsData.terms.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. - * @param request.licenseTermsData.terms.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. - * @param request.licenseTermsData.terms.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.licenseTermsData.terms.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. - * @param request.licenseTermsData.terms.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. - * @param request.licenseTermsData.terms.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.licenseTermsData.terms.uri The URI of the license terms, which can be used to fetch the offchain license terms. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param {Array} request.royaltyShares Authors of the IP and their shares of the royalty tokens. - * @param request.royaltyShares.recipient The address of the recipient. - * @param request.royaltyShares.percentage The percentage of the royalty share, 10 represents 10%. - * @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property, without encodedTxData option. - * @returns A Promise that resolves to a transaction hashes, IP ID, IP royalty vault and an array containing the license terms ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) - * @emits IpRoyaltyVaultDeployed (ipId, ipRoyaltyVault) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/royalty/IRoyaltyModule.sol#L88 | IRoyaltyModule} + * for a list of on-chain events emitted when an IP is registered, license terms are attached to an IP, and royalty tokens are distributed. */ public async registerIPAndAttachLicenseTermsAndDistributeRoyaltyTokens( request: RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest, @@ -1644,29 +1356,9 @@ export class IPAssetClient { /** * Register the given NFT as a derivative IP and attach license terms and distribute royalty tokens. In order to successfully distribute royalty tokens, the license terms attached to the IP must be * a commercial license. - * @param request - The request object that contains all data needed to register derivative IP and distribute royalty tokens. - * @param request.nftContract The address of the NFT collection. - * @param request.tokenId The ID of the NFT. - * @param {Object} request.derivData The derivative data to be used for registerDerivative. - * @param {Array} request.derivData.parentIpIds The IDs of the parent IPs to link the registered derivative IP. - * @param request.derivData.licenseTemplate [Optional] The address of the license template to be used for the linking, default value is Programmable IP License. - * @param {Array} request.derivData.licenseTermsIds The IDs of the license terms to be used for the linking. - * @param request.derivData.maxMintingFee The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. - * @param request.derivData.maxRts The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). - * @param request.derivData.maxRevenueShare The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100,000,000 (where 100,000,000 represents 100%). - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param {Array} request.royaltyShares Authors of the IP and their shares of the royalty tokens. - * @param request.royaltyShares.recipient The address of the recipient. - * @param request.royaltyShares.percentage The percentage of the royalty share, 10 represents 10%. - * @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property, without encodedTxData option. - * @returns A Promise that resolves to a transaction hashes, IP ID and IP royalty vault, token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) - * @emits IpRoyaltyVaultDeployed (ipId, ipRoyaltyVault) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/royalty/IRoyaltyModule.sol#L88| IRoyaltyModule} + * for a list of on-chain events emitted when a derivative IP is registered, license terms are attached to an IP, and royalty tokens are distributed. */ public async registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokens( request: RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest, @@ -1727,7 +1419,7 @@ export class IPAssetClient { object, ); }; - const { txHash, ipId, tokenId, receipt } = await this.commonRegistrationHandler({ + const { txHash, ipId, tokenId, receipt } = await this.handleRegistrationWithFees({ wipOptions: { ...request.wipOptions, useMulticallWhenPossible: false, @@ -1739,8 +1431,9 @@ export class IPAssetClient { contractCall, txOptions: { ...request.txOptions, waitForTransaction: true }, }); - if (tokenId === undefined || ipId === undefined) { - throw new Error("Failed to register derivative ip and deploy royalty vault"); + // Need to consider tokenId is 0n, so we can't check !tokenId. + if (tokenId === undefined || !ipId || !receipt) { + throw new Error("Failed to register derivative ip and deploy royalty vault."); } const { ipRoyaltyVault } = this.royaltyModuleEventClient .parseTxIpRoyaltyVaultDeployedEvent(receipt) @@ -1776,51 +1469,9 @@ export class IPAssetClient { /** * Mint an NFT and register the IP, attach PIL terms, and distribute royalty tokens. - * @param request - The request object that contains all data needed to mint an NFT and register the IP, attach PIL terms, and distribute royalty tokens. - * @param request.spgNftContract The address of the SPG NFT contract. - * @param request.allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. - * @param {Array} request.licenseTermsData The PIL terms and licensing configuration data to attach to the IP. - * @param {Object} request.licenseTermsData.terms The PIL terms to be attached. - * @param request.licenseTermsData.terms.transferable Indicates whether the license is transferable or not. - * @param request.licenseTermsData.terms.royaltyPolicy The address of the royalty policy contract which required to StoryProtocol in advance. - * @param request.licenseTermsData.terms.mintingFee The fee to be paid when minting a license. - * @param request.licenseTermsData.terms.expiration The expiration period of the license. - * @param request.licenseTermsData.terms.commercialUse Indicates whether the work can be used commercially or not, Commercial use is required to deploy a royalty vault. - * @param request.licenseTermsData.terms.commercialAttribution Whether attribution is required when reproducing the work commercially or not. - * @param request.licenseTermsData.terms.commercializerChecker Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced. - * @param request.licenseTermsData.terms.commercializerCheckerData The data to be passed to the commercializer checker contract. - * @param request.licenseTermsData.terms.commercialRevShare Percentage of revenue that must be shared with the licensor. - * @param request.licenseTermsData.terms.commercialRevCeiling The maximum revenue that can be generated from the commercial use of the work. - * @param request.licenseTermsData.terms.derivativesAllowed Indicates whether the licensee can create derivatives of his work or not. - * @param request.licenseTermsData.terms.derivativesAttribution Indicates whether attribution is required for derivatives of the work or not. - * @param request.licenseTermsData.terms.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.licenseTermsData.terms.derivativesReciprocal Indicates whether the licensee must license derivatives of the work under the same terms or not. - * @param request.licenseTermsData.terms.derivativeRevCeiling The maximum revenue that can be generated from the derivative use of the work. - * @param request.licenseTermsData.terms.currency The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol. - * @param request.licenseTermsData.terms.uri The URI of the license terms, which can be used to fetch the offchain license terms. - * @param {Object} request.licenseTermsData.licensingConfig The PIL terms and licensing configuration data to attach to the IP. - * @param request.licenseTermsData.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licenseTermsData.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licenseTermsData.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none - * @param request.licenseTermsData.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licenseTermsData.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licenseTermsData.licensingConfig.disabled Whether the licensing is disabled or not. - * @param request.licenseTermsData.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare,the IP cannot be added to the group. - * @param request.licenseTermsData.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or address(0) if the IP does not want to be added to any group. - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param {Array} request.royaltyShares Authors of the IP and their shares of the royalty tokens. - * @param request.royaltyShares.recipient The address of the recipient. - * @param request.royaltyShares.percentage The percentage of the royalty share, 10 represents 10%. - * @param request.recipient - [Optional] The address to receive the minted NFT,default value is your wallet address. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property, without encodedTxData option. - * @returns A Promise that resolves to a transaction hash, IP ID, IP royalty vault, Token ID, and an array containing the license terms ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) - * @emits IpRoyaltyVaultDeployed (ipId, ipRoyaltyVault) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/modules/royalty/IRoyaltyModule.sol#L88| IRoyaltyModule} + * for a list of on-chain events emitted when an IP is minted and registered, PIL terms are attached to an IP, and royalty tokens are distributed. */ public async mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens( request: MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensRequest, @@ -1839,7 +1490,7 @@ export class IPAssetClient { ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), licenseTermsData, royaltyShares, - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, }; const encodedTxData = this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensEncode( @@ -1850,7 +1501,7 @@ export class IPAssetClient { object, ); }; - const { txHash, ipId, tokenId, receipt } = await this.commonRegistrationHandler({ + const { txHash, ipId, tokenId, receipt } = await this.handleRegistrationWithFees({ wipOptions: request.wipOptions, sender: this.walletAddress, spgNftContract: object.spgNftContract, @@ -1881,28 +1532,8 @@ export class IPAssetClient { } /** * Mint an NFT and register the IP, make a derivative, and distribute royalty tokens. - * @param request - The request object that contains all data needed to mint an NFT and register the IP, make a derivative, and distribute royalty tokens. - * @param request.spgNftContract The address of the SPG NFT collection. - * @param request.derivData The derivative data to be used for registerDerivative. - * @param {Array} request.derivData.parentIpIds The IDs of the parent IPs to link the registered derivative IP. - * @param request.derivData.licenseTemplate [Optional] The address of the license template to be used for the linking, default value is Programmable IP License. - * @param {Array} request.derivData.licenseTermsIds The IDs of the license terms to be used for the linking. - * @param request.derivData.maxMintingFee The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. - * @param request.derivData.maxRts The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). - * @param request.derivData.maxRevenueShare The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100,000,000 (where 100,000,000 represents 100%). - * @param {Object} request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP. - * @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP. - * @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP. - * @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT. - * @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT. - * @param {Array} request.royaltyShares Authors of the IP and their shares of the royalty tokens. - * @param request.royaltyShares.recipient The address of the recipient. - * @param request.royaltyShares.percentage The percentage of the royalty share, 10 represents 10%. - * @param request.allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. - * @param request.recipient - [Optional] The address to receive the minted NFT,default value is your wallet address. - * @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property, without encodedTxData option.. - * @returns A Promise that resolves to a transaction hash, IP ID and token ID. - * @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate) + * @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/interfaces/registries/IIPAssetRegistry.sol#L17 | IIPAssetRegistry} + * for a list of on-chain events emitted when an IP is minted and registered, a derivative IP is made, and royalty tokens are distributed. */ public async mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( request: MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest, @@ -1920,7 +1551,7 @@ export class IPAssetClient { ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), derivData, royaltyShares: royaltyShares, - allowDuplicates: request.allowDuplicates, + allowDuplicates: request.allowDuplicates || true, }; const encodedTxData = @@ -1932,7 +1563,7 @@ export class IPAssetClient { object, ); }; - return this.commonRegistrationHandler({ + return this.handleRegistrationWithFees({ spgNftContract: object.spgNftContract, wipOptions: request.wipOptions, sender: this.walletAddress, @@ -2102,9 +1733,12 @@ export class IPAssetClient { getAddress(derivativeData.licenseTemplate, "derivativeData.licenseTemplate")) || this.licenseTemplateClient.address, royaltyContext: zeroAddress, - maxMintingFee: BigInt(derivativeData.maxMintingFee), - maxRts: Number(derivativeData.maxRts), - maxRevenueShare: getRevenueShare(derivativeData.maxRevenueShare), + maxMintingFee: BigInt(derivativeData.maxMintingFee || 0), + maxRts: Number(derivativeData.maxRts || MAX_ROYALTY_TOKEN), + maxRevenueShare: getRevenueShare( + derivativeData.maxRevenueShare || 100, + RevShareType.MAX_REVENUE_SHARE, + ), }; if (internalDerivativeData.parentIpIds.length === 0) { throw new Error("The parent IP IDs must be provided."); @@ -2159,10 +1793,10 @@ export class IPAssetClient { licenseTermsData: LicenseTermsData[], ): Promise<{ licenseTerms: LicenseTerms[]; - licenseTermsData: LicenseTermsData[]; + licenseTermsData: ValidatedLicenseTermsData[]; }> { const licenseTerms: LicenseTerms[] = []; - const processedLicenseTermsData: LicenseTermsData[] = []; + const processedLicenseTermsData: ValidatedLicenseTermsData[] = []; for (let i = 0; i < licenseTermsData.length; i++) { const licenseTerm = await validateLicenseTerms(licenseTermsData[i].terms, this.rpcClient); const licensingConfig = validateLicenseConfig(licenseTermsData[i].licensingConfig); @@ -2180,7 +1814,7 @@ export class IPAssetClient { return { licenseTerms, licenseTermsData: processedLicenseTermsData }; } - private async commonRegistrationHandler({ + private async handleRegistrationWithFees({ sender, derivData, spgNftContract, @@ -2189,9 +1823,9 @@ export class IPAssetClient { wipOptions, encodedTxs, contractCall, - }: CommonRegistrationHandlerParams) { + }: CommonRegistrationParams): Promise { let totalFees = 0n; - const wipSpenders: WipSpender[] = []; + const wipSpenders: Erc20Spender[] = []; // get spg minting fee if (spgNftContract) { @@ -2235,13 +1869,12 @@ export class IPAssetClient { ); } - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ totalFees, - wipOptions, - multicall3Client: this.multicall3Client, + options: { wipOptions }, + multicall3Address: this.multicall3Client.address, rpcClient: this.rpcClient, - wipClient: this.wipClient, - wipSpenders, + tokenSpenders: wipSpenders, contractCall, sender, wallet: this.wallet, @@ -2249,10 +1882,16 @@ export class IPAssetClient { encodedTxs, }); if (receipt) { - const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(receipt)[0]; - return { txHash, ipId, tokenId, receipt }; - } else { - return { txHash }; + const event = this.getIpIdAndTokenIdsFromEvent(receipt)?.[0]; + return { + txHash, + receipt, + ...(event && { + ipId: event.ipId ?? undefined, + tokenId: event.tokenId ?? undefined, + }), + }; } + return { txHash }; } } diff --git a/packages/core-sdk/src/resources/license.ts b/packages/core-sdk/src/resources/license.ts index ebf331c0f..96e388c10 100644 --- a/packages/core-sdk/src/resources/license.ts +++ b/packages/core-sdk/src/resources/license.ts @@ -43,10 +43,12 @@ import { getRevenueShare, validateLicenseTerms, } from "../utils/licenseTermsHelper"; -import { chain, getAddress } from "../utils/utils"; +import { chain, getAddress, validateAddress } from "../utils/utils"; import { ChainIds } from "../types/config"; -import { calculateLicenseWipMintFee, contractCallWithWipFees } from "../utils/wipFeeUtils"; -import { WipSpender } from "../types/utils/wip"; +import { calculateLicenseWipMintFee, contractCallWithFees } from "../utils/feeUtils"; +import { Erc20Spender } from "../types/utils/wip"; +import { validateLicenseConfig } from "../utils/validateLicenseConfig"; +import { RevShareType } from "../types/common"; export class LicenseClient { public licenseRegistryClient: LicenseRegistryEventClient; @@ -389,7 +391,7 @@ export class LicenseClient { receiver, royaltyContext: zeroAddress, maxMintingFee: BigInt(request.maxMintingFee), - maxRevenueShare: getRevenueShare(request.maxRevenueShare), + maxRevenueShare: getRevenueShare(request.maxRevenueShare, RevShareType.MAX_REVENUE_SHARE), } as const; if (req.maxMintingFee < 0) { throw new Error(`The maxMintingFee must be greater than 0.`); @@ -436,20 +438,19 @@ export class LicenseClient { amount: req.amount, }); - const wipSpenders: WipSpender[] = []; + const wipSpenders: Erc20Spender[] = []; if (licenseMintingFee > 0n) { wipSpenders.push({ address: royaltyModuleAddress[chain[this.chainId]], amount: licenseMintingFee, }); } - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ totalFees: licenseMintingFee, - wipOptions: request.wipOptions, - multicall3Client: this.multicall3Client, + options: { wipOptions: request.wipOptions }, + multicall3Address: this.multicall3Client.address, rpcClient: this.rpcClient, - wipClient: this.wipClient, - wipSpenders, + tokenSpenders: wipSpenders, contractCall: () => { return this.licensingModuleClient.mintLicenseTokens(req); }, @@ -539,23 +540,6 @@ export class LicenseClient { /** * Sets the licensing configuration for a specific license terms of an IP. If both licenseTemplate and licenseTermsId are not specified then the licensing config apply to all licenses of given IP. - * @param request - The request object that contains all data needed to set licensing config. - * @param request.ipId The address of the IP for which the configuration is being set. - * @param request.licenseTermsId The ID of the license terms within the license template. - * @param request.licenseTemplate The address of the license template used, If not specified, the configuration applies to all licenses. - * @param request.licensingConfig The licensing configuration for the license. - * @param request.licensingConfig.isSet Whether the configuration is set or not. - * @param request.licensingConfig.mintingFee The minting fee to be paid when minting license tokens. - * @param request.licensingConfig.hookData The data to be used by the licensing hook. - * @param request.licensingConfig.licensingHook The hook contract address for the licensing module, or address(0) if none. - * @param request.licensingConfig.commercialRevShare The commercial revenue share percentage. - * @param request.licensingConfig.disabled Whether the license is disabled or not. - * @param request.licensingConfig.expectMinimumGroupRewardShare The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. - * If the remaining reward share in the group is less than the minimumGroupRewardShare, the IP cannot be added to the group. - * @param request.licensingConfig.expectGroupRewardPool The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, - * or address(0) if the IP does not want to be added to any group. - * @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property. - * @returns A Promise that resolves to a transaction hash, and if encodedTxDataOnly is true, includes encoded transaction data, and if waitForTransaction is true, includes success. */ public async setLicensingConfig( request: SetLicensingConfigRequest, @@ -563,56 +547,36 @@ export class LicenseClient { try { const req: LicensingModuleSetLicensingConfigRequest = { ipId: request.ipId, - licenseTemplate: getAddress(request.licenseTemplate, "request.licenseTemplate"), + licenseTemplate: validateAddress(request.licenseTemplate), licenseTermsId: BigInt(request.licenseTermsId), - licensingConfig: { - isSet: request.licensingConfig.isSet, - mintingFee: BigInt(request.licensingConfig.mintingFee), - hookData: request.licensingConfig.hookData, - licensingHook: request.licensingConfig.licensingHook, - disabled: request.licensingConfig.disabled, - commercialRevShare: getRevenueShare(request.licensingConfig.commercialRevShare), - expectGroupRewardPool: getAddress( - request.licensingConfig.expectGroupRewardPool, - "request.licensingConfig.expectGroupRewardPool", - ), - expectMinimumGroupRewardShare: Number( - request.licensingConfig.expectMinimumGroupRewardShare, - ), - }, + licensingConfig: validateLicenseConfig(request.licensingConfig), }; - if (req.licensingConfig.mintingFee < 0) { - throw new Error("The minting fee must be greater than 0."); - } - if ( - request.licenseTemplate === zeroAddress && - request.licensingConfig.commercialRevShare !== 0 - ) { + if (req.licenseTemplate === zeroAddress && req.licensingConfig.commercialRevShare !== 0) { throw new Error( "The license template cannot be zero address if commercial revenue share is not zero.", ); } const isLicenseIpIdRegistered = await this.ipAssetRegistryClient.isRegistered({ - id: getAddress(request.ipId, "request.ipId"), + id: validateAddress(req.ipId), }); if (!isLicenseIpIdRegistered) { - throw new Error(`The licensor IP with id ${request.ipId} is not registered.`); + throw new Error(`The licensor IP with id ${req.ipId} is not registered.`); } const isExisted = await this.piLicenseTemplateReadOnlyClient.exists({ licenseTermsId: req.licenseTermsId, }); if (!isExisted) { - throw new Error(`License terms id ${request.licenseTermsId} do not exist.`); + throw new Error(`License terms id ${req.licenseTermsId} do not exist.`); } - if (request.licensingConfig.licensingHook !== zeroAddress) { + if (req.licensingConfig.licensingHook !== zeroAddress) { const isRegistered = await this.moduleRegistryReadOnlyClient.isRegistered({ - moduleAddress: request.licensingConfig.licensingHook, + moduleAddress: req.licensingConfig.licensingHook, }); if (!isRegistered) { throw new Error("The licensing hook is not registered."); } } - if (request.licenseTemplate === zeroAddress && request.licenseTermsId !== 0n) { + if (req.licenseTemplate === zeroAddress && req.licenseTermsId !== 0n) { throw new Error("The license template is zero address but license terms id is not zero."); } diff --git a/packages/core-sdk/src/resources/nftClient.ts b/packages/core-sdk/src/resources/nftClient.ts index 7f5cc3d2f..8a4772183 100644 --- a/packages/core-sdk/src/resources/nftClient.ts +++ b/packages/core-sdk/src/resources/nftClient.ts @@ -1,19 +1,21 @@ -import { PublicClient, isAddress, maxUint32, zeroAddress } from "viem"; +import { Address, PublicClient, isAddress, maxUint32, zeroAddress } from "viem"; import { RegistrationWorkflowsClient, RegistrationWorkflowsCreateCollectionRequest, SimpleWalletClient, + SpgnftImplReadOnlyClient, } from "../abi/generated"; import { CreateNFTCollectionRequest, CreateNFTCollectionResponse, } from "../types/resources/nftClient"; import { handleError } from "../utils/errors"; -import { getAddress } from "../utils/utils"; +import { getAddress, validateAddress } from "../utils/utils"; export class NftClient { public registrationWorkflowsClient: RegistrationWorkflowsClient; + private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; @@ -96,4 +98,25 @@ export class NftClient { handleError(error, "Failed to create a SPG NFT collection"); } } + /** + * Returns the current mint token of the collection. + */ + public async getMintFeeToken(spgNftContract: Address): Promise
{ + const spgNftClient = new SpgnftImplReadOnlyClient( + this.rpcClient, + validateAddress(spgNftContract), + ); + return spgNftClient.mintFeeToken(); + } + + /** + * Returns the current mint fee of the collection. + */ + public async getMintFee(spgNftContract: Address): Promise { + const spgNftClient = new SpgnftImplReadOnlyClient( + this.rpcClient, + validateAddress(spgNftContract), + ); + return spgNftClient.mintFee(); + } } diff --git a/packages/core-sdk/src/resources/royalty.ts b/packages/core-sdk/src/resources/royalty.ts index 3017117aa..c5f14e5ae 100644 --- a/packages/core-sdk/src/resources/royalty.ts +++ b/packages/core-sdk/src/resources/royalty.ts @@ -1,6 +1,5 @@ import { Address, - decodeEventLog, encodeFunctionData, erc20Abi, Hex, @@ -11,11 +10,12 @@ import { import { handleError } from "../utils/errors"; import { + BatchClaimAllRevenueRequest, + BatchClaimAllRevenueResponse, ClaimableRevenueRequest, ClaimableRevenueResponse, ClaimAllRevenueRequest, ClaimAllRevenueResponse, - ClaimedToken, PayRoyaltyOnBehalfRequest, PayRoyaltyOnBehalfResponse, TransferClaimedTokensFromIpToWalletParams, @@ -23,31 +23,28 @@ import { import { IpAccountImplClient, IpAssetRegistryClient, - ipRoyaltyVaultImplAbi, IpRoyaltyVaultImplEventClient, IpRoyaltyVaultImplReadOnlyClient, + IpRoyaltyVaultImplRevenueTokenClaimedEvent, Multicall3Client, RoyaltyModuleClient, - royaltyWorkflowsAbi, - royaltyWorkflowsAddress, + RoyaltyWorkflowsClient, SimpleWalletClient, WrappedIpClient, } from "../abi/generated"; -import { IPAccountClient } from "./ipAccount"; import { getAddress, validateAddress, validateAddresses } from "../utils/utils"; import { WIP_TOKEN_ADDRESS } from "../constants/common"; -import { contractCallWithWipFees } from "../utils/wipFeeUtils"; -import { WipSpender } from "../types/utils/wip"; -import { simulateAndWriteContract } from "../utils/contract"; +import { contractCallWithFees } from "../utils/feeUtils"; +import { Erc20Spender } from "../types/utils/wip"; export class RoyaltyClient { public royaltyModuleClient: RoyaltyModuleClient; public ipAssetRegistryClient: IpAssetRegistryClient; - public ipAccountClient: IPAccountClient; public ipRoyaltyVaultImplReadOnlyClient: IpRoyaltyVaultImplReadOnlyClient; public ipRoyaltyVaultImplEventClient: IpRoyaltyVaultImplEventClient; + public royaltyWorkflowsClient: RoyaltyWorkflowsClient; public multicall3Client: Multicall3Client; - public wipClient: WrappedIpClient; + public wrappedIpClient: WrappedIpClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; private readonly walletAddress: Address; @@ -57,14 +54,18 @@ export class RoyaltyClient { this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); this.ipRoyaltyVaultImplReadOnlyClient = new IpRoyaltyVaultImplReadOnlyClient(rpcClient); this.ipRoyaltyVaultImplEventClient = new IpRoyaltyVaultImplEventClient(rpcClient); - this.ipAccountClient = new IPAccountClient(rpcClient, wallet); + this.royaltyWorkflowsClient = new RoyaltyWorkflowsClient(rpcClient, wallet); this.multicall3Client = new Multicall3Client(rpcClient, wallet); - this.wipClient = new WrappedIpClient(rpcClient, wallet); + this.wrappedIpClient = new WrappedIpClient(rpcClient, wallet); this.rpcClient = rpcClient; this.wallet = wallet; this.walletAddress = wallet.account!.address; } - + /** + * Claims all revenue from the child IPs of an ancestor IP, then transfer. + * all claimed tokens to the wallet if the wallet owns the IP or is the claimer. + * If claimed token is WIP, it will also be converted back to IP. + */ public async claimAllRevenue(req: ClaimAllRevenueRequest): Promise { try { const ancestorIpId = validateAddress(req.ancestorIpId); @@ -72,61 +73,42 @@ export class RoyaltyClient { const childIpIds = validateAddresses(req.childIpIds); const royaltyPolicies = validateAddresses(req.royaltyPolicies); const currencyTokens = validateAddresses(req.currencyTokens); - - // todo: use generated code when aeneid explorer is available - const { txHash, receipt } = await simulateAndWriteContract({ - rpcClient: this.rpcClient, - wallet: this.wallet, - waitForTransaction: true, - data: { - abi: royaltyWorkflowsAbi, - address: royaltyWorkflowsAddress[1315], - functionName: "claimAllRevenue", - args: [ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens], - }, - }); const txHashes: Hex[] = []; + const txHash = await this.royaltyWorkflowsClient.claimAllRevenue({ + ancestorIpId, + claimer, + childIpIds, + royaltyPolicies, + currencyTokens, + }); + const receipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); txHashes.push(txHash); // determine if the claimer is an IP owned by the wallet - const isClaimerIp = await this.ipAssetRegistryClient.isRegistered({ - id: claimer, - }); - const ipAccount = new IpAccountImplClient(this.rpcClient, this.wallet, claimer); - let ownsClaimer = claimer === this.walletAddress; - if (isClaimerIp) { - const ipOwner = await ipAccount.owner(); - ownsClaimer = ipOwner === this.walletAddress; - } + const { ownsClaimer, isClaimerIp, ipAccount } = await this.getClaimerInfo(claimer); // if wallet does not own the claimer then we cannot auto claim or unwrap + // If ownsClaimer is false, it means the claimer is neither an IP owned by the wallet nor the wallet address itself. if (!ownsClaimer) { return { receipt, txHashes }; } - - const claimedTokens = this.getClaimedTokensFromReceipt(receipt!); - const skipTransfer = req.claimOptions?.autoTransferAllClaimedTokensFromIp === false; - const skipUnwrapIp = req.claimOptions?.autoUnwrapIpTokens === false; + const claimedTokens = + this.ipRoyaltyVaultImplEventClient.parseTxRevenueTokenClaimedEvent(receipt); + const autoTransfer = req.claimOptions?.autoTransferAllClaimedTokensFromIp !== false; + const autoUnwrapIp = req.claimOptions?.autoUnwrapIpTokens !== false; // transfer claimed tokens from IP to wallet if wallet owns IP - if (!skipTransfer && isClaimerIp && ownsClaimer) { + if (autoTransfer && isClaimerIp && ownsClaimer) { const hashes = await this.transferClaimedTokensFromIpToWallet({ ipAccount, - skipUnwrapIp, claimedTokens, }); txHashes.push(...hashes); - } else if (!skipUnwrapIp && this.walletAddress === claimer) { - // if the claimer is the wallet, then we can unwrap any claimed WIP tokens - for (const { token, amount } of claimedTokens) { - if (token !== WIP_TOKEN_ADDRESS) { - continue; - } - const hash = await this.wipClient.withdraw({ - value: amount, - }); - txHashes.push(hash); - await this.rpcClient.waitForTransactionReceipt({ hash }); + } + if (autoUnwrapIp) { + const hashes = await this.unwrapWipTokens(claimedTokens); + if (hashes) { + txHashes.push(hashes); } } return { receipt, claimedTokens, txHashes }; @@ -135,33 +117,138 @@ export class RoyaltyClient { } } + /** + * Automatically batch claims all revenue from the child IPs of multiple ancestor IPs. + * if multicall is disabled, it will call @link{claimAllRevenue} for each ancestor IP. + * Then transfer all claimed tokens to the wallet if the wallet owns the IP or is the claimer. + * If claimed token is WIP, it will also be converted back to IP. + */ + public async batchClaimAllRevenue( + request: BatchClaimAllRevenueRequest, + ): Promise { + try { + const txHashes: Hex[] = []; + const receipts: TransactionReceipt[] = []; + const claimedTokens: IpRoyaltyVaultImplRevenueTokenClaimedEvent[] = []; + // if the number of ancestor IPs is 1 or if multicall is disabled, then just call claimAllRevenue. + const useMulticallWhenPossible = request.options?.useMulticallWhenPossible !== false; + if (request.ancestorIps.length === 1 || !useMulticallWhenPossible) { + for (const ancestorIp of request.ancestorIps) { + const result = await this.claimAllRevenue({ + ...ancestorIp, + ancestorIpId: ancestorIp.ipId, + claimOptions: { + autoTransferAllClaimedTokensFromIp: false, + autoUnwrapIpTokens: false, + }, + }); + txHashes.push(...result.txHashes); + receipts.push(result.receipt); + if (result.claimedTokens) { + claimedTokens.push(...result.claimedTokens); + } + } + } else { + // Batch claimAllRevenue the calls into a single multicall + const encodedTxs = request.ancestorIps.map( + ({ ipId, claimer, childIpIds, royaltyPolicies, currencyTokens }) => { + const claim = { + ancestorIpId: validateAddress(ipId), + claimer: validateAddress(claimer), + childIpIds: validateAddresses(childIpIds), + royaltyPolicies: validateAddresses(royaltyPolicies), + currencyTokens: validateAddresses(currencyTokens), + }; + return this.royaltyWorkflowsClient.claimAllRevenueEncode(claim).data; + }, + ); + const txHash = await this.royaltyWorkflowsClient.multicall({ data: encodedTxs }); + const receipt = await this.rpcClient.waitForTransactionReceipt({ hash: txHash }); + txHashes.push(txHash); + receipts.push(receipt); + const claimedTokenLogs = + this.ipRoyaltyVaultImplEventClient.parseTxRevenueTokenClaimedEvent(receipt); + claimedTokens.push(...claimedTokenLogs); + } + + // Aggregate claimed tokens by claimer and token address + const aggregatedClaimedTokens = Object.values( + claimedTokens.reduce>( + (acc, curr) => { + const key = `${curr.claimer}_${curr.token}`; + if (!acc[key]) { + acc[key] = { ...curr }; + } else { + acc[key].amount += curr.amount; + } + return acc; + }, + {}, + ), + ); + const claimers = [...new Set(request.ancestorIps.map(({ claimer }) => claimer))]; + const autoTransfer = request.claimOptions?.autoTransferAllClaimedTokensFromIp !== false; + const autoUnwrapIp = request.claimOptions?.autoUnwrapIpTokens !== false; + for (const claimer of claimers) { + const { ownsClaimer, isClaimerIp, ipAccount } = await this.getClaimerInfo(claimer); + + // If ownsClaimer is false, it means the claimer is neither an IP owned by the wallet nor the wallet address itself. + if (!ownsClaimer) { + continue; + } + const filterClaimedTokens = aggregatedClaimedTokens.filter( + (item) => item.claimer === claimer, + ); + // transfer claimed tokens from IP to wallet if wallet owns IP + if (autoTransfer && isClaimerIp && ownsClaimer) { + const hashes = await this.transferClaimedTokensFromIpToWallet({ + ipAccount, + claimedTokens: filterClaimedTokens, + }); + txHashes.push(...hashes); + } + if (autoUnwrapIp) { + // if the claimer is the wallet, then we can unwrap any claimed WIP tokens + const hashes = await this.unwrapWipTokens(filterClaimedTokens); + if (hashes) { + txHashes.push(hashes); + } + } + } + return { + receipts, + claimedTokens: aggregatedClaimedTokens, + txHashes, + }; + } catch (error) { + handleError( + new Error((error as Error).message.replace("Failed to claim all revenue: ", "").trim()), + "Failed to batch claim all revenue", + ); + } + } + /** * Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. - * @param request - The request object that contains all data needed to pay royalty on behalf. - * @param request.receiverIpId The ipId that receives the royalties. - * @param request.payerIpId The ID of the IP asset that pays the royalties. - * @param request.token The token to use to pay the royalties. - * @param request.amount The amount to pay. - * @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 transaction hash. */ public async payRoyaltyOnBehalf( request: PayRoyaltyOnBehalfRequest, ): Promise { try { - const { receiverIpId, payerIpId, token, amount, wipOptions, txOptions } = request; + const { receiverIpId, payerIpId, token, amount, erc20Options, wipOptions, txOptions } = + request; const sender = this.wallet.account!.address; const payAmount = BigInt(amount); if (payAmount <= 0n) { throw new Error("The amount to pay must be number greater than 0."); } const isReceiverRegistered = await this.ipAssetRegistryClient.isRegistered({ - id: getAddress(receiverIpId, "request.receiverIpId"), + id: validateAddress(receiverIpId), }); if (!isReceiverRegistered) { throw new Error(`The receiver IP with id ${receiverIpId} is not registered.`); } - if (getAddress(payerIpId, "request.payerIpId") && payerIpId !== zeroAddress) { + if (validateAddress(payerIpId) && payerIpId !== zeroAddress) { const isPayerRegistered = await this.ipAssetRegistryClient.isRegistered({ id: payerIpId, }); @@ -172,7 +259,7 @@ export class RoyaltyClient { const req = { receiverIpId: receiverIpId, payerIpId: payerIpId, - token: getAddress(token, "request.token"), + token: validateAddress(token), amount: BigInt(amount), }; @@ -183,32 +270,25 @@ export class RoyaltyClient { const contractCall = () => { return this.royaltyModuleClient.payRoyaltyOnBehalf(req); }; - - // auto wrap wallet's IP to WIP if paying WIP - if (token === WIP_TOKEN_ADDRESS) { - const wipSpenders: WipSpender[] = [ - { - address: this.royaltyModuleClient.address, - amount: payAmount, - }, - ]; - return contractCallWithWipFees({ - totalFees: payAmount, - wipOptions, - multicall3Client: this.multicall3Client, - rpcClient: this.rpcClient, - wipClient: this.wipClient, - wipSpenders, - contractCall, - sender, - wallet: this.wallet, - txOptions, - encodedTxs: [encodedTxData], - }); - } else { - const txHash = await contractCall(); - return { txHash }; - } + const tokenSpenders: Erc20Spender[] = [ + { + address: this.royaltyModuleClient.address, + amount: payAmount, + }, + ]; + return contractCallWithFees({ + totalFees: payAmount, + options: { erc20Options, wipOptions }, + multicall3Address: this.multicall3Client.address, + rpcClient: this.rpcClient, + tokenSpenders: tokenSpenders, + contractCall, + sender, + token, + wallet: this.wallet, + txOptions, + encodedTxs: [encodedTxData], + }); } catch (error) { handleError(error, "Failed to pay royalty on behalf"); } @@ -254,33 +334,8 @@ export class RoyaltyClient { return await this.royaltyModuleClient.ipRoyaltyVaults({ ipId: royaltyVaultIpId }); } - private getClaimedTokensFromReceipt(receipt: TransactionReceipt): ClaimedToken[] { - const eventName = "RevenueTokenClaimed"; - const claimedTokens: ClaimedToken[] = []; - for (const log of receipt.logs) { - try { - const event = decodeEventLog({ - abi: ipRoyaltyVaultImplAbi, - eventName: eventName, - data: log.data, - topics: log.topics, - }); - if (event.eventName === eventName) { - claimedTokens.push({ - token: event.args.token, - amount: event.args.amount, - }); - } - } catch (e) { - /* empty */ - } - } - return claimedTokens; - } - private async transferClaimedTokensFromIpToWallet({ ipAccount, - skipUnwrapIp, claimedTokens, }: TransferClaimedTokensFromIpToWalletParams) { const txHashes: Hex[] = []; @@ -300,19 +355,42 @@ export class RoyaltyClient { }); await this.rpcClient.waitForTransactionReceipt({ hash }); txHashes.push(hash); - - // auto unwrap WIP tokens once they are transferred - if (token === WIP_TOKEN_ADDRESS && !skipUnwrapIp) { - const withdrawalHash = await this.wipClient.withdraw({ - value: amount, - }); - txHashes.push(withdrawalHash); - await this.rpcClient.waitForTransactionReceipt({ hash: withdrawalHash }); - } }; for (const { token, amount } of claimedTokens) { await transferToken(token, amount); } return txHashes; } + + private async getClaimerInfo(claimer: Address) { + const isClaimerIp = await this.ipAssetRegistryClient.isRegistered({ + id: claimer, + }); + const ipAccount = new IpAccountImplClient(this.rpcClient, this.wallet, claimer); + let ownsClaimer = claimer === this.walletAddress; + if (isClaimerIp) { + const ipOwner = await ipAccount.owner(); + ownsClaimer = ipOwner === this.walletAddress; + } + return { ownsClaimer, isClaimerIp, ipAccount }; + } + /** + * Unwraps WIP tokens back to their underlying IP tokens. Only accepts a single WIP token entry + * in the claimed tokens array. Throws an error if multiple WIP tokens are found. + */ + private async unwrapWipTokens(claimedTokens: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]) { + const wipTokens = claimedTokens.filter((token) => token.token === WIP_TOKEN_ADDRESS); + if (wipTokens.length > 1) { + throw new Error("Multiple WIP tokens found in the claimed tokens"); + } + const wipToken = wipTokens[0]; + if (!wipToken || wipToken.amount <= 0n) { + return; + } + const hash = await this.wrappedIpClient.withdraw({ + value: wipToken.amount, + }); + await this.rpcClient.waitForTransactionReceipt({ hash }); + return hash; + } } diff --git a/packages/core-sdk/src/resources/wip.ts b/packages/core-sdk/src/resources/wip.ts index 0ee6a530a..450429cea 100644 --- a/packages/core-sdk/src/resources/wip.ts +++ b/packages/core-sdk/src/resources/wip.ts @@ -1,19 +1,25 @@ -import { Address, Hex, PublicClient, WriteContractParameters } from "viem"; +import { Address, PublicClient, WriteContractParameters } from "viem"; import { handleError } from "../utils/errors"; import { SimpleWalletClient, WrappedIpClient, wrappedIpAbi } from "../abi/generated"; import { validateAddress } from "../utils/utils"; import { WIP_TOKEN_ADDRESS } from "../constants/common"; -import { ApproveRequest, DepositRequest, WithdrawRequest } from "../types/resources/wip"; +import { + ApproveRequest, + DepositRequest, + TransferFromRequest, + TransferRequest, + WithdrawRequest, +} from "../types/resources/wip"; import { handleTxOptions } from "../utils/txOptions"; export class WipClient { - public wipClient: WrappedIpClient; + public wrappedIpClient: WrappedIpClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { - this.wipClient = new WrappedIpClient(rpcClient, wallet, WIP_TOKEN_ADDRESS); + this.wrappedIpClient = new WrappedIpClient(rpcClient, wallet, WIP_TOKEN_ADDRESS); this.rpcClient = rpcClient; this.wallet = wallet; } @@ -54,7 +60,7 @@ export class WipClient { if (targetAmt <= 0) { throw new Error("WIP withdraw amount must be greater than 0."); } - const txHash = await this.wipClient.withdraw({ value: targetAmt }); + const txHash = await this.wrappedIpClient.withdraw({ value: targetAmt }); return handleTxOptions({ txHash, txOptions, @@ -68,14 +74,14 @@ export class WipClient { /** * Approve a spender to use the wallet's WIP balance. */ - public async approve(req: ApproveRequest): Promise<{ txHash: Hex }> { + public async approve(req: ApproveRequest) { try { const amount = BigInt(req.amount); if (amount <= 0) { throw new Error("WIP approve amount must be greater than 0."); } const spender = validateAddress(req.spender); - const txHash = await this.wipClient.approve({ + const txHash = await this.wrappedIpClient.approve({ spender, amount, }); @@ -94,7 +100,54 @@ export class WipClient { */ public async balanceOf(addr: Address): Promise { const owner = validateAddress(addr); - const ret = await this.wipClient.balanceOf({ owner }); + const ret = await this.wrappedIpClient.balanceOf({ owner }); return ret.result; } + + /** + * Transfers `amount` of WIP to a recipient `to`. + */ + public async transfer(request: TransferRequest) { + try { + const amount = BigInt(request.amount); + if (amount <= 0) { + throw new Error("WIP transfer amount must be greater than 0."); + } + const txHash = await this.wrappedIpClient.transfer({ + to: validateAddress(request.to), + amount, + }); + return handleTxOptions({ + txHash, + txOptions: request.txOptions, + rpcClient: this.rpcClient, + }); + } catch (error) { + handleError(error, "Failed to transfer WIP"); + } + } + + /** + * Transfers `amount` of WIP from `from` to a recipient `to`. + */ + public async transferFrom(request: TransferFromRequest) { + try { + const amount = BigInt(request.amount); + if (amount <= 0) { + throw new Error("WIP transfer amount must be greater than 0."); + } + const txHash = await this.wrappedIpClient.transferFrom({ + to: validateAddress(request.to), + amount, + from: validateAddress(request.from), + }); + return handleTxOptions({ + txHash, + txOptions: request.txOptions, + rpcClient: this.rpcClient, + }); + } catch (error) { + handleError(error, "Failed to transfer WIP"); + } + } } diff --git a/packages/core-sdk/src/types/common.ts b/packages/core-sdk/src/types/common.ts index 8de88073a..0b0898069 100644 --- a/packages/core-sdk/src/types/common.ts +++ b/packages/core-sdk/src/types/common.ts @@ -9,28 +9,46 @@ export type TypedData = { }; export type IpMetadataAndTxOptions = WithTxOptions & { + /** The desired metadata for the newly minted NFT and newly registered IP. */ ipMetadata?: Partial; }; - export type LicensingConfig = { + /** Whether the configuration is set or not */ isSet: boolean; + /** The minting fee to be paid when minting license tokens. */ mintingFee: bigint | string | number; + /** The hook contract address for the licensing module, or zero address if none. */ licensingHook: Address; + /** The data to be used by the licensing hook. */ hookData: Hex; + /** The commercial revenue share percentage (from 0 to 100%, represented as 100_000_000). */ commercialRevShare: number | string; + /** Whether the licensing is disabled or not. */ disabled: boolean; + /** The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100_000_000) that can be allocated to the IP when it is added to the group. */ expectMinimumGroupRewardShare: number | string; + /** The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or zero address if the IP does not want to be added to any group. */ expectGroupRewardPool: Address; }; -export type InnerLicensingConfig = { +export type ValidatedLicensingConfig = LicensingConfig & { mintingFee: bigint; commercialRevShare: number; expectMinimumGroupRewardShare: number; -} & LicensingConfig; +}; /** * Input for token amount, can be bigint or number. * Will be converted to bigint for contract calls. */ export type TokenAmountInput = bigint | number; + +/** + * The type of revenue share. + * It is used to determine the type of revenue share to be used in the revenue share calculation and throw error when the revenue share is not valid. + */ +export enum RevShareType { + COMMERCIAL_REVENUE_SHARE = "CommercialRevShare", + MAX_REVENUE_SHARE = "MaxRevenueShare", + MAX_ALLOWED_REWARD_SHARE = "MaxAllowedRewardShare", +} diff --git a/packages/core-sdk/src/types/options.ts b/packages/core-sdk/src/types/options.ts index e629b4981..ea2cfb8f4 100644 --- a/packages/core-sdk/src/types/options.ts +++ b/packages/core-sdk/src/types/options.ts @@ -1,16 +1,82 @@ -import { WaitForTransactionReceiptParameters } from "viem"; +import { Hex, PublicClient, TransactionReceipt, WaitForTransactionReceiptParameters } from "viem"; export type TxOptions = Omit & { - // Whether or not to wait for the transaction so you - // can receive a transaction receipt in return (which - // contains data about the transaction and return values). + /** + * Whether or not to wait for the transaction so you can receive a transaction receipt in return + * (which contains data about the transaction and return values). + */ waitForTransaction?: boolean; - // When the time of setting this option, the transaction will - // not submit and execute, it will only encode the abi and - // function data and return. + /** + * When this option is set, the transaction will not submit and execute. + * It will only encode the ABI and function data and return. + */ encodedTxDataOnly?: boolean; }; export type WithTxOptions = { txOptions?: TxOptions; }; + +export type ERC20Options = { + /** + * Automatically approve erc20 usage when erc20 is needed but current allowance + * is not sufficient. + * Set this to `false` to disable this behavior. + * + * @default true + */ + enableAutoApprove?: boolean; +}; +/** + * Options to override the default behavior of the auto approve logic + */ +export type WithERC20Options = { + erc20Options?: ERC20Options; +}; + +export type WipOptions = { + /** + * Use multicall to batch the WIP calls into one transaction when possible. + * + * @default true + */ + useMulticallWhenPossible?: boolean; + + /** + * By default IP is converted to WIP if the current WIP + * balance does not cover the fees. + * Set this to `false` to disable this behavior. + * + * @default true + */ + enableAutoWrapIp?: boolean; + + /** + * Automatically approve WIP usage when WIP is needed but current allowance + * is not sufficient. + * Set this to `false` to disable this behavior. + * + * @default true + */ + enableAutoApprove?: boolean; +}; +/** + * Options to override the default behavior of the auto wrapping IP + * and auto approve logic. + */ +export type WithWipOptions = { + /** options to configure WIP behavior */ + wipOptions?: WipOptions; +}; +export type HandleTxOptionsParams = { + txHash: Hex; + txOptions?: TxOptions; + rpcClient: PublicClient; +}; + +export type TransactionResponse = { + txHash: Hex; + + /** Transaction receipt, only available if waitForTransaction is set to true */ + receipt?: TransactionReceipt; +}; diff --git a/packages/core-sdk/src/types/resources/dispute.ts b/packages/core-sdk/src/types/resources/dispute.ts index e65c6a588..c377b6b39 100644 --- a/packages/core-sdk/src/types/resources/dispute.ts +++ b/packages/core-sdk/src/types/resources/dispute.ts @@ -1,15 +1,31 @@ -import { Address } from "viem"; +import { Address, Hex } from "viem"; -import { TxOptions } from "../options"; +import { TxOptions, WithTxOptions } from "../options"; import { EncodedTxData } from "../../abi/generated"; -export type RaiseDisputeRequest = { +export type RaiseDisputeRequest = WithTxOptions & { + /** The IP ID that is the target of the dispute. */ targetIpId: Address; + /** + * Content Identifier (CID) for the dispute evidence. + * This should be obtained by uploading your dispute evidence (documents, images, etc.) to IPFS. + * @example "QmX4zdp8VpzqvtKuEqMo6gfZPdoUx9TeHXCgzKLcFfSUbk" + */ cid: string; + /** + * The target tag of the dispute. + * @see https://docs.story.foundation/docs/dispute-module#dispute-tags + * @example "IMPROPER_REGISTRATION" + */ targetTag: string; + /** The liveness is the time window (in seconds) in which a counter dispute can be presented (30days). */ liveness: bigint | number | string; + /** + * The amount of wrapper IP that the dispute initiator pays upfront into a pool. + * To counter that dispute the opposite party of the dispute has to place a bond of the same amount. + * The winner of the dispute gets the original bond back + 50% of the other party bond. The remaining 50% of the loser party bond goes to the reviewer. + */ bond: bigint | number | string; - txOptions?: TxOptions; }; export type RaiseDisputeResponse = { @@ -39,3 +55,41 @@ export type ResolveDisputeResponse = { txHash?: string; encodedTxData?: EncodedTxData; }; + +export type TagIfRelatedIpInfringedRequest = { + infringementTags: { + /** The ipId to tag */ + ipId: Address; + /** The dispute id that tagged the related infringing ipId */ + disputeId: number | string | bigint; + }[]; + options?: { + /** + * Use multicall to batch the calls into one transaction when possible. + * + * If only 1 infringementTag is provided, multicall will not be used. + * @default true + */ + useMulticallWhenPossible?: boolean; + }; +} & WithTxOptions; + +export type DisputeAssertionRequest = { + /** + * The IP ID that is the target of the dispute. + */ + ipId: Address; + /** + * The identifier of the assertion that was disputed. + * + * You can get this from the `disputeId` by calling `dispute.disputeIdToAssertionId`. + */ + assertionId: Hex; + /** + * Content Identifier (CID) for the counter evidence. + * This should be obtained by uploading your dispute evidence (documents, images, etc.) to IPFS. + * + * @example "QmX4zdp8VpzqvtKuEqMo6gfZPdoUx9TeHXCgzKLcFfSUbk" + */ + counterEvidenceCID: string; +} & WithTxOptions; diff --git a/packages/core-sdk/src/types/resources/group.ts b/packages/core-sdk/src/types/resources/group.ts index 81f15e3a9..8ecf16759 100644 --- a/packages/core-sdk/src/types/resources/group.ts +++ b/packages/core-sdk/src/types/resources/group.ts @@ -2,27 +2,38 @@ import { Address } from "viem"; import { TxOptions } from "../options"; import { EncodedTxData } from "../../abi/generated"; -import { InnerLicensingConfig, IpMetadataAndTxOptions, LicensingConfig } from "../common"; +import { IpMetadataAndTxOptions, LicensingConfig, ValidatedLicensingConfig } from "../common"; export type LicenseData = { licenseTermsId: string | bigint | number; - licensingConfig: LicensingConfig; + licensingConfig?: LicensingConfig; licenseTemplate?: Address; }; -export type InnerLicenseData = { +export type ValidatedLicenseData = { licenseTermsId: bigint; - licensingConfig: InnerLicensingConfig; + licensingConfig: ValidatedLicensingConfig; licenseTemplate: Address; }; - export type MintAndRegisterIpAndAttachLicenseAndAddToGroupRequest = { spgNftContract: Address; + /** The ID of the group IP to add the newly registered IP. */ groupId: Address; - allowDuplicates: boolean; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; + /** The maximum reward share percentage that can be allocated to each member IP. */ maxAllowedRewardShare: number | string; + /** The data of the license and its configuration to be attached to the new group IP. */ licenseData: LicenseData[]; + /** The address of the recipient of the minted NFT. If not provided, the function will use the user's own wallet address. */ recipient?: Address; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: string | number | bigint; } & IpMetadataAndTxOptions; @@ -42,13 +53,19 @@ export type RegisterGroupResponse = { encodedTxData?: EncodedTxData; groupId?: Address; }; - export type RegisterIpAndAttachLicenseAndAddToGroupRequest = { nftContract: Address; tokenId: bigint | string | number; + /** The ID of the group IP to add the newly registered IP. */ groupId: Address; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: bigint; + /** The data of the license and its configuration to be attached to the new group IP. */ licenseData: LicenseData[]; + /** The maximum reward share percentage that can be allocated to each member IP. */ maxAllowedRewardShare: number | string; } & IpMetadataAndTxOptions; @@ -59,7 +76,9 @@ export type RegisterIpAndAttachLicenseAndAddToGroupResponse = { tokenId?: bigint; }; export type RegisterGroupAndAttachLicenseRequest = { + /** The address specifying how royalty will be split amongst the pool of IPs in the group. */ groupPool: Address; + /** The data of the license and its configuration to be attached to the new group IP. */ licenseData: LicenseData; txOptions?: TxOptions; }; @@ -69,11 +88,14 @@ export type RegisterGroupAndAttachLicenseResponse = { encodedTxData?: EncodedTxData; groupId?: Address; }; - export type RegisterGroupAndAttachLicenseAndAddIpsRequest = { + /** The address specifying how royalty will be split amongst the pool of IPs in the group. */ groupPool: Address; + /** The IP IDs of the IPs to be added to the group. */ ipIds: Address[]; + /** The data of the license and its configuration to be attached to the new group IP. */ licenseData: LicenseData; + /** The maximum reward share percentage that can be allocated to each member IP. */ maxAllowedRewardShare: number | string; txOptions?: TxOptions; }; diff --git a/packages/core-sdk/src/types/resources/ipAccount.ts b/packages/core-sdk/src/types/resources/ipAccount.ts index 745ea2f0c..4ebf6395a 100644 --- a/packages/core-sdk/src/types/resources/ipAccount.ts +++ b/packages/core-sdk/src/types/resources/ipAccount.ts @@ -12,7 +12,7 @@ export type IPAccountExecuteRequest = { }; export type IPAccountExecuteResponse = { - txHash?: string; + txHash?: Hex; encodedTxData?: EncodedTxData; }; @@ -28,7 +28,7 @@ export type IPAccountExecuteWithSigRequest = { }; export type IPAccountExecuteWithSigResponse = { - txHash?: string; + txHash?: Hex; encodedTxData?: EncodedTxData; }; @@ -39,3 +39,12 @@ export type TokenResponse = { tokenContract: Address; tokenId: bigint; }; + +export type SetIpMetadataRequest = { + ipId: Address; + /** The metadataURI to set for the IP asset. */ + metadataURI: string; + /** The hash of metadata at metadataURI. */ + metadataHash: Hex; + txOptions?: Omit; +}; diff --git a/packages/core-sdk/src/types/resources/ipAsset.ts b/packages/core-sdk/src/types/resources/ipAsset.ts index fff1a0f5d..30a771afa 100644 --- a/packages/core-sdk/src/types/resources/ipAsset.ts +++ b/packages/core-sdk/src/types/resources/ipAsset.ts @@ -1,18 +1,31 @@ import { Address, Hash, Hex, TransactionReceipt } from "viem"; -import { TxOptions } from "../options"; -import { RegisterPILTermsRequest } from "./license"; +import { TxOptions, WithWipOptions } from "../options"; +import { LicenseTerms, RegisterPILTermsRequest } from "./license"; import { EncodedTxData } from "../../abi/generated"; -import { IpMetadataAndTxOptions, LicensingConfig } from "../common"; +import { IpMetadataAndTxOptions, LicensingConfig, ValidatedLicensingConfig } from "../common"; import { IpMetadataForWorkflow } from "../../utils/getIpMetadataForWorkflow"; -import { WithWipOptions } from "../utils/wip"; export type DerivativeData = { parentIpIds: Address[]; + /** The IDs of the license terms that the parent IP supports. */ licenseTermsIds: bigint[] | string[] | number[]; - maxMintingFee: bigint | string | number; - maxRts: number | string; - maxRevenueShare: number | string; + /** + * The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. + * @default 0 + */ + maxMintingFee?: bigint | string | number; + /** + * The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). + * @default 100_000_000 + */ + maxRts?: number | string; + /** + * The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100_000_000). + * @default 100 + */ + maxRevenueShare?: number | string; + /** The license template address, default value is Programmable IP License. */ licenseTemplate?: Address; }; export type InternalDerivativeData = { @@ -24,9 +37,9 @@ export type InternalDerivativeData = { maxRevenueShare: number; licenseTemplate: Address; }; -export type RegisterIpResponse = { +export type RegisterIpResponse = RegistrationResponse & { encodedTxData?: EncodedTxData; -} & CommonRegistrationResponse; +}; export type RegisterRequest = { nftContract: Address; @@ -46,25 +59,39 @@ export type RegisterDerivativeWithLicenseTokensResponse = { encodedTxData?: EncodedTxData; }; -export type RegisterDerivativeRequest = { - txOptions?: TxOptions; - childIpId: Address; -} & DerivativeData; +export type RegisterDerivativeRequest = WithWipOptions & + DerivativeData & { + txOptions?: TxOptions; + childIpId: Address; + }; export type RegisterDerivativeResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; }; -export type LicenseTermsData = { +export type LicenseTermsData = { terms: T; - licensingConfig: U; + licensingConfig?: C; }; + +export type ValidatedLicenseTermsData = Omit< + LicenseTermsData, + "licensingConfig" +> & { + licensingConfig: ValidatedLicensingConfig; +}; + export type MintAndRegisterIpAssetWithPilTermsRequest = { spgNftContract: Address; - allowDuplicates: boolean; - licenseTermsData: LicenseTermsData[]; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; + /** The data of the license and its configuration to be attached to the IP. */ + licenseTermsData: LicenseTermsData[]; + /** The address to receive the minted NFT. If not provided, the function will use the user's own wallet address. */ recipient?: Address; - royaltyPolicyAddress?: Address; } & IpMetadataAndTxOptions & WithWipOptions; @@ -80,13 +107,13 @@ export type MintAndRegisterIpAssetWithPilTermsResponse = { export type RegisterIpAndMakeDerivativeRequest = { nftContract: Address; tokenId: string | number | bigint; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: string | number | bigint; + /** The derivative data to be used for register derivative. */ derivData: DerivativeData; - sigMetadataAndRegister?: { - signer: Address; - deadline: bigint | string | number; - signature: Hex; - }; } & IpMetadataAndTxOptions & WithWipOptions; @@ -101,7 +128,12 @@ export type RegisterIpAndMakeDerivativeResponse = { export type RegisterIpAndAttachPilTermsRequest = { nftContract: Address; tokenId: bigint | string | number; - licenseTermsData: LicenseTermsData[]; + /** The data of the license and its configuration to be attached to the IP. */ + licenseTermsData: LicenseTermsData[]; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: bigint | number | string; } & IpMetadataAndTxOptions; @@ -112,69 +144,22 @@ export type RegisterIpAndAttachPilTermsResponse = { licenseTermsIds?: bigint[]; tokenId?: bigint; }; - export type MintAndRegisterIpAndMakeDerivativeRequest = { spgNftContract: Address; + /** The derivative data to be used for register derivative. */ derivData: DerivativeData; + /** The address to receive the minted NFT. If not provided, the function will use the user's own wallet address. */ recipient?: Address; - allowDuplicates: boolean; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; } & IpMetadataAndTxOptions & WithWipOptions; -export type MintAndRegisterIpAndMakeDerivativeResponse = { +export type MintAndRegisterIpAndMakeDerivativeResponse = RegistrationResponse & { encodedTxData?: EncodedTxData; -} & CommonRegistrationResponse; - -export type IpRelationship = { - parentIpId: Address; - type: string; -}; - -export type IpCreator = { - name: string; - address: Address; - description?: string; - image?: string; - socialMedia?: IpCreatorSocial[]; - role?: string; - contributionPercent: number; // add up to 100 -}; - -export type IpCreatorSocial = { - platform: string; - url: string; -}; - -export type IpMedia = { - name: string; - url: string; - mimeType: string; -}; - -export type IpAttribute = { - key: string; - value: string | number; -}; - -export type StoryProtocolApp = { - id: string; - name: string; - website: string; - action?: string; -}; - -export type GenerateCreatorMetadataParam = { - name: string; - address: Address; - contributionPercent: number; - description?: string; - image?: string; - socialMedia?: IpCreatorSocial[]; - role?: string; -}; -export type IPRobotTerms = { - userAgent: string; - allow: string; }; type IPMetadataInfo = { @@ -186,46 +171,23 @@ type IPMetadataInfo = { }; }; -export type GenerateIpMetadataParam = { - title?: string; - description?: string; - ipType?: string; - relationships?: IpRelationship[]; - createdAt?: string; - watermarkImg?: string; - creators?: IpCreator[]; - media?: IpMedia[]; - attributes?: IpAttribute[]; - app?: StoryProtocolApp; - tags?: string[]; - robotTerms?: IPRobotTerms; - additionalProperties?: { [key: string]: unknown }; -}; -export type IpMetadata = { - title?: string; - description?: string; - ipType?: string; - relationships?: IpRelationship[]; - createdAt?: string; - watermarkImg?: string; - creators?: IpCreator[]; - media?: IpMedia[]; - attributes?: IpAttribute[]; - appInfo?: StoryProtocolApp[]; - tags?: string[]; - robotTerms?: IPRobotTerms; - [key: string]: unknown; -}; - -export type MintAndRegisterIpRequest = { - spgNftContract: Address; - recipient?: Address; - allowDuplicates: boolean; -} & IpMetadataAndTxOptions; - +export type MintAndRegisterIpRequest = IpMetadataAndTxOptions & + WithWipOptions & { + spgNftContract: Address; + recipient?: Address; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; + }; export type RegisterPilTermsAndAttachRequest = { ipId: Address; - licenseTermsData: LicenseTermsData[]; + /** The data of the license and its configuration to be attached to the IP. */ + licenseTermsData: LicenseTermsData[]; + /** The deadline for the signature in seconds. + * @default 1000 + */ deadline?: string | number | bigint; txOptions?: TxOptions; }; @@ -241,7 +203,11 @@ export type MintAndRegisterIpAndMakeDerivativeWithLicenseTokensRequest = { licenseTokenIds: string[] | bigint[] | number[]; recipient?: Address; maxRts: number | string; - allowDuplicates: boolean; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; } & IpMetadataAndTxOptions & WithWipOptions; @@ -297,12 +263,17 @@ export type BatchRegisterResponse = { spgTxHash?: Hex; results?: IpIdAndTokenId<"nftContract">[]; }; - export type RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest = { nftContract: Address; tokenId: bigint | string | number; - licenseTermsData: LicenseTermsData[]; + /** The data of the license and its configuration to be attached to the new group IP. */ + licenseTermsData: LicenseTermsData[]; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: string | number | bigint; + /** Authors of the IP and their shares of the royalty tokens. */ royaltyShares: RoyaltyShare[]; txOptions?: Omit; } & IPMetadataInfo; @@ -322,7 +293,12 @@ export type DistributeRoyaltyTokens = { txOptions?: Omit; }; export type RoyaltyShare = { + /** The address of the recipient. */ recipient: Address; + /** + * The percentage of the total royalty share. For example, a value of 10 represents 10% of max royalty shares, which is 10,000,000. + * @example 10 + */ percentage: number; }; export type IpIdAndTokenId = T extends undefined @@ -332,9 +308,16 @@ export type IpIdAndTokenId = T extends undefined export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest = { nftContract: Address; tokenId: bigint | string | number; + /** + * The deadline for the signature in seconds. + * @default 1000 + */ deadline?: string | number | bigint; + /** The derivative data to be used for register derivative.*/ derivData: DerivativeData; + /** Authors of the IP and their shares of the royalty tokens. */ royaltyShares: RoyaltyShare[]; + /** The desired metadata for the newly minted NFT and newly registered IP. */ ipMetadata?: IpMetadataForWorkflow; txOptions?: Omit; } & WithWipOptions; @@ -346,15 +329,19 @@ export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRes tokenId: bigint; ipRoyaltyVault: Address; }; - export type MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensRequest = { + /** The address of the SPG NFT contract. */ spgNftContract: Address; - allowDuplicates: boolean; - licenseTermsData: { - terms: RegisterPILTermsRequest; - licensingConfig: LicensingConfig; - }[]; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; + /** The data of the license and its configuration to be attached to the new group IP. */ + licenseTermsData: LicenseTermsData[]; + /** Authors of the IP and their shares of the royalty tokens */ royaltyShares: RoyaltyShare[]; + /** The address to receive the minted NFT. If not provided, the function will use the user's own wallet address. */ recipient?: Address; txOptions?: Omit; } & IPMetadataInfo & @@ -367,11 +354,19 @@ export type MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensResponse ipRoyaltyVault?: Address; tokenId?: bigint; }; + export type MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest = { spgNftContract: Address; + /** The derivative data to be used for register derivative. */ derivData: DerivativeData; + /** Authors of the IP and their shares of the royalty tokens. */ royaltyShares: RoyaltyShare[]; - allowDuplicates: boolean; + /** + * Set to true to allow minting an NFT with a duplicate metadata hash. + * @default true + */ + allowDuplicates?: boolean; + /** The address to receive the minted NFT. If not provided, the function will use the user's own wallet address. */ recipient?: Address; txOptions?: Omit; } & IPMetadataInfo & @@ -383,7 +378,7 @@ export type MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensResponse tokenId?: bigint; }; -export type CommonRegistrationHandlerParams = WithWipOptions & { +export type CommonRegistrationParams = WithWipOptions & { contractCall: () => Promise; encodedTxs: EncodedTxData[]; spgNftContract?: Address; @@ -394,9 +389,13 @@ export type CommonRegistrationHandlerParams = WithWipOptions & { txOptions?: TxOptions; }; -export type CommonRegistrationResponse = { +export type RegistrationResponse = { txHash?: Hex; + receipt?: TransactionReceipt; ipId?: Address; tokenId?: bigint; - receipt?: TransactionReceipt; +}; + +export type CommonRegistrationTxResponse = RegistrationResponse & { + txHash: Hex; }; diff --git a/packages/core-sdk/src/types/resources/ipMetadata.ts b/packages/core-sdk/src/types/resources/ipMetadata.ts new file mode 100644 index 000000000..5757a35d3 --- /dev/null +++ b/packages/core-sdk/src/types/resources/ipMetadata.ts @@ -0,0 +1,285 @@ +import { Address, Hash } from "viem"; + +export type AIMetadata = { + /** + * Url to the character file. + * + * @example https://github.com/elizaOS/characterfile/blob/main/examples/example.character.json + */ + characterFileUrl: string; + /** + * Hash of the character file using SHA-256 hashing algorithm. + */ + characterFileHash: Hash; +}; + +/** + * IPA Metadata Standard Parameters + * + * This is the metadata that is associated with an IP Asset, + * and gets stored inside of an IP Account. + * + * @see {@link https://docs.story.foundation/docs/ipa-metadata-standard|IPA Metadata Standard Docs} + */ +export type IpMetadata = { + /** Title of the IP. Required for Story Explorer. */ + title?: string; + /** Description of the IP. Required for Story Explorer. */ + description?: string; + /** + * Date/Time that the IP was created (either ISO8601 or unix format). + * + * This field can be used to specify historical dates that aren’t on-chain. + * For example, Harry Potter was published on June 26. + * + * Required for Story Explorer. + * + * @example "1728401700" + */ + createdAt?: string; + /** An image for the IP. Required for Story Explorer. */ + image?: string; + /** + * Hash of your `image` using SHA-256 hashing algorithm. + * See {@link https://docs.story.foundation/docs/ipa-metadata-standard#hashing-content|here} for how that is done. + * + * Required for Story Explorer. + */ + imageHash?: Hash; + /** + * An array of information about the creators. + * + * Required for Story Explorer. + */ + creators?: IpCreator[]; + /** + * Url to the actual media file (ex video, audio, image, etc). + * Used for infringement checking. + * + * Required for Commercial Infringement Check + * + * @example https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8 + */ + mediaUrl?: string; + /** + * Hashed string of the media using SHA-256 hashing algorithm. + * + * Required for Commercial Infringement Check + * + * @example 0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e + */ + mediaHash?: Hash; + /** + * Type of media (audio, video, image), based on mimeType. + * + * See {@link https://docs.story.foundation/docs/ipa-metadata-standard#media-types|here} + * for all the allowed media types. + * + * Required for Commercial Infringement Check + */ + mediaType?: string; + /** + * Used for registering & displaying AI Agent Metadata. + * + * Required for AI Agents + */ + aiMetadata?: AIMetadata; + + /** + * Any other fields can be included in the metadata. + */ + [key: string]: unknown; +} & IpMetadataExperimentalAttributes; + +/** + * Experimental ip metadata fields that are not required but may be + * considered for future use. + */ +export type IpMetadataExperimentalAttributes = { + /** + * **Experimental**: Type of the IP Asset, can be defined arbitrarily by the + * creator. I.e. “character”, “chapter”, “location”, “items”, "music", etc + */ + ipType?: string; + /** + * **Experimental**: The detailed relationship info with the IPA’s direct parent asset, + * such as APPEARS_IN, FINETUNED_FROM, etc + */ + relationships?: IpRelationship[]; + /** + * **Experimental**: A separate image with your watermark already applied. + * This way apps choosing to use it can render this version of the image (with watermark applied). + */ + watermarkImg?: string; + /** + * **Experimental**: An array of supporting media + */ + media?: IpMedia[]; + /** + * **Experimental**: This is assigned to verified application from + * Story Protocol directly (on a request basis so far). + */ + app?: StoryProtocolApp; + /** + * **Experimental**: Any tags that can help surface this IPA + */ + tags?: string[]; + /** + * **Experimental**: Allows you to set Do Not Train for a specific agent + */ + robotTerms?: IPRobotTerms; +}; + +export type IPRobotTerms = { + userAgent: string; + allow: string; +}; + +export type StoryProtocolApp = { + id: string; + name: string; + website: string; + action?: string; +}; + +export type IpMedia = { + name: string; + url: string; + mimeType: string; +}; + +export type IpRelationship = { + parentIpId: Address; + /** + * Type of relationship between the parent and child IP. + * + * @see {@link https://docs.story.foundation/docs/ipa-metadata-standard#relationship-types|Relationship Types} for all relationship types. + * + * @example StoryRelationship.APPEARS_IN + */ + type: StoryRelationship | AIRelationship; +}; + +export type IpCreator = { + /** + * Name of the creator. + * + * @example "Story Foundation" + */ + name: string; + address: Address; + description?: string; + image?: string; + socialMedia?: IpCreatorSocial[]; + role?: string; + /** + * Contribution percent of the creator. + * + * Total contribution percent of all creators should add up to 100. + */ + contributionPercent: number; +}; + +export type IpCreatorSocial = { + /** + * Social media platform. + * + * @example "Discord" + */ + platform: string; + url: string; +}; + +/** + * Enum representing the various relationship types in a story narrative. + */ +export enum StoryRelationship { + /** A character appears in a chapter. */ + APPEARS_IN = "APPEARS_IN", + /** A chapter belongs to a book. */ + BELONGS_TO = "BELONGS_TO", + /** A book is part of a series. */ + PART_OF = "PART_OF", + /** A chapter continues from the previous one. */ + CONTINUES_FROM = "CONTINUES_FROM", + /** An event leads to a consequence. */ + LEADS_TO = "LEADS_TO", + /** An event foreshadows future developments. */ + FORESHADOWS = "FORESHADOWS", + /** A character conflicts with another character. */ + CONFLICTS_WITH = "CONFLICTS_WITH", + /** A decision results in a significant change. */ + RESULTS_IN = "RESULTS_IN", + /** A subplot depends on the main plot. */ + DEPENDS_ON = "DEPENDS_ON", + /** A prologue sets up the story. */ + SETS_UP = "SETS_UP", + /** A chapter follows from the previous one. */ + FOLLOWS_FROM = "FOLLOWS_FROM", + /** A twist reveals that something unexpected occurred. */ + REVEALS_THAT = "REVEALS_THAT", + /** A character develops over the course of the story. */ + DEVELOPS_OVER = "DEVELOPS_OVER", + /** A chapter introduces a new character or element. */ + INTRODUCES = "INTRODUCES", + /** A conflict resolves in a particular outcome. */ + RESOLVES_IN = "RESOLVES_IN", + /** A theme connects to the main narrative. */ + CONNECTS_TO = "CONNECTS_TO", + /** A subplot relates to the central theme. */ + RELATES_TO = "RELATES_TO", + /** A scene transitions from one setting to another. */ + TRANSITIONS_FROM = "TRANSITIONS_FROM", + /** A character interacted with another character. */ + INTERACTED_WITH = "INTERACTED_WITH", + /** An event leads into the climax. */ + LEADS_INTO = "LEADS_INTO", + /** Story happening in parallel or around the same timeframe. */ + PARALLEL = "PARALLEL", +} + +/** + * Enum representing the different relationship types for AI-related metadata. + */ +export enum AIRelationship { + /** A model is trained on a dataset. */ + TRAINED_ON = "TRAINED_ON", + /** A model is finetuned from a base model. */ + FINETUNED_FROM = "FINETUNED_FROM", + /** An image is generated from a fine-tuned model. */ + GENERATED_FROM = "GENERATED_FROM", + /** A model requires data for training. */ + REQUIRES_DATA = "REQUIRES_DATA", + /** A remix is based on a specific workflow. */ + BASED_ON = "BASED_ON", + /** Sample data influences model output. */ + INFLUENCES = "INFLUENCES", + /** A pipeline creates a fine-tuned model. */ + CREATES = "CREATES", + /** A workflow utilizes a base model. */ + UTILIZES = "UTILIZES", + /** A fine-tuned model is derived from a base model. */ + DERIVED_FROM = "DERIVED_FROM", + /** A model produces generated images. */ + PRODUCES = "PRODUCES", + /** A remix modifies the base workflow. */ + MODIFIES = "MODIFIES", + /** An AI-generated image references original data. */ + REFERENCES = "REFERENCES", + /** A model is optimized by specific algorithms. */ + OPTIMIZED_BY = "OPTIMIZED_BY", + /** A fine-tuned model inherits features from the base model. */ + INHERITS = "INHERITS", + /** A fine-tuning process applies to a model. */ + APPLIES_TO = "APPLIES_TO", + /** A remix combines elements from multiple datasets. */ + COMBINES = "COMBINES", + /** A model generates variants of an image. */ + GENERATES_VARIANTS = "GENERATES_VARIANTS", + /** A fine-tuning process expands on base capabilities. */ + EXPANDS_ON = "EXPANDS_ON", + /** A workflow configures a model’s parameters. */ + CONFIGURES = "CONFIGURES", + /** A fine-tuned model adapts to new data. */ + ADAPTS_TO = "ADAPTS_TO", +} diff --git a/packages/core-sdk/src/types/resources/license.ts b/packages/core-sdk/src/types/resources/license.ts index 7c40c83cc..4195d4d80 100644 --- a/packages/core-sdk/src/types/resources/license.ts +++ b/packages/core-sdk/src/types/resources/license.ts @@ -1,9 +1,8 @@ import { Address, TransactionReceipt } from "viem"; -import { WithTxOptions, TxOptions } from "../options"; +import { WithTxOptions, TxOptions, WithWipOptions } from "../options"; import { EncodedTxData } from "../../abi/generated"; import { LicensingConfig } from "../common"; -import { WithWipOptions } from "../utils/wip"; export type LicenseApiResponse = { data: License; @@ -140,16 +139,15 @@ export type PredictMintingLicenseFeeRequest = { txOptions?: TxOptions; }; -export type InnerLicensingConfig = { - mintingFee: bigint; - commercialRevShare: number; - expectMinimumGroupRewardShare: number; -} & LicensingConfig; export type SetLicensingConfigRequest = { + /** The address of the IP for which the configuration is being set. */ ipId: Address; + /** The ID of the license terms within the license template. */ licenseTermsId: string | number | bigint; - licensingConfig: LicensingConfig; + /** The address of the license template used. */ licenseTemplate: Address; + /** The licensing configuration for the license. */ + licensingConfig: LicensingConfig; txOptions?: TxOptions; }; diff --git a/packages/core-sdk/src/types/resources/royalty.ts b/packages/core-sdk/src/types/resources/royalty.ts index 4be343377..8bc7d251c 100644 --- a/packages/core-sdk/src/types/resources/royalty.ts +++ b/packages/core-sdk/src/types/resources/royalty.ts @@ -1,52 +1,32 @@ import { Address, Hash, TransactionReceipt } from "viem"; -import { TxOptions, WithTxOptions } from "../options"; -import { EncodedTxData, IpAccountImplClient } from "../../abi/generated"; -import { WithWipOptions } from "../utils/wip"; +import { WithTxOptions, WithWipOptions } from "../options"; +import { + EncodedTxData, + IpAccountImplClient, + IpRoyaltyVaultImplRevenueTokenClaimedEvent, +} from "../../abi/generated"; +import { WithERC20Options } from "../options"; import { TokenAmountInput } from "../common"; -export type RoyaltyPolicyApiResponse = { - data: RoyaltyPolicy; -}; - -export type RoyaltyPolicy = { - id: Address; // ipId - targetAncestors: string[]; - targetRoyaltyAmount: string[]; -}; - -export type RoyaltyContext = { - targetAncestors: string[]; - targetRoyaltyAmount: number[]; - parentAncestors1: string[]; - parentAncestors2: string[]; - parentAncestorsRoyalties1: number[]; - parentAncestorsRoyalties2: number[]; -}; - -export type RoyaltyData = [ - isUnlinkableToParents: boolean, - ipRoyaltyVault: Address, - royaltyStack: bigint, - ancestorsAddresses: Address, - ancestorsRoyalties: bigint[], -]; - export type ClaimableRevenueRequest = { royaltyVaultIpId: Address; claimer: Address; token: Address; }; - export type ClaimableRevenueResponse = bigint; - -export type PayRoyaltyOnBehalfRequest = { - receiverIpId: Address; - payerIpId: Address; - token: Address; - amount: TokenAmountInput; -} & WithTxOptions & - WithWipOptions; +export type PayRoyaltyOnBehalfRequest = WithTxOptions & + WithERC20Options & + WithWipOptions & { + /** The IP ID that receives the royalties. */ + receiverIpId: Address; + /** The IP ID that pays the royalties. */ + payerIpId: Address; + /** The token to use to pay the royalties. */ + token: Address; + /** The amount to pay. */ + amount: TokenAmountInput; + }; export type PayRoyaltyOnBehalfResponse = { txHash?: string; @@ -54,93 +34,7 @@ export type PayRoyaltyOnBehalfResponse = { encodedTxData?: EncodedTxData; }; -export type SnapshotRequest = { - royaltyVaultIpId: Address; - txOptions?: TxOptions; -}; - -export type ClaimRevenueRequest = { - snapshotIds: string[] | number[] | bigint[]; - token: Address; - royaltyVaultIpId: Address; - account?: Address; - txOptions?: TxOptions; -}; - -export type ClaimRevenueResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - claimableToken?: bigint; -}; -export type SnapshotResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - snapshotId?: bigint; -}; -type RoyaltyClaimDetail = { - childIpId: Address; - royaltyPolicy: Address; - currencyToken: Address; - amount: bigint | string | number; -}; -export type TransferToVaultAndSnapshotAndClaimByTokenBatchRequest = { - ancestorIpId: Address; - royaltyClaimDetails: RoyaltyClaimDetail[]; - claimer?: Address; - txOptions?: TxOptions; -}; -export type TransferToVaultAndSnapshotAndClaimByTokenBatchResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - snapshotId?: bigint; - amountsClaimed?: bigint; -}; -export type TransferToVaultAndSnapshotAndClaimBySnapshotBatchRequest = { - ancestorIpId: Address; - unclaimedSnapshotIds: bigint[] | number[] | string[]; - claimer?: Address; - royaltyClaimDetails: RoyaltyClaimDetail[]; - txOptions?: TxOptions; -}; -export type TransferToVaultAndSnapshotAndClaimBySnapshotBatchResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - snapshotId?: bigint; - amountsClaimed?: bigint; -}; -export type SnapshotAndClaimByTokenBatchRequest = { - royaltyVaultIpId: Address; - currencyTokens: Address[]; - claimer?: Address; - txOptions?: TxOptions; -}; -export type SnapshotAndClaimByTokenBatchResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - snapshotId?: bigint; - amountsClaimed?: bigint; -}; -export type SnapshotAndClaimBySnapshotBatchRequest = { - royaltyVaultIpId: Address; - unclaimedSnapshotIds: bigint[] | number[] | string[]; - currencyTokens: Address[]; - claimer?: Address; - txOptions?: TxOptions; -}; - -export type SnapshotAndClaimBySnapshotBatchResponse = { - txHash?: string; - encodedTxData?: EncodedTxData; - snapshotId?: bigint; - amountsClaimed?: bigint; -}; - -/** - * Claims all revenue from the child IPs of an ancestor IP, then transfer - * all claimed tokens to the wallet if the wallet owns the IP or is the claimer. - * If claimed token is WIP, it will also be converted back to IP. - */ -export type ClaimAllRevenueRequest = { +export type ClaimAllRevenueRequest = WithClaimOptions & { /** The address of the ancestor IP from which the revenue is being claimed. */ ancestorIpId: Address; /** @@ -159,7 +53,9 @@ export type ClaimAllRevenueRequest = { royaltyPolicies: Address[]; /** The addresses of the currency tokens in which royalties will be claimed */ currencyTokens: Address[]; +}; +export type WithClaimOptions = { claimOptions?: { /** * When enabled, all claimed tokens on the claimer are transferred to the @@ -182,20 +78,41 @@ export type ClaimAllRevenueRequest = { autoUnwrapIpTokens?: boolean; }; }; +export type BatchClaimAllRevenueRequest = WithClaimOptions & { + /** The ancestor IPs from which the revenue is being claimed. */ + ancestorIps: (Omit & { + /** The address of the ancestor IP from which the revenue is being claimed. */ + ipId: Address; + })[]; + options?: { + /** + * Use multicall to batch the calls `claimAllRevenue` into one transaction when possible. + * + * If only 1 ancestorIp is provided, multicall will not be used. + * @default true + */ + useMulticallWhenPossible?: boolean; + }; +}; -export type ClaimedToken = { - token: Address; - amount: bigint; +export type BatchClaimAllRevenueResponse = { + txHashes: Hash[]; + receipts: TransactionReceipt[]; + claimedTokens?: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; }; export type ClaimAllRevenueResponse = { txHashes: Hash[]; - receipt?: TransactionReceipt; - claimedTokens?: ClaimedToken[]; + receipt: TransactionReceipt; + /** + * Aggregate list of all tokens claimed across all transactions in the batch. + * Events are aggregated by unique combinations of claimer and token addresses, + * summing up the amounts for the same claimer-token pairs. + */ + claimedTokens?: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; }; export type TransferClaimedTokensFromIpToWalletParams = { ipAccount: IpAccountImplClient; - skipUnwrapIp: boolean; - claimedTokens: ClaimedToken[]; + claimedTokens: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; }; diff --git a/packages/core-sdk/src/types/resources/wip.ts b/packages/core-sdk/src/types/resources/wip.ts index 21d6433fe..ec5076c01 100644 --- a/packages/core-sdk/src/types/resources/wip.ts +++ b/packages/core-sdk/src/types/resources/wip.ts @@ -17,3 +17,12 @@ export type DepositRequest = WithTxOptions & { export type WithdrawRequest = WithTxOptions & { amount: TokenAmountInput; }; + +export type TransferRequest = WithTxOptions & { + to: Address; + amount: TokenAmountInput; +}; + +export type TransferFromRequest = TransferRequest & { + from: Address; +}; diff --git a/packages/core-sdk/src/types/utils/erc20.ts b/packages/core-sdk/src/types/utils/erc20.ts deleted file mode 100644 index e04040b76..000000000 --- a/packages/core-sdk/src/types/utils/erc20.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Address, PublicClient } from "viem"; - -export type GetERC20BalanceParams = { - tokenAddress: Address; - walletAddress: Address; - rcpClient: PublicClient; -}; diff --git a/packages/core-sdk/src/types/utils/txOptions.ts b/packages/core-sdk/src/types/utils/txOptions.ts deleted file mode 100644 index 3cd332622..000000000 --- a/packages/core-sdk/src/types/utils/txOptions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Hex, PublicClient, TransactionReceipt } from "viem"; - -import { TxOptions } from "../options"; - -export type HandleTxOptionsParams = { - txHash: Hex; - txOptions?: TxOptions; - rpcClient: PublicClient; -}; - -export type HandleTxOptionsResponse = { - txHash: Hex; - - /** Transaction receipt, only available if waitForTransaction is set to true */ - receipt?: TransactionReceipt; -}; diff --git a/packages/core-sdk/src/types/utils/wip.ts b/packages/core-sdk/src/types/utils/wip.ts index 66927d2f1..dd3279adb 100644 --- a/packages/core-sdk/src/types/utils/wip.ts +++ b/packages/core-sdk/src/types/utils/wip.ts @@ -7,58 +7,36 @@ import { SimpleWalletClient, PiLicenseTemplateClient, LicensingModuleClient, - WrappedIpClient, + Erc20Client, } from "../../abi/generated"; -import { TxOptions } from "../options"; - -/** - * Options to override the default behavior of the auto wrapping IP - * and auto approve logic. - */ -export type WithWipOptions = { - /** options to configure WIP behavior */ - wipOptions?: { - /** - * Use multicall to batch the WIP calls into one transaction when possible. - * - * @default true - */ - useMulticallWhenPossible?: boolean; - - /** - * By default IP is converted to WIP if the current WIP - * balance does not cover the fees. - * Set this to `false` to disable this behavior. - * - * @default true - */ - enableAutoWrapIp?: boolean; - - /** - * Automatically approve WIP usage when WIP is needed but current allowance - * is not sufficient. - * Set this to `false` to disable this behavior. - * - * @default true - */ - enableAutoApprove?: boolean; - }; -}; +import { ERC20Options, TxOptions, WipOptions, WithWipOptions } from "../options"; +import { TokenClient, WipTokenClient } from "../../utils/token"; export type Multicall3ValueCall = Multicall3Aggregate3Request["calls"][0] & { value: bigint }; -export type WipSpender = { +export type Erc20Spender = { address: Address; /** - * Amount that the address will spend in WIP. + * Amount that the address will spend in erc20 token. * If not provided, then unlimited amount is assumed. */ amount?: bigint; }; -export type WipApprovalCall = { - spenders: WipSpender[]; - client: WrappedIpClient; +export type ApprovalCall = { + spenders: Erc20Spender[]; + client: TokenClient; + rpcClient: PublicClient; + /** owner is the address calling the approval */ + owner: Address; + /** when true, will return an array of {@link Multicall3ValueCall} */ + useMultiCall: boolean; + multicallAddress?: Address; +}; + +export type TokenApprovalCall = { + spenders: Erc20Spender[]; + client: Erc20Client; multicallAddress: Address; rpcClient: PublicClient; /** owner is the address calling the approval */ @@ -67,17 +45,21 @@ export type WipApprovalCall = { useMultiCall: boolean; }; -export type ContractCallWithWipFees = WithWipOptions & { +export type ContractCallWithFees = { totalFees: bigint; - multicall3Client: Multicall3Client; - wipClient: WrappedIpClient; - /** all possible spenders of the wip */ - wipSpenders: WipSpender[]; + multicall3Address: Address; + /** all possible spenders of the erc20 token */ + tokenSpenders: Erc20Spender[]; contractCall: () => Promise; encodedTxs: EncodedTxData[]; rpcClient: PublicClient; wallet: SimpleWalletClient; sender: Address; + options: { + wipOptions?: WipOptions; + erc20Options?: ERC20Options; + }; + token?: Address; txOptions?: TxOptions; }; @@ -85,9 +67,9 @@ export type MulticallWithWrapIp = WithWipOptions & { calls: Multicall3ValueCall[]; ipAmountToWrap: bigint; contractCall: () => Promise; - wipSpenders: WipSpender[]; - multicall3Client: Multicall3Client; - wipClient: WrappedIpClient; + wipSpenders: Erc20Spender[]; + multicall3Address: Address; + wipClient: WipTokenClient; rpcClient: PublicClient; wallet: SimpleWalletClient; }; diff --git a/packages/core-sdk/src/utils/chain.ts b/packages/core-sdk/src/utils/chain.ts index e2078bcd1..bfee6b9ac 100644 --- a/packages/core-sdk/src/utils/chain.ts +++ b/packages/core-sdk/src/utils/chain.ts @@ -18,7 +18,7 @@ export const aeneid = defineChain({ contracts: { multicall3: { address: "0xca11bde05977b3631167028862be2a173976ca11", - blockCreated: 5882, + blockCreated: 1792, }, }, testnet: true, @@ -42,7 +42,7 @@ export const mainnet = defineChain({ contracts: { multicall3: { address: "0xca11bde05977b3631167028862be2a173976ca11", - blockCreated: 5882, + blockCreated: 340998, }, }, testnet: false, diff --git a/packages/core-sdk/src/utils/erc20.ts b/packages/core-sdk/src/utils/erc20.ts deleted file mode 100644 index 7883df188..000000000 --- a/packages/core-sdk/src/utils/erc20.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { erc20Abi } from "viem"; - -import { GetERC20BalanceParams } from "../types/utils/erc20"; - -export async function getERC20Balance({ - rcpClient, - walletAddress, - tokenAddress, -}: GetERC20BalanceParams) { - return rcpClient.readContract({ - address: tokenAddress, - abi: erc20Abi, - functionName: "balanceOf", - args: [walletAddress], - }); -} diff --git a/packages/core-sdk/src/utils/wipFeeUtils.ts b/packages/core-sdk/src/utils/feeUtils.ts similarity index 66% rename from packages/core-sdk/src/utils/wipFeeUtils.ts rename to packages/core-sdk/src/utils/feeUtils.ts index 298e6e366..d657e5bf7 100644 --- a/packages/core-sdk/src/utils/wipFeeUtils.ts +++ b/packages/core-sdk/src/utils/feeUtils.ts @@ -4,15 +4,16 @@ import { multicall3Abi, SpgnftImplReadOnlyClient, wrappedIpAbi } from "../abi/ge import { WIP_TOKEN_ADDRESS } from "../constants/common"; import { getTokenAmountDisplay } from "./utils"; import { - WipApprovalCall, + ApprovalCall, Multicall3ValueCall, CalculateDerivativeMintFeeParams, MulticallWithWrapIp, - ContractCallWithWipFees, + ContractCallWithFees, } from "../types/utils/wip"; import { simulateAndWriteContract } from "./contract"; import { handleTxOptions } from "./txOptions"; -import { HandleTxOptionsResponse } from "../types/utils/txOptions"; +import { TransactionResponse } from "../types/options"; +import { ERC20Client, WipTokenClient } from "./token"; /** * check the allowance of all spenders and call approval if any spender @@ -26,7 +27,7 @@ const approvalAllSpenders = async ({ useMultiCall, rpcClient, multicallAddress, -}: WipApprovalCall) => { +}: ApprovalCall) => { const approvals = await Promise.all( spenders.map(async (spender) => { // make sure we never give approval to the multicall contract @@ -34,14 +35,11 @@ const approvalAllSpenders = async ({ return; } const spenderAmount = spender.amount || maxUint256; - const { result: allowance } = await client.allowance({ - owner: owner, - spender: spender.address, - }); + const allowance = await client.allowance(owner, spender.address); if (allowance < spenderAmount) { return { spender: spender.address, - amount: maxUint256, // approve max amount to avoid approvals in the future + value: maxUint256, // approve max amount to avoid approvals in the future }; } return; @@ -53,7 +51,7 @@ const approvalAllSpenders = async ({ if (!approval) { return; } - const encodedData = client.approveEncode(approval); + const encodedData = client.approveEncode(approval.spender, approval.value); allCalls.push({ target: encodedData.to, allowFailure: false, @@ -69,7 +67,7 @@ const approvalAllSpenders = async ({ if (!approval) { continue; } - const hash = await client.approve(approval); + const hash = await client.approve(approval.spender, approval.value); await rpcClient.waitForTransactionReceipt({ hash }); } return []; @@ -101,7 +99,7 @@ export const calculateSPGWipMintFee = async (spgNftClient: SpgnftImplReadOnlyCli const multiCallWrapIp = async ({ ipAmountToWrap, wipClient, - multicall3Client, + multicall3Address, wipSpenders, calls, rpcClient, @@ -130,7 +128,7 @@ const multiCallWrapIp = async ({ wallet: wallet, data: { abi: wrappedIpAbi, - address: WIP_TOKEN_ADDRESS, + address: wipClient.address, functionName: "deposit", value: ipAmountToWrap, }, @@ -143,8 +141,8 @@ const multiCallWrapIp = async ({ const approvalCalls = await approvalAllSpenders({ spenders: wipSpenders, client: wipClient, - multicallAddress: multicall3Client.address, - owner: useMultiCall ? multicall3Client.address : wallet.account!.address, + multicallAddress: multicall3Address, + owner: useMultiCall ? multicall3Address : wallet.account!.address, rpcClient, useMultiCall, }); @@ -164,7 +162,7 @@ const multiCallWrapIp = async ({ wallet: wallet, data: { abi: multicall3Abi, - address: multicall3Client.address, + address: multicall3Address, functionName: "aggregate3Value", args: [multiCalls], value: ipAmountToWrap, @@ -175,63 +173,54 @@ const multiCallWrapIp = async ({ }; /** - * Handle contract calls that require WIP fees by automatically wrapping IP to WIP when needed. + * Handle contract calls that require token fees. For fees in WIP, it automatically wraps IP to WIP when insufficient WIP balance. + * For all other ERC20 tokens, it handles approvals if insufficient allowance. * * @remarks * This function will automatically handle the following: * - * If the user does not have enough WIP, it will wrap IP to WIP, unless + * If token is wip and the user does not have enough WIP, it will wrap IP to WIP, unless * disabled via `disableAutoWrappingIp`. * - * If the user have enough WIP, it will check for if approvals are needed - * for each spender address and batch them in a multicall, unless disabled via - * `disableAutoApprove`. + * If the user have enough token, it will check for if approvals are needed + * for each spender address and approve it, unless disabled via `disableAutoApprove`. */ -export const contractCallWithWipFees = async ({ +export const contractCallWithFees = async ({ totalFees, - wipOptions, - multicall3Client, - rpcClient, - wipClient, + options, + multicall3Address, wallet, - wipSpenders, + tokenSpenders, contractCall, sender, txOptions, encodedTxs, -}: ContractCallWithWipFees): Promise => { - // if no fees, skip all WIP logic + rpcClient, + token, +}: ContractCallWithFees): Promise => { + const wipTokenClient = new WipTokenClient(rpcClient, wallet); + const isWip = token === wipTokenClient.address || token === undefined; + const selectedOptions = isWip ? options?.wipOptions : options.erc20Options; + const tokenClient = isWip ? wipTokenClient : new ERC20Client(rpcClient, wallet, token); + // if no fees, skip all logic if (totalFees === 0n) { const txHash = await contractCall(); return handleTxOptions({ rpcClient, txOptions, txHash }); } + const balance = await tokenClient.balanceOf(sender); + const autoApprove = selectedOptions?.enableAutoApprove !== false; - const wipBalanceOf = await wipClient.balanceOf({ - owner: sender, - }); - const wipBalance = wipBalanceOf.result; - const calls = encodedTxs.map((data) => ({ - target: data.to, - allowFailure: false, - value: 0n, - callData: data.data, - })); - - const autoApprove = wipOptions?.enableAutoApprove !== false; - const autoWrapIp = wipOptions?.enableAutoWrapIp !== false; - - // handle when there's enough WIP to cover all fees - if (wipBalance >= totalFees) { + // handle when there's enough token to cover all fees + if (balance >= totalFees) { if (autoApprove) { await approvalAllSpenders({ - spenders: wipSpenders, - client: wipClient, + spenders: tokenSpenders, + client: tokenClient, owner: sender, // sender owns the wip - multicallAddress: multicall3Client.address, + multicallAddress: multicall3Address, rpcClient, - // since sender has all wip, if using multicall, we will also need to transfer - // sender's wip to multicall, which brings more complexity. So in this case, - // we don't use multicall here and instead just wait for each approval to be finished. + // since sender has all token, if using multicall, we cannot approve transfer token into multicall by multicall. + // So in this case, we don't use multicall here and instead just wait for each approval to be finished. useMultiCall: false, }); } @@ -239,13 +228,21 @@ export const contractCallWithWipFees = async ({ return handleTxOptions({ rpcClient, txOptions, txHash }); } + if (!isWip) { + throw new Error( + `Wallet does not have enough erc20 token to pay for fees. Total fees: ${getTokenAmountDisplay( + totalFees, + )}, balance: ${getTokenAmountDisplay(balance)}.`, + ); + } + const autoWrapIp = options?.wipOptions?.enableAutoWrapIp !== false; const startingBalance = await rpcClient.getBalance({ address: sender }); // error if wallet does not have enough IP to cover fees if (startingBalance < totalFees) { throw new Error( `Wallet does not have enough IP to wrap to WIP and pay for fees. Total fees: ${getTokenAmountDisplay( totalFees, - )}, balance: ${getTokenAmountDisplay(startingBalance)}`, + )}, balance: ${getTokenAmountDisplay(startingBalance)}.`, ); } // error if there's enough IP to cover fees and we cannot wrap IP to WIP @@ -253,17 +250,22 @@ export const contractCallWithWipFees = async ({ throw new Error( `Wallet does not have enough WIP to pay for fees. Total fees: ${getTokenAmountDisplay( totalFees, - )}, balance: ${getTokenAmountDisplay(wipBalance, "WIP")}`, + )}, balance: ${getTokenAmountDisplay(balance, "WIP")}.`, ); } - + const calls = encodedTxs?.map((data) => ({ + target: data.to, + allowFailure: false, + value: 0n, + callData: data.data, + })); const { txHash } = await multiCallWrapIp({ ipAmountToWrap: totalFees, - multicall3Client, - wipClient, - wipOptions, + multicall3Address, + wipClient: wipTokenClient, + wipOptions: options?.wipOptions, contractCall, - wipSpenders, + wipSpenders: tokenSpenders, rpcClient, wallet, calls, diff --git a/packages/core-sdk/src/utils/getIpMetadataForWorkflow.ts b/packages/core-sdk/src/utils/getIpMetadataForWorkflow.ts index 964672feb..d27b739ad 100644 --- a/packages/core-sdk/src/utils/getIpMetadataForWorkflow.ts +++ b/packages/core-sdk/src/utils/getIpMetadataForWorkflow.ts @@ -1,9 +1,13 @@ import { Hex, zeroHash } from "viem"; export type IpMetadataForWorkflow = { + /** The URI of the metadata for the IP. */ ipMetadataURI: string; + /** The hash of the metadata for the IP. */ ipMetadataHash: Hex; + /** The URI of the metadata for the NFT. */ nftMetadataURI: string; + /** The hash of the metadata for the IP NFT. */ nftMetadataHash: Hex; }; diff --git a/packages/core-sdk/src/utils/licenseTermsHelper.ts b/packages/core-sdk/src/utils/licenseTermsHelper.ts index edc830a4c..333c57823 100644 --- a/packages/core-sdk/src/utils/licenseTermsHelper.ts +++ b/packages/core-sdk/src/utils/licenseTermsHelper.ts @@ -1,9 +1,10 @@ import { Address, PublicClient, zeroAddress } from "viem"; import { PIL_TYPE, LicenseTerms, RegisterPILTermsRequest } from "../types/resources/license"; -import { getAddress } from "./utils"; +import { validateAddress } from "./utils"; import { RoyaltyModuleReadOnlyClient } from "../abi/generated"; import { MAX_ROYALTY_TOKEN } from "../constants/common"; +import { RevShareType } from "../types/common"; export function getLicenseTermByType( type: PIL_TYPE, @@ -35,20 +36,23 @@ export function getLicenseTermByType( }; if (type === PIL_TYPE.NON_COMMERCIAL_REMIX) { licenseTerms.commercializerCheckerData = "0x"; + licenseTerms.uri = + "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json"; return licenseTerms; } else if (type === PIL_TYPE.COMMERCIAL_USE) { if (!term || term.defaultMintingFee === undefined || term.currency === undefined) { throw new Error("DefaultMintingFee, currency are required for commercial use PIL."); } - licenseTerms.royaltyPolicy = getAddress( - term.royaltyPolicyAddress, - "term.royaltyPolicyLAPAddress", - ); + licenseTerms.royaltyPolicy = validateAddress(term.royaltyPolicyAddress); licenseTerms.defaultMintingFee = BigInt(term.defaultMintingFee); licenseTerms.commercialUse = true; licenseTerms.commercialAttribution = true; licenseTerms.derivativesReciprocal = false; - licenseTerms.currency = getAddress(term.currency, "term.currency"); + licenseTerms.currency = validateAddress(term.currency); + licenseTerms.uri = + "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json"; + licenseTerms.derivativesAllowed = false; + licenseTerms.derivativesAttribution = false; return licenseTerms; } else { if ( @@ -61,17 +65,15 @@ export function getLicenseTermByType( "DefaultMintingFee, currency and commercialRevShare are required for commercial remix PIL.", ); } - licenseTerms.royaltyPolicy = getAddress( - term.royaltyPolicyAddress, - "term.royaltyPolicyLAPAddress", - ); + licenseTerms.royaltyPolicy = validateAddress(term.royaltyPolicyAddress); licenseTerms.defaultMintingFee = BigInt(term.defaultMintingFee); licenseTerms.commercialUse = true; licenseTerms.commercialAttribution = true; - + licenseTerms.uri = + "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json"; licenseTerms.commercialRevShare = getRevenueShare(term.commercialRevShare); licenseTerms.derivativesReciprocal = true; - licenseTerms.currency = getAddress(term.currency, "term.currency"); + licenseTerms.currency = validateAddress(term.currency); return licenseTerms; } } @@ -82,14 +84,14 @@ export async function validateLicenseTerms( ): Promise { const { royaltyPolicy, currency } = params; const royaltyModuleReadOnlyClient = new RoyaltyModuleReadOnlyClient(rpcClient); - if (getAddress(royaltyPolicy, "params.royaltyPolicy") !== zeroAddress) { + if (validateAddress(royaltyPolicy) !== zeroAddress) { const isWhitelistedArbitrationPolicy = await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyPolicy({ royaltyPolicy }); if (!isWhitelistedArbitrationPolicy) { throw new Error("The royalty policy is not whitelisted."); } } - if (getAddress(currency, "params.currency") !== zeroAddress) { + if (validateAddress(currency) !== zeroAddress) { const isWhitelistedRoyaltyToken = await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken({ token: currency, }); @@ -169,13 +171,16 @@ const verifyDerivatives = (terms: LicenseTerms) => { } }; -export const getRevenueShare = (revShare: number | string) => { +export const getRevenueShare = ( + revShare: number | string, + type: RevShareType = RevShareType.COMMERCIAL_REVENUE_SHARE, +) => { const revShareNumber = Number(revShare); if (isNaN(revShareNumber)) { - throw new Error("CommercialRevShare must be a valid number."); + throw new Error(`${type} must be a valid number.`); } if (revShareNumber < 0 || revShareNumber > 100) { - throw new Error("CommercialRevShare should be between 0 and 100."); + throw new Error(`${type} must be between 0 and 100.`); } return (revShareNumber / 100) * MAX_ROYALTY_TOKEN; }; diff --git a/packages/core-sdk/src/utils/token.ts b/packages/core-sdk/src/utils/token.ts new file mode 100644 index 000000000..ecb63b47f --- /dev/null +++ b/packages/core-sdk/src/utils/token.ts @@ -0,0 +1,72 @@ +import { Address, Hash, PublicClient } from "viem"; + +import { EncodedTxData, Erc20Client, SimpleWalletClient, WrappedIpClient } from "../abi/generated"; + +export interface TokenClient { + balanceOf(account: Address): Promise; + allowance(owner: string, spender: string): Promise; + approve(spender: string, value: bigint): Promise; + approveEncode(spender: Address, value: bigint): EncodedTxData; +} + +export class ERC20Client implements TokenClient { + private ercClient: Erc20Client; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address: Address) { + this.ercClient = new Erc20Client(rpcClient, wallet, address); + } + + async balanceOf(account: Address): Promise { + return await this.ercClient.balanceOf({ account }); + } + + async allowance(owner: Address, spender: Address): Promise { + return await this.ercClient.allowance({ owner, spender }); + } + + async approve(spender: Address, value: bigint): Promise { + return await this.ercClient.approve({ spender, value }); + } + + approveEncode(spender: Address, value: bigint): EncodedTxData { + return this.ercClient.approveEncode({ spender, value }); + } + // The method only will work in test environment + async mint(to: Address, amount: bigint) { + return await this.ercClient.mint({ to, amount }); + } +} + +export class WipTokenClient implements TokenClient { + private wipClient: WrappedIpClient; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { + this.wipClient = new WrappedIpClient(rpcClient, wallet); + } + + async balanceOf(account: Address): Promise { + const { result: balance } = await this.wipClient.balanceOf({ owner: account }); + return balance; + } + + async allowance(owner: Address, spender: Address): Promise { + const { result: allowance } = await this.wipClient.allowance({ owner, spender }); + return allowance; + } + + async approve(spender: Address, value: bigint): Promise { + return await this.wipClient.approve({ spender, amount: value }); + } + + approveEncode(spender: Address, value: bigint): EncodedTxData { + return this.wipClient.approveEncode({ spender, amount: value }); + } + + depositEncode(): EncodedTxData { + return this.wipClient.depositEncode(); + } + + get address(): Address { + return this.wipClient.address; + } +} diff --git a/packages/core-sdk/src/utils/txOptions.ts b/packages/core-sdk/src/utils/txOptions.ts index 7728cdf51..5a92c1023 100644 --- a/packages/core-sdk/src/utils/txOptions.ts +++ b/packages/core-sdk/src/utils/txOptions.ts @@ -1,10 +1,10 @@ -import { HandleTxOptionsParams, HandleTxOptionsResponse } from "../types/utils/txOptions"; +import { HandleTxOptionsParams, TransactionResponse } from "../types/options"; export async function handleTxOptions({ txOptions, rpcClient, txHash, -}: HandleTxOptionsParams): Promise { +}: HandleTxOptionsParams): Promise { if (!txOptions || !txOptions.waitForTransaction) { return { txHash }; } diff --git a/packages/core-sdk/src/utils/utils.ts b/packages/core-sdk/src/utils/utils.ts index 2b928f6ce..21657cf75 100644 --- a/packages/core-sdk/src/utils/utils.ts +++ b/packages/core-sdk/src/utils/utils.ts @@ -100,7 +100,7 @@ export const chain: { [key in SupportedChainIds]: "1315" | "1514" } = { export function validateAddress(address: string): Address { if (!isAddress(address, { strict: false })) { - throw Error(`Invalid address: ${address}`); + throw Error(`Invalid address: ${address}.`); } return address; } diff --git a/packages/core-sdk/src/utils/validateLicenseConfig.ts b/packages/core-sdk/src/utils/validateLicenseConfig.ts index f3ccf2d24..62f68eba3 100644 --- a/packages/core-sdk/src/utils/validateLicenseConfig.ts +++ b/packages/core-sdk/src/utils/validateLicenseConfig.ts @@ -1,19 +1,33 @@ -import { LicensingConfig } from "../types/common"; -import { InnerLicensingConfig } from "../types/resources/license"; +import { zeroAddress } from "viem"; + +import { LicensingConfig, ValidatedLicensingConfig } from "../types/common"; import { getRevenueShare } from "./licenseTermsHelper"; -import { getAddress } from "./utils"; +import { validateAddress } from "./utils"; -export const validateLicenseConfig = (licensingConfig: LicensingConfig): InnerLicensingConfig => { +export const validateLicenseConfig = ( + licensingConfig?: LicensingConfig, +): ValidatedLicensingConfig => { + if (!licensingConfig) { + return { + isSet: false, + mintingFee: 0n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }; + } const licenseConfig = { - ...licensingConfig, expectMinimumGroupRewardShare: Number(licensingConfig.expectMinimumGroupRewardShare), commercialRevShare: getRevenueShare(licensingConfig.commercialRevShare), mintingFee: BigInt(licensingConfig.mintingFee), - expectGroupRewardPool: getAddress( - licensingConfig.expectGroupRewardPool, - "licensingConfig.expectGroupRewardPool", - ), - licensingHook: getAddress(licensingConfig.licensingHook, "licensingConfig.licensingHook"), + expectGroupRewardPool: validateAddress(licensingConfig.expectGroupRewardPool), + licensingHook: validateAddress(licensingConfig.licensingHook), + hookData: licensingConfig.hookData, + isSet: licensingConfig.isSet, + disabled: licensingConfig.disabled, } as const; if (isNaN(licenseConfig.expectMinimumGroupRewardShare)) { throw new Error(`The expectMinimumGroupRewardShare must be a valid number.`); diff --git a/packages/core-sdk/test/integration/dispute.test.ts b/packages/core-sdk/test/integration/dispute.test.ts index aad7e51dd..3de7a583d 100644 --- a/packages/core-sdk/test/integration/dispute.test.ts +++ b/packages/core-sdk/test/integration/dispute.test.ts @@ -1,29 +1,100 @@ import chai from "chai"; import { StoryClient } from "../../src"; import { RaiseDisputeRequest } from "../../src/index"; -import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; +import { + mockERC721, + getStoryClient, + getTokenId, + publicClient, + aeneid, + RPC, + TEST_WALLET_ADDRESS, + walletClient, +} from "./utils/util"; import chaiAsPromised from "chai-as-promised"; -import { Address } from "viem"; -import { MockERC20 } from "./utils/mockERC20"; -import { arbitrationPolicyUmaAddress, wrappedIpAddress } from "../../src/abi/generated"; +import { + Address, + WalletClient, + createWalletClient, + http, + maxUint256, + parseEther, + zeroAddress, +} from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { + arbitrationPolicyUmaAddress, + disputeModuleAddress, + evenSplitGroupPoolAddress, + royaltyPolicyLapAddress, + wrappedIpAddress, +} from "../../src/abi/generated"; +import { chainStringToViemChain } from "../../src/utils/utils"; +import { disputeModuleAbi } from "../../src/abi/generated"; +import { CID } from "multiformats/cid"; +import * as sha256 from "multiformats/hashes/sha2"; +import { WipTokenClient } from "../../src/utils/token"; + const expect = chai.expect; chai.use(chaiAsPromised); +const DISPUTE_MODULE_ADDRESS = disputeModuleAddress[aeneid]; +const SET_DISPUTE_JUDGEMENT_ABI = disputeModuleAbi.find( + (item) => item.type === "function" && item.name === "setDisputeJudgement", +); + +const generateCID = async () => { + // Generate a random 32-byte buffer + const randomBytes = crypto.getRandomValues(new Uint8Array(32)); + // Hash the bytes using SHA-256 + const hash = await sha256.sha256.digest(randomBytes); + // Create a CIDv1 in dag-pb format + const cidv1 = CID.createV1(0x70, hash); // 0x70 = dag-pb codec + // Convert CIDv1 to CIDv0 (Base58-encoded) + return cidv1.toV0().toString(); +}; + describe("Dispute Functions", () => { let clientA: StoryClient; let clientB: StoryClient; let ipIdB: Address; before(async () => { + const privateKey = generatePrivateKey(); clientA = getStoryClient(); - clientB = getStoryClient(); - const mockERC20 = new MockERC20(wrappedIpAddress[aeneid]); - await mockERC20.approve(arbitrationPolicyUmaAddress[aeneid]); - const tokenId = await getTokenId(); + clientB = getStoryClient(privateKey); + const walletB = privateKeyToAccount(privateKey); + + // ClientA transfer some funds to walletB + const clientAWalletClient = createWalletClient({ + chain: chainStringToViemChain("aeneid"), + transport: http(RPC), + account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Address), + }); + const txHash = await clientAWalletClient.sendTransaction({ + to: walletB.address, + value: parseEther("0.25"), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + // clientA approves the arbitration policyUma module to spend the some tokens + const mockERC20 = new WipTokenClient(publicClient, walletClient); + await mockERC20.approve(arbitrationPolicyUmaAddress[aeneid], maxUint256); + + const txData = await clientA.nftClient.createNFTCollection({ + name: "test-collection", + symbol: "TEST", + maxSupply: 100, + isPublicMinting: true, + mintOpen: true, + contractURI: "test-uri", + mintFeeRecipient: TEST_WALLET_ADDRESS, + txOptions: { waitForTransaction: true }, + }); + const nftContract = txData.spgNftContract!; ipIdB = ( - await clientB.ipAsset.register({ - nftContract: mockERC721, - tokenId: tokenId!, + await clientB.ipAsset.mintAndRegisterIp({ + spgNftContract: nftContract, txOptions: { waitForTransaction: true, }, @@ -31,86 +102,105 @@ describe("Dispute Functions", () => { ).ipId!; }); - it.skip("should raise a dispute", async () => { - const raiseDisputeRequest: RaiseDisputeRequest = { - targetIpId: ipIdB, - cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - targetTag: "IMPROPER_REGISTRATION", - liveness: 2592000, - bond: 0, - txOptions: { - waitForTransaction: true, - }, - }; - const response = await clientA.dispute.raiseDispute(raiseDisputeRequest); - expect(response.txHash).to.be.a("string").and.not.empty; - expect(response.disputeId).to.be.a("bigint"); - }); + describe("raiseDispute and counter dispute", () => { + let disputeId: bigint | undefined; + it("should raise a dispute", async () => { + const raiseDisputeRequest: RaiseDisputeRequest = { + targetIpId: ipIdB, + cid: await generateCID(), + targetTag: "IMPROPER_REGISTRATION", + liveness: 2592000, + bond: 0, + txOptions: { + waitForTransaction: true, + }, + }; + const response = await clientA.dispute.raiseDispute(raiseDisputeRequest); + expect(response.txHash).to.be.a("string").and.not.empty; + expect(response.disputeId).to.be.a("bigint"); + disputeId = response.disputeId; + }); + it("should be able to counter existing dispute once", async () => { + const assertionId = await clientB.dispute.disputeIdToAssertionId(disputeId!); + const counterEvidenceCID = await generateCID(); + const ret = await clientB.dispute.disputeAssertion({ + ipId: ipIdB, + assertionId, + counterEvidenceCID, + }); + expect(ret.txHash).to.be.a("string").and.not.empty; - it("should throw error when liveness is out of bounds", async () => { - const minLiveness = await clientA.dispute.arbitrationPolicyUmaReadOnlyClient.minLiveness(); - const raiseDisputeRequest: RaiseDisputeRequest = { - targetIpId: ipIdB, - cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - targetTag: "IMPROPER_REGISTRATION", - liveness: Number(minLiveness) - 1, // Below minimum - bond: 0, - txOptions: { waitForTransaction: true }, - }; + // should throw error if attempting to dispute assertion again + const secondDispute = await clientB.dispute.disputeAssertion({ + ipId: ipIdB, + assertionId, + counterEvidenceCID, + txOptions: { + waitForTransaction: true, + }, + }); + expect(secondDispute.receipt?.status).to.equal("reverted"); + }); - await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( - `Liveness must be between`, - ); - }); + it("should throw error when liveness is out of bounds", async () => { + const minLiveness = await clientA.dispute.arbitrationPolicyUmaClient.minLiveness(); + const raiseDisputeRequest: RaiseDisputeRequest = { + targetIpId: ipIdB, + cid: await generateCID(), + targetTag: "IMPROPER_REGISTRATION", + liveness: Number(minLiveness) - 1, + bond: 0, + txOptions: { waitForTransaction: true }, + }; - it("should throw error when bond exceeds maximum", async () => { - const maxBonds = await clientA.dispute.arbitrationPolicyUmaReadOnlyClient.maxBonds({ - token: wrappedIpAddress[aeneid], + await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( + `Liveness must be between`, + ); }); - const raiseDisputeRequest: RaiseDisputeRequest = { - targetIpId: ipIdB, - cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - targetTag: "IMPROPER_REGISTRATION", - liveness: 2592000, - bond: 2000000000000000000, - txOptions: { - waitForTransaction: true, - }, - }; - - await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( - `Bonds must be less than`, - ); - }); + it("should throw error when bond exceeds maximum", async () => { + const raiseDisputeRequest: RaiseDisputeRequest = { + targetIpId: ipIdB, + cid: await generateCID(), + targetTag: "IMPROPER_REGISTRATION", + liveness: 2592000, + bond: 2000000000000000000, + txOptions: { + waitForTransaction: true, + }, + }; - it("should throw error for non-whitelisted dispute tag", async () => { - const raiseDisputeRequest: RaiseDisputeRequest = { - targetIpId: ipIdB, - cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - targetTag: "INVALID_TAG", - liveness: 2592000, - bond: 0, - txOptions: { waitForTransaction: true }, - }; + await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( + `Bonds must be less than`, + ); + }); + + it("should throw error for non-whitelisted dispute tag", async () => { + const raiseDisputeRequest: RaiseDisputeRequest = { + targetIpId: ipIdB, + cid: await generateCID(), + targetTag: "INVALID_TAG", + liveness: 2592000, + bond: 0, + txOptions: { waitForTransaction: true }, + }; - await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( - `The target tag INVALID_TAG is not whitelisted`, - ); + await expect(clientA.dispute.raiseDispute(raiseDisputeRequest)).to.be.rejectedWith( + `The target tag INVALID_TAG is not whitelisted`, + ); + }); }); - it.skip("it should not cancel a dispute (yet)", async () => { - // First raise a dispute + it("it should not cancel a dispute (yet)", async () => { const raiseResponse = await clientA.dispute.raiseDispute({ targetIpId: ipIdB, - cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", + cid: await generateCID(), targetTag: "IMPROPER_REGISTRATION", liveness: 2592000, bond: 0, txOptions: { waitForTransaction: true }, }); - // Then you shouldnn't be able to cancel it expect( clientA.dispute.cancelDispute({ disputeId: raiseResponse.disputeId!, @@ -118,4 +208,429 @@ describe("Dispute Functions", () => { }), ).to.be.rejected; }); + + /** + * Setup for dispute resolution testing + * + * On mainnet, disputes are judged by UMA's optimistic oracle. For testing purposes, + * we simulate this process by setting up a whitelisted judge account that can + * directly set dispute judgements. The process creates a wallet client with the + * whitelisted judge account, after which a user raises a dispute through the dispute + * module. The judge account then sets the dispute judgement (simulating UMA's role), + * and finally the dispute can be resolved based on this judgement. + */ + describe("Dispute resolution", () => { + let disputeId: bigint; + let nftContract: Address; + let parentIpId: Address; + let licenseTermsId: bigint; + let childIpId: Address; + let childIpId2: Address; + let judgeWalletClient: WalletClient; + + before(async function (this: Mocha.Context) { + // Skip tests if whitelisted judge private key is not configured + if (!process.env.JUDGE_PRIVATE_KEY) { + this.skip(); + } + // Set up judge wallet client using whitelisted account + judgeWalletClient = createWalletClient({ + chain: chainStringToViemChain("aeneid"), + transport: http(RPC), + account: privateKeyToAccount(process.env.JUDGE_PRIVATE_KEY as Address), + }); + }); + + beforeEach(async function (this: Mocha.Context) { + // Setup NFT collection + const txData = await clientA.nftClient.createNFTCollection({ + name: "test-collection", + symbol: "TEST", + maxSupply: 100, + isPublicMinting: true, + mintOpen: true, + contractURI: "test-uri", + mintFeeRecipient: TEST_WALLET_ADDRESS, + txOptions: { waitForTransaction: true }, + }); + nftContract = txData.spgNftContract!; + + // Get parent IP ID and license terms ID + const ipIdAndLicenseResponse = await clientA.ipAsset.mintAndRegisterIpAssetWithPilTerms({ + spgNftContract: nftContract, + allowDuplicates: false, + licenseTermsData: [ + { + terms: { + transferable: true, + royaltyPolicy: royaltyPolicyLapAddress[aeneid], + defaultMintingFee: 0n, + expiration: 0n, + commercialUse: true, + commercialAttribution: false, + commercializerChecker: zeroAddress, + commercializerCheckerData: zeroAddress, + commercialRevShare: 90, + commercialRevCeiling: 0n, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCeiling: 0n, + currency: wrappedIpAddress[aeneid], + uri: "", + }, + licensingConfig: { + isSet: true, + mintingFee: 0n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: evenSplitGroupPoolAddress[aeneid], + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + parentIpId = ipIdAndLicenseResponse.ipId!; + licenseTermsId = ipIdAndLicenseResponse.licenseTermsIds![0]; + + //Create a derivative ip + const derivativeIpIdResponse1 = await clientA.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContract, + derivData: { + parentIpIds: [parentIpId!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 1n, + maxRts: 5 * 10 ** 6, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + childIpId = derivativeIpIdResponse1.ipId!; + + // Create a second derivative ip + const derivativeIpIdResponse2 = await clientA.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContract, + derivData: { + parentIpIds: [parentIpId!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 1n, + maxRts: 5 * 10 ** 6, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + childIpId2 = derivativeIpIdResponse2.ipId!; + + // Raise a dispute + const response = await clientA.dispute.raiseDispute({ + targetIpId: parentIpId, + cid: await generateCID(), + targetTag: "IMPROPER_REGISTRATION", + liveness: 2592000, + bond: 0, + txOptions: { + waitForTransaction: true, + }, + }); + disputeId = response.disputeId!; + }); + + it("should tag infringing ip", async () => { + // Step 1: Judge sets dispute judgement + // This simulates UMA's role on mainnet by directly setting the judgement + const { request } = await publicClient.simulateContract({ + address: DISPUTE_MODULE_ADDRESS, + abi: [SET_DISPUTE_JUDGEMENT_ABI], + functionName: "setDisputeJudgement", + args: [disputeId, true, "0x"], + account: judgeWalletClient.account!, + }); + const txHash = await judgeWalletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + // Step 2: Tag derivative IP as infringing + const results = await clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: childIpId, + disputeId: disputeId, + }, + { + ipId: childIpId2, + disputeId: disputeId, + }, + ], + txOptions: { waitForTransaction: true }, + }); + expect(results[0].txHash).to.be.a("string").and.not.empty; + }); + + it("should tag a single IP as infringing without using multicall", async () => { + /** + * Test Flow: + * 1. Set judgment on an existing dispute to mark it as valid + * 2. Verify the dispute state changed correctly after judgment + * 3. Try to tag a derivative IP using the judged dispute + */ + + // Step 1: Set dispute judgment using the judge wallet + // When judgment is true, the dispute's currentTag will be set to the targetTag + // When false, currentTag would be set to bytes32(0) + const { request } = await publicClient.simulateContract({ + address: DISPUTE_MODULE_ADDRESS, + abi: [SET_DISPUTE_JUDGEMENT_ABI], + functionName: "setDisputeJudgement", + args: [disputeId, true, "0x"], + account: judgeWalletClient.account!, + }); + const judgmentTxHash = await judgeWalletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: judgmentTxHash }); + + // Step 2: Verify dispute state + // The disputes() function returns multiple values about the dispute: + // - targetTag: the tag we wanted to apply when raising the dispute + // - currentTag: the current state of the dispute after judgment + // After a successful judgment, currentTag should equal targetTag + const [ + _targetIpId, // IP being disputed + _disputeInitiator, // Address that raised the dispute + _disputeTimestamp, // When dispute was raised + _arbitrationPolicy, // Policy used for arbitration + _disputeEvidenceHash, // Evidence hash for dispute + targetTag, // Tag we want to apply (e.g. "IMPROPER_REGISTRATION") + currentTag, // Current state of dispute + _infringerDisputeId, // Related dispute ID if this is a propagated tag + ] = await publicClient.readContract({ + address: disputeModuleAddress[aeneid], + abi: disputeModuleAbi, + functionName: "disputes", + args: [disputeId], + }); + expect(currentTag).to.equal(targetTag); // Verify judgment was recorded correctly + + // Step 3: Attempt to tag a derivative IP + // This will fail if: + // - The dispute is not in a valid state (still IN_DISPUTE or cleared) + // - The IP we're trying to tag is not actually a derivative of the disputed IP + // - The dispute has already been used to tag this IP + const response = await clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: childIpId, // The derivative IP to tag + disputeId: disputeId, // Using the judged dispute as basis for tagging + }, + ], + options: { + useMulticallWhenPossible: false, // Force single transaction instead of batch + }, + txOptions: { waitForTransaction: true }, + }); + + // Verify we got the expected response + expect(response).to.have.lengthOf(1); + expect(response[0].txHash).to.be.a("string").and.not.empty; + }); + + it("should tag multiple IPs as infringing using multicall", async () => { + const disputeResponse = await clientA.dispute.raiseDispute({ + targetIpId: parentIpId, + cid: await generateCID(), + targetTag: "IMPROPER_REGISTRATION", + liveness: 2592000, + bond: 0, + txOptions: { waitForTransaction: true }, + }); + const testDisputeId = disputeResponse.disputeId!; + + const derivativeResponse2 = await clientA.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContract, + derivData: { + parentIpIds: [parentIpId!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 1n, + maxRts: 5 * 10 ** 6, + maxRevenueShare: 100, + }, + allowDuplicates: true, + txOptions: { waitForTransaction: true }, + }); + const childIpId2 = derivativeResponse2.ipId!; + + const { request } = await publicClient.simulateContract({ + address: DISPUTE_MODULE_ADDRESS, + abi: [SET_DISPUTE_JUDGEMENT_ABI], + functionName: "setDisputeJudgement", + args: [testDisputeId, true, "0x"], + account: judgeWalletClient.account!, + }); + const judgmentTxHash = await judgeWalletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: judgmentTxHash }); + + const disputeState = await publicClient.readContract({ + address: disputeModuleAddress[aeneid], + abi: disputeModuleAbi, + functionName: "disputes", + args: [testDisputeId], + }); + expect(disputeState[6]).to.equal(disputeState[5]); + + const response = await clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: childIpId, + disputeId: testDisputeId, + }, + { + ipId: childIpId2, + disputeId: testDisputeId, + }, + ], + options: { + useMulticallWhenPossible: true, + }, + txOptions: { waitForTransaction: true }, + }); + + expect(response).to.have.lengthOf(1); + expect(response[0].txHash).to.be.a("string").and.not.empty; + }); + + it("should tag multiple IPs without multicall when specified", async () => { + // Create two new derivative IPs sequentially + const derivativeResponse3 = await clientA.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContract, + derivData: { + parentIpIds: [parentIpId!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 1n, + maxRts: 5 * 10 ** 6, + maxRevenueShare: 100, + }, + allowDuplicates: true, + txOptions: { waitForTransaction: true }, + }); + + const derivativeResponse4 = await clientA.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContract, + derivData: { + parentIpIds: [parentIpId!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 1n, + maxRts: 5 * 10 ** 6, + maxRevenueShare: 100, + }, + allowDuplicates: true, + txOptions: { waitForTransaction: true }, + }); + + const { request } = await publicClient.simulateContract({ + address: DISPUTE_MODULE_ADDRESS, + abi: [SET_DISPUTE_JUDGEMENT_ABI], + functionName: "setDisputeJudgement", + args: [disputeId, true, "0x"], + account: judgeWalletClient.account!, + }); + const judgmentTxHash = await judgeWalletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: judgmentTxHash }); + + const disputeState = await publicClient.readContract({ + address: disputeModuleAddress[aeneid], + abi: disputeModuleAbi, + functionName: "disputes", + args: [disputeId], + }); + expect(disputeState[6]).to.equal(disputeState[5]); + + const response1 = await clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: derivativeResponse3.ipId!, + disputeId: disputeId, + }, + ], + options: { + useMulticallWhenPossible: false, + }, + txOptions: { waitForTransaction: true }, + }); + + const response2 = await clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: derivativeResponse4.ipId!, + disputeId: disputeId, + }, + ], + options: { + useMulticallWhenPossible: false, + }, + txOptions: { waitForTransaction: true }, + }); + + const responses = [...response1, ...response2]; + expect(responses).to.have.lengthOf(2); + expect(responses[0].txHash).to.be.a("string").and.not.empty; + expect(responses[1].txHash).to.be.a("string").and.not.empty; + }); + + it("should fail when trying to tag with invalid dispute ID", async () => { + await expect( + clientA.dispute.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: childIpId, + disputeId: 999999n, + }, + ], + txOptions: { waitForTransaction: true }, + }), + ).to.be.rejected; + }); + + it("should resolve a dispute successfully when initiated by dispute initiator", async () => { + // First set judgment + const { request } = await publicClient.simulateContract({ + address: DISPUTE_MODULE_ADDRESS, + abi: [SET_DISPUTE_JUDGEMENT_ABI], + functionName: "setDisputeJudgement", + args: [disputeId, true, "0x"], + account: judgeWalletClient.account!, + }); + const judgmentTxHash = await judgeWalletClient.writeContract(request); + await publicClient.waitForTransactionReceipt({ hash: judgmentTxHash }); + + const disputeState = await publicClient.readContract({ + address: disputeModuleAddress[aeneid], + abi: disputeModuleAbi, + functionName: "disputes", + args: [disputeId], + }); + expect(disputeState[6]).to.equal(disputeState[5]); + + const response = await clientA.dispute.resolveDispute({ + disputeId: disputeId, + data: "0x", + txOptions: { + waitForTransaction: true, + }, + }); + expect(response.txHash).to.be.a("string").and.not.empty; + }); + + it("should fail when non-initiator tries to resolve the dispute", async () => { + await expect( + clientB.dispute.resolveDispute({ + disputeId: disputeId, + data: "0x", + txOptions: { + waitForTransaction: true, + }, + }), + ).to.be.rejectedWith("NotDisputeInitiator"); + }); + }); }); diff --git a/packages/core-sdk/test/integration/group.test.ts b/packages/core-sdk/test/integration/group.test.ts index dfff9fd50..f1310a66b 100644 --- a/packages/core-sdk/test/integration/group.test.ts +++ b/packages/core-sdk/test/integration/group.test.ts @@ -167,7 +167,6 @@ describe("Group Functions", () => { }, }, ], - allowDuplicates: true, maxAllowedRewardShare: 5, txOptions: { waitForTransaction: true }, }); diff --git a/packages/core-sdk/test/integration/ipAccount.test.ts b/packages/core-sdk/test/integration/ipAccount.test.ts index abd0a7990..23a1761ff 100644 --- a/packages/core-sdk/test/integration/ipAccount.test.ts +++ b/packages/core-sdk/test/integration/ipAccount.test.ts @@ -2,7 +2,7 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { AccessPermission, StoryClient } from "../../src"; import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; -import { Hex, encodeFunctionData, getAddress, toFunctionSelector } from "viem"; +import { Hex, encodeFunctionData, getAddress, toFunctionSelector, toHex } from "viem"; import { accessControllerAbi, accessControllerAddress, @@ -150,4 +150,13 @@ describe("IPAccount Functions", () => { .rejected; }); }); + + it("should successfully set ip metadata", async () => { + const txHash = await client.ipAccount.setIpMetadata({ + ipId: ipId, + metadataURI: "https://example.com", + metadataHash: toHex("test", { size: 32 }), + }); + expect(txHash).to.be.a("string").and.not.empty; + }); }); diff --git a/packages/core-sdk/test/integration/ipAsset.test.ts b/packages/core-sdk/test/integration/ipAsset.test.ts index bb288ccab..948e29522 100644 --- a/packages/core-sdk/test/integration/ipAsset.test.ts +++ b/packages/core-sdk/test/integration/ipAsset.test.ts @@ -1,7 +1,7 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { StoryClient } from "../../src"; -import { Address, Hex, toHex, zeroAddress, zeroHash } from "viem"; +import { Address, Hex, maxUint256, toHex, zeroAddress, zeroHash } from "viem"; import { mockERC721, getStoryClient, @@ -9,17 +9,19 @@ import { mintBySpg, approveForLicenseToken, aeneid, + publicClient, + walletClient, } from "./utils/util"; -import { MockERC20 } from "./utils/mockERC20"; import { evenSplitGroupPoolAddress, royaltyPolicyLapAddress, derivativeWorkflowsAddress, royaltyTokenDistributionWorkflowsAddress, wrappedIpAddress, - mockErc20Address, + erc20Address, } from "../../src/abi/generated"; import { MAX_ROYALTY_TOKEN, WIP_TOKEN_ADDRESS } from "../../src/constants/common"; +import { ERC20Client } from "../../src/utils/token"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -290,10 +292,10 @@ describe("IP Asset Functions", () => { licenseTermsId = result.licenseTermsIds![0]; // Setup ERC20 - const mockERC20 = new MockERC20(); - await mockERC20.approve(derivativeWorkflowsAddress[aeneid]); - await mockERC20.approve(royaltyTokenDistributionWorkflowsAddress[aeneid]); - await mockERC20.mint(); + const mockERC20 = new ERC20Client(publicClient, walletClient, erc20Address[aeneid]); + await mockERC20.approve(derivativeWorkflowsAddress[aeneid], maxUint256); + await mockERC20.approve(royaltyTokenDistributionWorkflowsAddress[aeneid], maxUint256); + await mockERC20.mint(walletAddress, 100000n); }); it("should register IP Asset with metadata", async () => { @@ -417,7 +419,6 @@ describe("IP Asset Functions", () => { maxRts: 5 * 10 ** 6, maxRevenueShare: 100, }, - allowDuplicates: true, txOptions: { waitForTransaction: true }, }); expect(result.txHash).to.be.a("string").and.not.empty; @@ -501,7 +502,6 @@ describe("IP Asset Functions", () => { spgNftContract: nftContract, licenseTokenIds: [mintLicenseTokensResult.licenseTokenIds![0]], maxRts: 5 * 10 ** 6, - allowDuplicates: true, ipMetadata: { ipMetadataURI: "test-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), @@ -633,7 +633,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -705,7 +705,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -769,7 +769,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -799,7 +799,7 @@ describe("IP Asset Functions", () => { ).to.be.rejectedWith("The sum of the royalty shares cannot exceeds 100"); }); - it("should fail with non-commercial license terms for royalty distributio", async () => { + it("should fail with non-commercial license terms for royalty distribution", async () => { const tokenId = await getTokenId(); await expect( client.ipAsset.registerIPAndAttachLicenseTermsAndDistributeRoyaltyTokens({ @@ -823,7 +823,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -877,7 +877,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -932,11 +932,10 @@ describe("IP Asset Functions", () => { expect(result.tokenId).to.be.a("bigint"); }); - it("should mint and register IP and attach PIL terms and distribute royalty tokens", async () => { + it("should mint and register IP and attach PIL terms and distribute royalty tokens without licensing config", async () => { const result = await client.ipAsset.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens({ spgNftContract: nftContract, - allowDuplicates: true, licenseTermsData: [ { terms: { @@ -958,16 +957,6 @@ describe("IP Asset Functions", () => { currency: wrappedIpAddress[aeneid], uri: "test case", }, - licensingConfig: { - isSet: true, - mintingFee: 10000n, - licensingHook: zeroAddress, - hookData: zeroAddress, - commercialRevShare: 0, - disabled: false, - expectMinimumGroupRewardShare: 0, - expectGroupRewardPool: zeroAddress, - }, }, ], royaltyShares: [ @@ -1017,7 +1006,6 @@ describe("IP Asset Functions", () => { // create parent ip with minting fee const result = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ spgNftContract: nftContractWithMintingFee, - allowDuplicates: true, licenseTermsData: [ { terms: { @@ -1074,7 +1062,6 @@ describe("IP Asset Functions", () => { nftMetadataURI: "test", nftMetadataHash: zeroHash, }, - allowDuplicates: true, txOptions: { waitForTransaction: true }, }); expect(rsp.txHash).to.be.a("string").and.not.empty; @@ -1106,7 +1093,6 @@ describe("IP Asset Functions", () => { spgNftContract: nftContractWithMintingFee, licenseTokenIds: licenseTokenIds!, maxRts: MAX_ROYALTY_TOKEN, - allowDuplicates: true, ipMetadata: { ipMetadataURI: "test", ipMetadataHash: zeroHash, @@ -1152,7 +1138,6 @@ describe("IP Asset Functions", () => { maxRts: MAX_ROYALTY_TOKEN, maxRevenueShare: 100, }, - allowDuplicates: true, ipMetadata: { ipMetadataURI: "test", ipMetadataHash: zeroHash, @@ -1219,7 +1204,6 @@ describe("IP Asset Functions", () => { maxRts: MAX_ROYALTY_TOKEN, maxRevenueShare: 100, }, - allowDuplicates: true, ipMetadata: { ipMetadataURI: "test", ipMetadataHash: zeroHash, @@ -1344,7 +1328,6 @@ describe("IP Asset Functions", () => { }, }, ], - allowDuplicates: true, }, { spgNftContract: nftContract, @@ -1381,7 +1364,6 @@ describe("IP Asset Functions", () => { }, }, ], - allowDuplicates: true, }, ], txOptions: { waitForTransaction: true }, @@ -1405,7 +1387,6 @@ describe("IP Asset Functions", () => { maxRts: 5 * 10 ** 6, maxRevenueShare: "0", }, - allowDuplicates: true, }, { spgNftContract: nftContract, @@ -1416,7 +1397,6 @@ describe("IP Asset Functions", () => { maxRts: 5 * 10 ** 6, maxRevenueShare: "0", }, - allowDuplicates: true, }, ], txOptions: { waitForTransaction: true }, diff --git a/packages/core-sdk/test/integration/license.test.ts b/packages/core-sdk/test/integration/license.test.ts index 208bd2459..ccdcaee06 100644 --- a/packages/core-sdk/test/integration/license.test.ts +++ b/packages/core-sdk/test/integration/license.test.ts @@ -1,15 +1,23 @@ import chai from "chai"; import { StoryClient } from "../../src"; -import { Hex, zeroAddress } from "viem"; +import { Hex, maxUint256, zeroAddress } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; -import { MockERC20 } from "./utils/mockERC20"; import { + mockERC721, + getStoryClient, + getTokenId, + aeneid, + publicClient, + walletClient, +} from "./utils/util"; +import { + erc20Address, licensingModuleAddress, piLicenseTemplateAddress, wrappedIpAddress, } from "../../src/abi/generated"; import { WIP_TOKEN_ADDRESS } from "../../src/constants/common"; +import { ERC20Client } from "../../src/utils/token"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -92,8 +100,8 @@ describe("License Functions", () => { waitForTransaction: true, }, }); - const mockERC20 = new MockERC20(); - await mockERC20.approve(licensingModuleAddress[aeneid]); + const mockERC20 = new ERC20Client(publicClient, walletClient, erc20Address[aeneid]); + await mockERC20.approve(licensingModuleAddress[aeneid], maxUint256); ipId = registerResult.ipId!; const registerLicenseResult = await client.license.registerCommercialRemixPIL({ defaultMintingFee: 0, diff --git a/packages/core-sdk/test/integration/nftClient.test.ts b/packages/core-sdk/test/integration/nftClient.test.ts index dcf613ed8..362d2b524 100644 --- a/packages/core-sdk/test/integration/nftClient.test.ts +++ b/packages/core-sdk/test/integration/nftClient.test.ts @@ -3,12 +3,16 @@ import { getStoryClient } from "./utils/util"; import { Address } from "viem"; import chai from "chai"; import chaiAsPromised from "chai-as-promised"; +import { erc20Address } from "../../src/abi/generated"; +import { aeneid } from "../unit/mockData"; + const expect = chai.expect; chai.use(chaiAsPromised); describe("nftClient Functions", () => { let client: StoryClient; let testWalletAddress: Address; + let spgNftContract: Address; before(async () => { client = getStoryClient(); @@ -42,13 +46,14 @@ describe("nftClient Functions", () => { mintFeeRecipient: testWalletAddress, mintOpen: true, contractURI: "test-uri", - mintFee: 1000000000000000000n, - mintFeeToken: "0x3eD6de5146C3235cC0d15023F3beaD8cb172F63b", + mintFee: 10000000n, + mintFeeToken: erc20Address[aeneid], txOptions: { waitForTransaction: true, }, }); expect(txData.spgNftContract).to.be.a("string").and.not.empty; + spgNftContract = txData.spgNftContract!; }); it("should successfully create private collection", async () => { @@ -138,4 +143,16 @@ describe("nftClient Functions", () => { ).to.be.rejectedWith("Invalid mint fee token address"); }); }); + + describe("Mint Fee", () => { + it("should successfully get mint fee token", async () => { + const mintFeeToken = await client.nftClient.getMintFeeToken(spgNftContract); + expect(mintFeeToken).to.equal(erc20Address[aeneid]); + }); + + it("should successfully get mint fee", async () => { + const mintFee = await client.nftClient.getMintFee(spgNftContract); + expect(mintFee).to.equal(10000000n); + }); + }); }); diff --git a/packages/core-sdk/test/integration/royalty.test.ts b/packages/core-sdk/test/integration/royalty.test.ts index 3dd8081d1..5405c013b 100644 --- a/packages/core-sdk/test/integration/royalty.test.ts +++ b/packages/core-sdk/test/integration/royalty.test.ts @@ -1,11 +1,25 @@ import chai from "chai"; import { StoryClient } from "../../src"; -import { Address, Hex, encodeFunctionData, zeroAddress } from "viem"; +import { Address, Hex, encodeFunctionData, parseEther, zeroAddress } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { mockERC721, getTokenId, getStoryClient, aeneid } from "./utils/util"; -import { MockERC20 } from "./utils/mockERC20"; -import { mockErc20Address, royaltyPolicyLapAddress } from "../../src/abi/generated"; +import { + mockERC721, + getTokenId, + getStoryClient, + aeneid, + publicClient, + TEST_WALLET_ADDRESS, + walletClient, +} from "./utils/util"; +import { + erc20Address, + royaltyPolicyLapAddress, + wrappedIpAddress, + royaltyPolicyLrpAddress, +} from "../../src/abi/generated"; import { MAX_ROYALTY_TOKEN, WIP_TOKEN_ADDRESS } from "../../src/constants/common"; +import { describe } from "mocha"; +import { ERC20Client } from "../../src/utils/token"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -16,7 +30,7 @@ describe("Royalty Functions", () => { let childIpId: Hex; let licenseTermsId: bigint; let parentIpIdRoyaltyAddress: Address; - let mockERC20: MockERC20; + let mockERC20: ERC20Client; // Helper functions const getIpId = async (): Promise => { @@ -35,7 +49,7 @@ describe("Royalty Functions", () => { const getCommercialPolicyId = async (): Promise => { const response = await client.license.registerCommercialRemixPIL({ defaultMintingFee: "100000", - currency: mockErc20Address[aeneid], + currency: erc20Address[aeneid], commercialRevShare: 10, txOptions: { waitForTransaction: true }, }); @@ -78,7 +92,7 @@ describe("Royalty Functions", () => { before(async () => { client = getStoryClient(); - mockERC20 = new MockERC20(); + mockERC20 = new ERC20Client(publicClient, walletClient, erc20Address[aeneid]); // Setup initial state parentIpId = await getIpId(); @@ -87,7 +101,7 @@ describe("Royalty Functions", () => { // Setup relationships and approvals await attachLicenseTerms(parentIpId, licenseTermsId); - await mockERC20.approve(client.royalty.royaltyModuleClient.address); + await mockERC20.mint(TEST_WALLET_ADDRESS, 1000n); // Register derivative await client.ipAsset.registerDerivative({ @@ -108,11 +122,10 @@ describe("Royalty Functions", () => { const response = await client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[aeneid], - amount: 10 * 10 ** 2, + token: erc20Address[aeneid], + amount: 1, txOptions: { waitForTransaction: true }, }); - expect(response.txHash).to.be.a("string").and.not.empty; }); @@ -134,7 +147,7 @@ describe("Royalty Functions", () => { const response = await client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[aeneid], + token: erc20Address[aeneid], amount: 10 * 10 ** 2, txOptions: { encodedTxDataOnly: true }, }); @@ -149,7 +162,7 @@ describe("Royalty Functions", () => { client.royalty.payRoyaltyOnBehalf({ receiverIpId: unregisteredIpId, payerIpId: childIpId, - token: mockErc20Address[aeneid], + token: erc20Address[aeneid], amount: 10 * 10 ** 2, txOptions: { waitForTransaction: true }, }), @@ -162,7 +175,7 @@ describe("Royalty Functions", () => { const response = await client.royalty.claimableRevenue({ royaltyVaultIpId: parentIpId, claimer: process.env.TEST_WALLET_ADDRESS as Address, - token: mockErc20Address[aeneid], + token: erc20Address[aeneid], }); expect(response).to.be.a("bigint"); @@ -188,7 +201,7 @@ describe("Royalty Functions", () => { client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[aeneid], + token: erc20Address[aeneid], amount: -1, txOptions: { waitForTransaction: true }, }), @@ -232,7 +245,6 @@ describe("Royalty Functions", () => { const retA = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ spgNftContract, - allowDuplicates: true, licenseTermsData: [ { terms: { @@ -273,7 +285,6 @@ describe("Royalty Functions", () => { const retB = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ spgNftContract, - allowDuplicates: true, derivData: { parentIpIds: [ipA!], licenseTermsIds: [licenseTermsId!], @@ -287,7 +298,6 @@ describe("Royalty Functions", () => { const retC = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ spgNftContract, - allowDuplicates: true, derivData: { parentIpIds: [ipB!], licenseTermsIds: [licenseTermsId!], @@ -301,7 +311,6 @@ describe("Royalty Functions", () => { const retD = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ spgNftContract, - allowDuplicates: true, derivData: { parentIpIds: [ipC!], licenseTermsIds: [licenseTermsId!], @@ -314,7 +323,7 @@ describe("Royalty Functions", () => { ipD = retD.ipId!; }); - it("should claim all revenue and convert WIP back to IP", async () => { + it("should claim all revenue", async () => { const ret = await client.royalty.claimAllRevenue({ ancestorIpId: ipA, claimer: ipA, @@ -326,4 +335,215 @@ describe("Royalty Functions", () => { expect(ret.claimedTokens![0].amount).to.equal(120n); }); }); + + describe("BatchClaimAllRevenue With WIP", () => { + let ipA: Address; + let ipA1: Address; + let ipA2: Address; + let ipA3: Address; + let ipB: Address; + let ipB1: Address; + let ipB2: Address; + let ipB3: Address; + let spgNftContract: Address; + let licenseTermsId: bigint; + let licenseTermsId1: bigint; + before(async () => { + await client.wipClient.deposit({ + amount: parseEther("5"), + txOptions: { waitForTransaction: true }, + }); + // set up + // ipA ->ipA1->ipA2->ipA3 minting Fee: 100, 10% LAP rev share, A expect to get 120 WIP + // ipB->ipB1,ipB2 + // ipB1,ipB2->ipB3 minting Fee: 150, 10% Lrp rev share, B expect to get 330 WIP + const txData = await client.nftClient.createNFTCollection({ + name: "free-collection", + symbol: "FREE", + maxSupply: 100, + isPublicMinting: true, + mintOpen: true, + contractURI: "test-uri", + mintFeeRecipient: zeroAddress, + txOptions: { waitForTransaction: true }, + }); + spgNftContract = txData.spgNftContract!; + const { results: ret1 } = await client.ipAsset.batchMintAndRegisterIpAssetWithPilTerms({ + args: [ + { + spgNftContract, + licenseTermsData: [ + { + terms: { + transferable: true, + royaltyPolicy: royaltyPolicyLapAddress[aeneid], + defaultMintingFee: 100n, + expiration: 0n, + commercialUse: true, + commercialAttribution: false, + commercializerChecker: zeroAddress, + commercializerCheckerData: zeroAddress, + commercialRevShare: 10, + commercialRevCeiling: 0n, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCeiling: 0n, + currency: WIP_TOKEN_ADDRESS, + uri: "", + }, + licensingConfig: { + isSet: false, + mintingFee: 100n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }, + }, + ], + }, + { + spgNftContract, + licenseTermsData: [ + { + terms: { + transferable: true, + royaltyPolicy: royaltyPolicyLrpAddress[aeneid], + defaultMintingFee: 150n, + expiration: 0n, + commercialUse: true, + commercialAttribution: false, + commercializerChecker: zeroAddress, + commercializerCheckerData: zeroAddress, + commercialRevShare: 10, + commercialRevCeiling: 0n, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCeiling: 0n, + currency: WIP_TOKEN_ADDRESS, + uri: "", + }, + licensingConfig: { + isSet: false, + mintingFee: 150n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }, + }, + ], + }, + ], + txOptions: { waitForTransaction: true }, + }); + ipA = ret1?.[0].ipId!; + licenseTermsId = ret1?.[0].licenseTermsIds![0]!; + ipB = ret1?.[1].ipId!; + licenseTermsId1 = ret1?.[1].licenseTermsIds![0]!; + + const { results: ret2 } = await client.ipAsset.batchMintAndRegisterIpAndMakeDerivative({ + args: [ + { + spgNftContract, + derivData: { + parentIpIds: [ipA], + licenseTermsIds: [licenseTermsId], + }, + }, + { + spgNftContract, + derivData: { + parentIpIds: [ipB], + licenseTermsIds: [licenseTermsId1], + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + ipA1 = ret2?.[0].ipId!; + ipB1 = ret2?.[1].ipId!; + const { results: ret3 } = await client.ipAsset.batchMintAndRegisterIpAndMakeDerivative({ + args: [ + { + spgNftContract, + derivData: { + parentIpIds: [ipA1], + licenseTermsIds: [licenseTermsId], + }, + }, + { + spgNftContract, + derivData: { + parentIpIds: [ipB], + licenseTermsIds: [licenseTermsId1], + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + ipA2 = ret3?.[0].ipId!; + ipB2 = ret3?.[1].ipId!; + const ret4 = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract, + derivData: { + parentIpIds: [ipA2], + licenseTermsIds: [licenseTermsId], + }, + txOptions: { waitForTransaction: true }, + }); + ipA3 = ret4.ipId!; + + const { results: ret5 } = await client.ipAsset.batchMintAndRegisterIpAndMakeDerivative({ + args: [ + { + spgNftContract, + derivData: { + parentIpIds: [ipB1, ipB2], + licenseTermsIds: [licenseTermsId1, licenseTermsId1], + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + ipB3 = ret5?.[0].ipId!; + const balance = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); + await client.wipClient.withdraw({ + amount: balance, + txOptions: { waitForTransaction: true }, + }); + }); + + it("should batch claim all revenue", async () => { + const result = await client.royalty.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId: ipA, + claimer: ipA, + childIpIds: [ipA1, ipA2], + royaltyPolicies: [royaltyPolicyLapAddress[aeneid], royaltyPolicyLapAddress[aeneid]], + currencyTokens: [WIP_TOKEN_ADDRESS, WIP_TOKEN_ADDRESS], + }, + { + ipId: ipB, + claimer: ipB, + childIpIds: [ipB1, ipB2], + royaltyPolicies: [royaltyPolicyLrpAddress[aeneid], royaltyPolicyLrpAddress[aeneid]], + currencyTokens: [WIP_TOKEN_ADDRESS, WIP_TOKEN_ADDRESS], + }, + ], + }); + expect(result.txHashes).to.be.an("array").and.not.empty; + expect(result.claimedTokens![0].amount).to.equal(120n); + expect(result.claimedTokens![1].amount).to.equal(330n); + }); + }); }); diff --git a/packages/core-sdk/test/integration/utils/mockERC20.ts b/packages/core-sdk/test/integration/utils/mockERC20.ts deleted file mode 100644 index a10469929..000000000 --- a/packages/core-sdk/test/integration/utils/mockERC20.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - PublicClient, - WalletClient, - http, - createPublicClient, - createWalletClient, - Hex, - Address, -} from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { chainStringToViemChain, waitTx } from "../../../src/utils/utils"; -import { RPC, aeneid } from "./util"; -import { mockErc20Address } from "../../../src/abi/generated"; -export class MockERC20 { - private publicClient: PublicClient; - private walletClient: WalletClient; - public address: Address = mockErc20Address[aeneid]; - - constructor(address?: Address) { - const baseConfig = { - chain: chainStringToViemChain("aeneid"), - transport: http(RPC), - } as const; - this.publicClient = createPublicClient(baseConfig); - this.walletClient = createWalletClient({ - ...baseConfig, - account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), - }); - this.address = address || mockErc20Address[aeneid]; - } - - public async approve(contract: Address): Promise { - const abi = [ - { - inputs: [ - { - internalType: "address", - name: "spender", - type: "address", - }, - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - ], - name: "approve", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - ]; - const { request: call } = await this.publicClient.simulateContract({ - abi: abi, - address: this.address, - functionName: "approve", - args: [contract, BigInt(100000 * 10 ** 6)], - account: this.walletClient.account, - }); - const approveHash = await this.walletClient.writeContract(call); - await waitTx(this.publicClient, approveHash); - } - - public async mint(): Promise { - const { request } = await this.publicClient.simulateContract({ - abi: [ - { - inputs: [ - { - internalType: "address", - name: "to", - type: "address", - }, - { - internalType: "uint256", - name: "amount", - type: "uint256", - }, - ], - name: "mint", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - ], - address: this.address, - functionName: "mint", - account: this.walletClient.account, - args: [process.env.TEST_WALLET_ADDRESS! as Address, BigInt(100000 * 10 ** 6)], - }); - const mintHash = await this.walletClient.writeContract(request); - await waitTx(this.publicClient, mintHash); - } -} diff --git a/packages/core-sdk/test/integration/utils/util.ts b/packages/core-sdk/test/integration/utils/util.ts index 257ba6d1b..67c5ede58 100644 --- a/packages/core-sdk/test/integration/utils/util.ts +++ b/packages/core-sdk/test/integration/utils/util.ts @@ -1,15 +1,6 @@ import { privateKeyToAccount } from "viem/accounts"; import { chainStringToViemChain, waitTx } from "../../../src/utils/utils"; -import { - http, - createPublicClient, - createWalletClient, - Hex, - Address, - zeroHash, - TransactionReceipt, - parseEther, -} from "viem"; +import { http, createPublicClient, createWalletClient, Hex, Address, zeroHash } from "viem"; import { StoryClient, StoryConfig } from "../../../src"; import { licenseTokenAbi, @@ -18,7 +9,6 @@ import { } from "../../../src/abi/generated"; export const RPC = "https://aeneid.storyrpc.io"; export const aeneid = 1315; - export const mockERC721 = "0xa1119092ea911202E0a65B743a13AE28C5CF2f21"; export const licenseToken = licenseTokenAddress[aeneid]; export const spgNftBeacon = spgnftBeaconAddress[aeneid]; @@ -130,11 +120,12 @@ export const approveForLicenseToken = async (address: Address, tokenId: bigint) const hash = await walletClient.writeContract(call); await waitTx(publicClient, hash); }; -export const getStoryClient = (): StoryClient => { +export const getStoryClient = (privateKey?: Address): StoryClient => { const config: StoryConfig = { chainId: "aeneid", transport: http(RPC), - account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Address), + account: privateKeyToAccount(privateKey ?? (process.env.WALLET_PRIVATE_KEY as Address)), }; + return StoryClient.newClient(config); }; diff --git a/packages/core-sdk/test/integration/wip.test.ts b/packages/core-sdk/test/integration/wip.test.ts index f4f78cf96..c0a38ccc0 100644 --- a/packages/core-sdk/test/integration/wip.test.ts +++ b/packages/core-sdk/test/integration/wip.test.ts @@ -33,6 +33,17 @@ describe("WIP Functions", () => { expect(balanceAfter).to.equal(balanceBefore - ipAmt - gasCost); }); }); + describe("transfer", () => { + it("should transfer WIP", async () => { + const rsp = await client.wipClient.transfer({ + to: TEST_WALLET_ADDRESS, + amount: parseEther("0.01"), + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + //Due to approve cannot approve msy.sender, so skip transferFrom test + }); + }); describe("withdraw", () => { it("should withdrawal WIP", async () => { diff --git a/packages/core-sdk/test/unit/mockData.ts b/packages/core-sdk/test/unit/mockData.ts new file mode 100644 index 000000000..5386fa8f2 --- /dev/null +++ b/packages/core-sdk/test/unit/mockData.ts @@ -0,0 +1,6 @@ +export const txHash = "0x063834efe214f4199b1ad7181ce8c5ced3e15d271c8e866da7c89e86ee629cfb"; +export const ipId = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; +export const aeneid = "1315"; +export const mockERC20 = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; +export const walletAddress = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; +export const mockAddress = "0x73fcb515cee99e4991465ef586cfe2b072ebb513"; diff --git a/packages/core-sdk/test/unit/resources/dispute.test.ts b/packages/core-sdk/test/unit/resources/dispute.test.ts index 19d70d762..d0c8890c6 100644 --- a/packages/core-sdk/test/unit/resources/dispute.test.ts +++ b/packages/core-sdk/test/unit/resources/dispute.test.ts @@ -1,12 +1,13 @@ import * as sinon from "sinon"; -import { createMock } from "../testUtils"; +import { createMock, generateRandomHash } from "../testUtils"; +import { ipId, txHash } from "../mockData"; import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { PublicClient, WalletClient } from "viem"; import { DisputeClient } from "../../../src"; +import { IpAccountImplClient } from "../../../src/abi/generated"; chai.use(chaiAsPromised); -const txHash = "0x063834efe214f4199b1ad7181ce8c5ced3e15d271c8e866da7c89e86ee629cfb"; describe("Test DisputeClient", () => { let disputeClient: DisputeClient; @@ -23,13 +24,11 @@ describe("Test DisputeClient", () => { sinon.restore(); }); - describe("Test raiseDispute", () => { + describe("raiseDispute", () => { it("throw address error when call raiseDispute with invalid targetIpId", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds").resolves(1000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(1000n); sinon .stub(disputeClient.disputeModuleClient, "isWhitelistedDisputeTag") .resolves({ allowed: true }); @@ -42,17 +41,13 @@ describe("Test DisputeClient", () => { liveness: 2592000, }); } catch (e) { - expect((e as Error).message).equal( - "Failed to raise dispute: request.targetIpId address is invalid: 0x, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", - ); + expect((e as Error).message).equal("Failed to raise dispute: Invalid address: 0x."); } }); - it("throw liveness error when call raiseDispute given livenss out of range", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(100n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); + it("throw liveness error when call raiseDispute given liveness out of range", async () => { + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(100n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); try { await disputeClient.raiseDispute({ targetIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -69,11 +64,9 @@ describe("Test DisputeClient", () => { }); it("throw bond error when call raiseDispute given bond more than max bonds", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds").resolves(1000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(1000n); try { await disputeClient.raiseDispute({ targetIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -90,11 +83,9 @@ describe("Test DisputeClient", () => { }); it("should throw tag error when call raiseDispute given tag not whitelisted", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds").resolves(1000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(1000n); sinon .stub(disputeClient.disputeModuleClient, "isWhitelistedDisputeTag") .resolves({ allowed: false }); @@ -113,13 +104,9 @@ describe("Test DisputeClient", () => { } }); it("should return txHash when call raiseDispute successfully", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds") - .resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(100000000000n); sinon .stub(disputeClient.disputeModuleClient, "isWhitelistedDisputeTag") .resolves({ allowed: true }); @@ -137,13 +124,9 @@ describe("Test DisputeClient", () => { }); it("should return txHash and disputeId when call raiseDispute successfully with waitForTransaction", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds") - .resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(100000000000n); sinon .stub(disputeClient.disputeModuleClient, "isWhitelistedDisputeTag") .resolves({ allowed: true }); @@ -174,13 +157,9 @@ describe("Test DisputeClient", () => { }); it("should return encodedTxData when call raiseDispute successfully with encodedTxDataOnly", async () => { - sinon.stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "minLiveness").resolves(0n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxLiveness") - .resolves(100000000000n); - sinon - .stub(disputeClient.arbitrationPolicyUmaReadOnlyClient, "maxBonds") - .resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "minLiveness").resolves(0n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxLiveness").resolves(100000000000n); + sinon.stub(disputeClient.arbitrationPolicyUmaClient, "maxBonds").resolves(100000000000n); sinon .stub(disputeClient.disputeModuleClient, "isWhitelistedDisputeTag") .resolves({ allowed: true }); @@ -197,7 +176,7 @@ describe("Test DisputeClient", () => { }); }); - describe("Test cancelDispute", () => { + describe("cancelDispute", () => { it("should throw error when call cancelDispute failed", async () => { sinon.stub(disputeClient.disputeModuleClient, "cancelDispute").rejects(new Error("500")); try { @@ -239,7 +218,7 @@ describe("Test DisputeClient", () => { }); }); - describe("Test resolveDispute", () => { + describe("resolveDispute", () => { it("should throw error when call resolveDispute failed", async () => { sinon.stub(disputeClient.disputeModuleClient, "resolveDispute").rejects(new Error("500")); try { @@ -283,4 +262,96 @@ describe("Test DisputeClient", () => { expect(result.encodedTxData?.data).to.be.a("string").and.not.empty; }); }); + + describe("tagIfRelatedIpInfringed", () => { + let aggregate3Stub: sinon.SinonStub; + let tagIfRelatedIpInfringedStub: sinon.SinonStub; + beforeEach(() => { + aggregate3Stub = sinon.stub(disputeClient.multicall3Client, "aggregate3").resolves(txHash); + tagIfRelatedIpInfringedStub = sinon + .stub(disputeClient.disputeModuleClient, "tagIfRelatedIpInfringed") + .resolves(txHash); + }); + it("should not call multicall3 when call tagIfRelatedIpInfringed give disable useMulticallWhenPossible", async () => { + const result = await disputeClient.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: ipId, + disputeId: 1, + }, + { + ipId: ipId, + disputeId: 1, + }, + ], + options: { useMulticallWhenPossible: false }, + }); + + expect(result.length).equal(2); + expect(aggregate3Stub.called).to.be.false; + expect(tagIfRelatedIpInfringedStub.callCount).equal(2); + }); + + it("should call multicall3 when call tagIfRelatedIpInfringed give more than one infringementTags", async () => { + const result = await disputeClient.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: ipId, + disputeId: 1, + }, + { + ipId: ipId, + disputeId: 1, + }, + ], + }); + + expect(result.length).equal(1); + expect(aggregate3Stub.calledOnce).to.be.true; + expect(tagIfRelatedIpInfringedStub.callCount).equal(0); + }); + + it("should not call multicall3 when call tagIfRelatedIpInfringed give only one infringementTags", async () => { + const result = await disputeClient.tagIfRelatedIpInfringed({ + infringementTags: [ + { + ipId: ipId, + disputeId: 1, + }, + ], + }); + + expect(result.length).equal(1); + expect(aggregate3Stub.called).to.be.false; + expect(tagIfRelatedIpInfringedStub.calledOnce).to.be.true; + }); + }); + + describe("disputeAssertion", () => { + it("should dispute assertion successfully", async () => { + const accountExecuteMock = sinon + .stub(IpAccountImplClient.prototype, "execute") + .resolves(txHash); + const result = await disputeClient.disputeAssertion({ + ipId, + assertionId: generateRandomHash(), + counterEvidenceCID: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", + }); + expect(result.txHash).equal(txHash); + expect(accountExecuteMock.calledOnce).to.be.true; + }); + }); + + describe("disputeIdToAssertionId", () => { + it("should return assertionId", async () => { + const mockAssertionId = generateRandomHash(); + const mock = sinon + .stub(disputeClient.arbitrationPolicyUmaClient, "disputeIdToAssertionId") + .resolves(mockAssertionId); + const disputeId = BigInt(10); + const result = await disputeClient.disputeIdToAssertionId(disputeId); + expect(result).equal(mockAssertionId); + expect(mock.calledOnceWith({ disputeId })).to.be.true; + }); + }); }); diff --git a/packages/core-sdk/test/unit/resources/group.test.ts b/packages/core-sdk/test/unit/resources/group.test.ts index 81b270a1f..43fadac74 100644 --- a/packages/core-sdk/test/unit/resources/group.test.ts +++ b/packages/core-sdk/test/unit/resources/group.test.ts @@ -1,10 +1,11 @@ import chai from "chai"; import { createMock } from "../testUtils"; import * as sinon from "sinon"; -import { PublicClient, WalletClient, Account, zeroAddress } from "viem"; +import { PublicClient, WalletClient, Account, zeroAddress, zeroHash } from "viem"; import chaiAsPromised from "chai-as-promised"; import { GroupClient } from "../../../src"; import { LicenseData } from "../../../src/types/resources/group"; +import { walletAddress } from "../mockData"; const { IpAccountImplClient } = require("../../../src/abi/generated"); chai.use(chaiAsPromised); @@ -376,6 +377,34 @@ describe("Test IpAssetClient", () => { }); expect(result.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values when mintAndRegisterIpAndAttachLicenseAndAddToGroup without providing allowDuplicates, ipMetadata, recipient", async () => { + sinon.stub(groupClient.ipAssetRegistryClient, "isRegistered").resolves(true); + const mintAndRegisterIpAndAttachLicenseAndAddToGroupStub = sinon + .stub(groupClient.groupingWorkflowsClient, "mintAndRegisterIpAndAttachLicenseAndAddToGroup") + .resolves(txHash); + await groupClient.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ + groupId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + maxAllowedRewardShare: 5, + spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + licenseData: [mockLicenseData], + }); + + expect( + mintAndRegisterIpAndAttachLicenseAndAddToGroupStub.args[0][0].allowDuplicates, + ).to.equal(true); + expect( + mintAndRegisterIpAndAttachLicenseAndAddToGroupStub.args[0][0].ipMetadata, + ).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataURI: "", + nftMetadataHash: zeroHash, + }); + expect(mintAndRegisterIpAndAttachLicenseAndAddToGroupStub.args[0][0].recipient).to.equal( + walletAddress, + ); + }); }); describe("Test groupClient.registerIpAndAttachLicenseAndAddToGroup", async () => { diff --git a/packages/core-sdk/test/unit/resources/ipAccount.test.ts b/packages/core-sdk/test/unit/resources/ipAccount.test.ts index d6a9e2201..5f48aeea7 100644 --- a/packages/core-sdk/test/unit/resources/ipAccount.test.ts +++ b/packages/core-sdk/test/unit/resources/ipAccount.test.ts @@ -4,20 +4,20 @@ import * as sinon from "sinon"; import { IPAccountClient } from "../../../src/resources/ipAccount"; import { IPAccountExecuteRequest, IPAccountExecuteWithSigRequest } from "../../../src"; import * as utils from "../../../src/utils/utils"; -import { Account, PublicClient, WalletClient, zeroAddress } from "viem"; -const { IpAccountImplClient } = require("../../../src/abi/generated"); +import { Account, PublicClient, toHex, WalletClient, zeroAddress } from "viem"; +import { aeneid, ipId, txHash } from "../mockData"; +import { IpAccountImplClient } from "../../../src/abi/generated"; describe("Test IPAccountClient", () => { let ipAccountClient: IPAccountClient; let rpcMock: PublicClient; let walletMock: WalletClient; - const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; beforeEach(() => { rpcMock = createMock(); walletMock = createMock(); const accountMock = createMock(); walletMock.account = accountMock; - ipAccountClient = new IPAccountClient(rpcMock, walletMock); + ipAccountClient = new IPAccountClient(rpcMock, walletMock, aeneid); sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); sinon.stub(IpAccountImplClient.prototype, "executeEncode").returns({ data: "0x", to: "0x" }); sinon.stub(IpAccountImplClient.prototype, "executeWithSig").resolves(txHash); @@ -195,4 +195,26 @@ describe("Test IPAccountClient", () => { expect(token).to.deep.equal({ chainId: 1513n, tokenContract: zeroAddress, tokenId: 1n }); }); }); + + describe("Test setIpMetadata", () => { + it("should throw error when call setIpMetadata given wrong ipId", async () => { + try { + await ipAccountClient.setIpMetadata({ + ipId: "0x", + metadataURI: "https://example.com", + metadataHash: toHex("test", { size: 32 }), + }); + } catch (err) { + expect((err as Error).message).equal("Failed to set the IP metadata: Invalid address: 0x."); + } + }); + it("should return txHash when call setIpMetadata successfully", async () => { + const result = await ipAccountClient.setIpMetadata({ + ipId: ipId, + metadataURI: "https://example.com", + metadataHash: toHex("test", { size: 32 }), + }); + expect(result).to.equal(txHash); + }); + }); }); diff --git a/packages/core-sdk/test/unit/resources/ipAsset.test.ts b/packages/core-sdk/test/unit/resources/ipAsset.test.ts index ba31b367a..f9981d682 100644 --- a/packages/core-sdk/test/unit/resources/ipAsset.test.ts +++ b/packages/core-sdk/test/unit/resources/ipAsset.test.ts @@ -1,7 +1,7 @@ import chai from "chai"; import { createMock } from "../testUtils"; import * as sinon from "sinon"; -import { IPAssetClient, LicenseTerms } from "../../../src"; +import { IPAssetClient, LicenseTerms, StoryRelationship } from "../../../src"; import { PublicClient, WalletClient, @@ -17,6 +17,7 @@ import { LicenseRegistryReadOnlyClient } from "../../../src/abi/generated"; import { MAX_ROYALTY_TOKEN, royaltySharesTotalSupply } from "../../../src/constants/common"; import { LicensingConfig } from "../../../src/types/common"; import { DerivativeData } from "../../../src/types/resources/ipAsset"; +import { txHash, walletAddress } from "../mockData"; const { RoyaltyModuleReadOnlyClient, IpRoyaltyVaultImplReadOnlyClient, @@ -24,7 +25,6 @@ const { SpgnftImplReadOnlyClient, LicensingModuleClient, } = require("../../../src/abi/generated"); -const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; chai.use(chaiAsPromised); const expect = chai.expect; const licenseTerms: LicenseTerms = { @@ -136,7 +136,7 @@ describe("Test IpAssetClient", () => { relationships: [ { parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512" as Address, - type: "APPEARS_IN", + type: StoryRelationship.APPEARS_IN, }, ], createdAt: "2024-08-22T10:20:30Z", @@ -145,10 +145,6 @@ describe("Test IpAssetClient", () => { media: [ { name: "Cover Image", url: "https://example.com/cover.jpg", mimeType: "image/jpeg" }, ], - attributes: [ - { key: "Genre", value: "Adventure" }, - { key: "Pages", value: 350 }, - ], app: { id: "app_001", name: "Story Protocol", website: "https://story.foundation" }, tags: ["Adventure", "Thriller"], robotTerms: { userAgent: "*", allow: "/" }, @@ -186,7 +182,10 @@ describe("Test IpAssetClient", () => { .to.have.property("relationships") .that.is.an("array") .that.deep.equals([ - { parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", type: "APPEARS_IN" }, + { + parentIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + type: StoryRelationship.APPEARS_IN, + }, ]); expect(metadata).to.have.property("createdAt", "2024-08-22T10:20:30Z"); expect(metadata).to.have.property("watermarkImg", "https://example.com/watermark.png"); @@ -200,13 +199,6 @@ describe("Test IpAssetClient", () => { .that.deep.equals([ { name: "Cover Image", url: "https://example.com/cover.jpg", mimeType: "image/jpeg" }, ]); - expect(metadata) - .to.have.property("attributes") - .that.is.an("array") - .that.deep.equals([ - { key: "Genre", value: "Adventure" }, - { key: "Pages", value: 350 }, - ]); expect(metadata).to.have.property("app").that.deep.equals({ id: "app_001", name: "Story Protocol", @@ -661,6 +653,9 @@ describe("Test IpAssetClient", () => { sinon.stub(ipAssetClient.licenseRegistryReadOnlyClient, "getRoyaltyPercent").resolves({ royaltyPercent: 100, }); + // Because registerDerivative doesn't call trigger IPRegistered event, but the `handleRegistrationWithFees` + // will call it, so we need to mock the result of parseTxIpRegisteredEvent to avoid the error. + sinon.stub(ipAssetClient.ipAssetRegistryClient, "parseTxIpRegisteredEvent").returns([]); const res = await ipAssetClient.registerDerivative({ childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -710,6 +705,34 @@ describe("Test IpAssetClient", () => { expect(res.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values of maxMintingFee, maxRts, maxRevenueShare when registerDerivative given maxMintingFee, maxRts, maxRevenueShare is not provided", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onCall(0) + .resolves(true) + .onCall(1) + .resolves(true); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + const registerDerivativeStub = sinon + .stub(ipAssetClient.licensingModuleClient, "registerDerivative") + .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); + sinon.stub(ipAssetClient.licenseRegistryReadOnlyClient, "getRoyaltyPercent").resolves({ + royaltyPercent: 100, + }); + + await ipAssetClient.registerDerivative({ + childIpId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + parentIpIds: ["0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"], + licenseTermsIds: ["1"], + licenseTemplate: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + }); + expect(registerDerivativeStub.args[0][0].maxMintingFee).equal(0n); + expect(registerDerivativeStub.args[0][0].maxRts).equal(MAX_ROYALTY_TOKEN); + expect(registerDerivativeStub.args[0][0].maxRevenueShare).equal(MAX_ROYALTY_TOKEN); + }); }); describe("Test ipAssetClient.registerDerivativeWithLicenseTokens", async () => { @@ -924,7 +947,6 @@ describe("Test IpAssetClient", () => { ], allowDuplicates: false, recipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", - royaltyPolicyAddress: zeroAddress, ipMetadata: { ipMetadataURI: "", ipMetadataHash: toHex(0, { size: 32 }), @@ -1025,7 +1047,6 @@ describe("Test IpAssetClient", () => { licensingConfig, }, ], - allowDuplicates: false, recipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", ipMetadata: { ipMetadataURI: "", @@ -1038,6 +1059,29 @@ describe("Test IpAssetClient", () => { expect(result.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values when createIpAssetWithPilTerms without providing allowDuplicates, ipMetadata, recipient", async () => { + const mintAndRegisterIpAndAttachPilTermsStub = sinon + .stub(ipAssetClient.licenseAttachmentWorkflowsClient, "mintAndRegisterIpAndAttachPilTerms") + .resolves(txHash); + await ipAssetClient.mintAndRegisterIpAssetWithPilTerms({ + spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + licenseTermsData: [ + { + terms: licenseTerms, + licensingConfig, + }, + ], + }); + expect(mintAndRegisterIpAndAttachPilTermsStub.args[0][0].allowDuplicates).to.equal(true); + expect(mintAndRegisterIpAndAttachPilTermsStub.args[0][0].ipMetadata).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataURI: "", + nftMetadataHash: zeroHash, + }); + expect(mintAndRegisterIpAndAttachPilTermsStub.args[0][0].recipient).to.equal(walletAddress); + }); }); describe("Test ipAssetClient.registerDerivativeIp", async () => { @@ -1490,7 +1534,6 @@ describe("Test IpAssetClient", () => { const res = await ipAssetClient.mintAndRegisterIpAndMakeDerivative({ spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", derivData, - allowDuplicates: false, ipMetadata: { ipMetadataURI: "https://", nftMetadataHash: toHex("nftMetadata", { size: 32 }), @@ -1502,6 +1545,53 @@ describe("Test IpAssetClient", () => { expect(res.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values when mintAndRegisterIpAndMakeDerivative without providing allowDuplicates, ipMetadata", async () => { + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "ipId") + .resolves("0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"); + sinon + .stub(ipAssetClient.ipAssetRegistryClient, "isRegistered") + .onFirstCall() + .resolves(true) + .onSecondCall() + .resolves(false); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon.stub(ipAssetClient.licenseRegistryReadOnlyClient, "getRoyaltyPercent").resolves({ + royaltyPercent: 100, + }); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "parseTxIpRegisteredEvent").returns([ + { + ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + chainId: 0n, + tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + tokenId: 1n, + name: "", + uri: "", + registrationDate: 0n, + }, + ]); + + const mintAndRegisterIpAndMakeDerivativeStub = sinon + .stub(ipAssetClient.derivativeWorkflowsClient, "mintAndRegisterIpAndMakeDerivative") + .resolves(txHash); + + await ipAssetClient.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + derivData, + }); + + expect(mintAndRegisterIpAndMakeDerivativeStub.args[0][0].allowDuplicates).to.equal(true); + expect(mintAndRegisterIpAndMakeDerivativeStub.args[0][0].ipMetadata).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataURI: "", + nftMetadataHash: zeroHash, + }); + expect(mintAndRegisterIpAndMakeDerivativeStub.args[0][0].recipient).to.equal(walletAddress); + }); }); describe("Test ipAssetClient.mintAndRegisterIp", async () => { it("should throw spgNftContract error when mintAndRegisterIp given spgNftContract is wrong address", async () => { @@ -1601,6 +1691,25 @@ describe("Test IpAssetClient", () => { expect(result.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values when mintAndRegisterIp without providing allowDuplicates, ipMetadata, recipient", async () => { + const mintAndRegisterIpStub = sinon + .stub(ipAssetClient.registrationWorkflowsClient, "mintAndRegisterIp") + .resolves(txHash); + + await ipAssetClient.mintAndRegisterIp({ + spgNftContract, + }); + + expect(mintAndRegisterIpStub.args[0][0].allowDuplicates).to.equal(true); + expect(mintAndRegisterIpStub.args[0][0].ipMetadata).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataURI: "", + nftMetadataHash: zeroHash, + }); + expect(mintAndRegisterIpStub.args[0][0].recipient).to.equal(walletAddress); + }); }); describe("Test ipAssetClient.registerPilTermsAndAttach", async () => { @@ -1709,6 +1818,34 @@ describe("Test IpAssetClient", () => { }); expect(result.txHash).to.equal(txHash); }); + + it("should call with default values of licensingConfig when registerPilTermsAndAttach given licensingConfig is not provided", async () => { + const registerPilTermsAndAttachStub = sinon + .stub(ipAssetClient.licenseAttachmentWorkflowsClient, "registerPilTermsAndAttach") + .resolves(txHash); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(true); + + await ipAssetClient.registerPilTermsAndAttach({ + ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + licenseTermsData: [ + { + terms: licenseTerms, + }, + ], + }); + expect( + registerPilTermsAndAttachStub.args[0][0].licenseTermsData[0].licensingConfig, + ).to.deep.equal({ + isSet: false, + mintingFee: 0n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }); + }); }); describe("Test ipAssetClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens", async () => { @@ -1839,6 +1976,42 @@ describe("Test IpAssetClient", () => { expect(result.encodedTxData!.data).to.be.a("string").and.not.empty; }); + + it("should call with default values when mintAndRegisterIpAndMakeDerivativeWithLicenseTokens without providing allowDuplicates, ipMetadata, royaltyContext, recipient", async () => { + sinon + .stub(ipAssetClient.licenseTokenReadOnlyClient, "ownerOf") + .resolves("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); + const mintAndRegisterIpAndMakeDerivativeWithLicenseTokensStub = sinon + .stub( + ipAssetClient.derivativeWorkflowsClient, + "mintAndRegisterIpAndMakeDerivativeWithLicenseTokens", + ) + .resolves(txHash); + + await ipAssetClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens({ + spgNftContract, + licenseTokenIds: ["0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"], + maxRts: 0, + }); + + expect( + mintAndRegisterIpAndMakeDerivativeWithLicenseTokensStub.args[0][0].allowDuplicates, + ).to.equal(true); + expect( + mintAndRegisterIpAndMakeDerivativeWithLicenseTokensStub.args[0][0].ipMetadata, + ).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataURI: "", + nftMetadataHash: zeroHash, + }); + expect( + mintAndRegisterIpAndMakeDerivativeWithLicenseTokensStub.args[0][0].royaltyContext, + ).to.equal(zeroAddress); + expect(mintAndRegisterIpAndMakeDerivativeWithLicenseTokensStub.args[0][0].recipient).to.equal( + walletAddress, + ); + }); }); describe("Test ipAssetClient.registerIpAndMakeDerivativeWithLicenseTokens", async () => { @@ -3060,8 +3233,7 @@ describe("Test IpAssetClient", () => { tokenId: "1", }); expect(result).to.deep.equal({ - registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: - "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: txHash, distributeRoyaltyTokensTxHash: txHash, ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", ipRoyaltyVault: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -3141,8 +3313,7 @@ describe("Test IpAssetClient", () => { }, }); expect(result).to.deep.equal({ - registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: - "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", + registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: txHash, distributeRoyaltyTokensTxHash: txHash, ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", ipRoyaltyVault: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", @@ -3285,6 +3456,43 @@ describe("Test IpAssetClient", () => { ipRoyaltyVault: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", }); }); + + it("should call with default values when mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens without providing allowDuplicates, ipMetadata, recipient", async () => { + const mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensStub = sinon + .stub( + ipAssetClient.royaltyTokenDistributionWorkflowsClient, + "mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens", + ) + .resolves(txHash); + + await ipAssetClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens({ + spgNftContract, + licenseTermsData: [ + { + terms: licenseTerms, + licensingConfig, + }, + ], + royaltyShares: [ + { recipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", percentage: 100 }, + ], + }); + + expect( + mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensStub.args[0][0].allowDuplicates, + ).to.equal(true); + expect( + mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensStub.args[0][0].ipMetadata, + ).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataHash: zeroHash, + nftMetadataURI: "", + }); + expect( + mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensStub.args[0][0].recipient, + ).to.equal(walletAddress); + }); }); describe("Test ipAssetClient.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", async () => { @@ -3370,7 +3578,7 @@ describe("Test IpAssetClient", () => { }); } catch (err) { expect((err as Error).message).equal( - "Failed to mint and register IP and make derivative and distribute royalty tokens: CommercialRevShare should be between 0 and 100.", + "Failed to mint and register IP and make derivative and distribute royalty tokens: MaxRevenueShare must be between 0 and 100.", ); } }); @@ -3398,7 +3606,7 @@ describe("Test IpAssetClient", () => { }); } catch (err) { expect((err as Error).message).equal( - "Failed to mint and register IP and make derivative and distribute royalty tokens: CommercialRevShare should be between 0 and 100.", + "Failed to mint and register IP and make derivative and distribute royalty tokens: MaxRevenueShare must be between 0 and 100.", ); } }); @@ -3509,5 +3717,53 @@ describe("Test IpAssetClient", () => { tokenId: 0n, }); }); + + it("should call with default values when mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens without providing allowDuplicates, ipMetadata, recipient", async () => { + sinon.stub(ipAssetClient.licenseRegistryReadOnlyClient, "getRoyaltyPercent").resolves({ + royaltyPercent: 100, + }); + sinon + .stub(ipAssetClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(ipAssetClient.licenseTemplateClient, "getLicenseTerms").resolves({ + terms: licenseTerms, + }); + const mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensStub = sinon + .stub( + ipAssetClient.royaltyTokenDistributionWorkflowsClient, + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", + ) + .resolves(txHash); + + await ipAssetClient.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens({ + spgNftContract, + derivData: { + parentIpIds: ["0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"], + licenseTermsIds: [1n], + maxMintingFee: 100, + maxRts: 100, + maxRevenueShare: 100, + }, + royaltyShares: [ + { recipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", percentage: 100 }, + ], + }); + + expect( + mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensStub.args[0][0].allowDuplicates, + ).to.equal(true); + expect( + mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensStub.args[0][0].ipMetadata, + ).to.deep.equal({ + ipMetadataURI: "", + ipMetadataHash: zeroHash, + nftMetadataHash: zeroHash, + nftMetadataURI: "", + }); + expect( + mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensStub.args[0][0].recipient, + ).to.equal(walletAddress); + }); }); }); diff --git a/packages/core-sdk/test/unit/resources/license.test.ts b/packages/core-sdk/test/unit/resources/license.test.ts index 5fa8f8227..63d3f3ae8 100644 --- a/packages/core-sdk/test/unit/resources/license.test.ts +++ b/packages/core-sdk/test/unit/resources/license.test.ts @@ -4,10 +4,13 @@ import * as sinon from "sinon"; import { LicenseClient } from "../../../src"; import { PublicClient, WalletClient, Account, zeroAddress, Hex } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { PiLicenseTemplateGetLicenseTermsResponse } from "../../../src/abi/generated"; +import { + PiLicenseTemplateGetLicenseTermsResponse, + RoyaltyModuleReadOnlyClient, + WrappedIpClient, +} from "../../../src/abi/generated"; import { LicenseTerms } from "../../../src/types/resources/license"; import { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; -const { RoyaltyModuleReadOnlyClient } = require("../../../src/abi/generated"); chai.use(chaiAsPromised); const expect = chai.expect; @@ -42,10 +45,10 @@ describe("Test LicenseClient", () => { describe("Test licenseClient.registerPILTerms", async () => { beforeEach(() => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() + sinon.stub(RoyaltyModuleReadOnlyClient.prototype, "isWhitelistedRoyaltyToken").resolves(true); + sinon + .stub(RoyaltyModuleReadOnlyClient.prototype, "isWhitelistedRoyaltyPolicy") .resolves(true); - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyToken = sinon.stub().resolves(true); }); const licenseTerms: LicenseTerms = { defaultMintingFee: 1513n, @@ -830,9 +833,7 @@ describe("Test LicenseClient", () => { describe("With Minting Fees", () => { let mintLicenseTokensStub: sinon.SinonStub; - let wipBalanceOfStub: sinon.SinonStub; let balanceStub: sinon.SinonStub; - let approveStub: sinon.SinonStub; let simulateContractStub: sinon.SinonStub; beforeEach(() => { @@ -840,11 +841,11 @@ describe("Test LicenseClient", () => { currencyToken: WIP_TOKEN_ADDRESS, tokenAmount: 100n, }); - approveStub = sinon.stub(licenseClient.wipClient, "approve").resolves(txHash); - sinon.stub(licenseClient.wipClient, "allowance").resolves({ + sinon.stub(WrappedIpClient.prototype, "approve").resolves(txHash); + sinon.stub(WrappedIpClient.prototype, "allowance").resolves({ result: 50n, }); - wipBalanceOfStub = sinon.stub(licenseClient.wipClient, "balanceOf").resolves({ + sinon.stub(WrappedIpClient.prototype, "balanceOf").resolves({ result: 0n, }); balanceStub = sinon.stub().resolves(200n); @@ -873,7 +874,6 @@ describe("Test LicenseClient", () => { }); expect(result.txHash).to.equal(txHash); expect(result.receipt).to.be.undefined; - expect(approveStub.calledOnce).to.be.true; expect(mintLicenseTokensStub.calledOnce).to.be.true; expect(mintLicenseTokensStub.firstCall.args[0].receiver).to.equal( walletMock.account!.address, @@ -1098,7 +1098,7 @@ describe("Test LicenseClient", () => { }); } catch (error) { expect((error as Error).message).equal( - "Failed to set licensing config: The minting fee must be greater than 0.", + "Failed to set licensing config: The mintingFee must be greater than 0.", ); } }); diff --git a/packages/core-sdk/test/unit/resources/nftClient.test.ts b/packages/core-sdk/test/unit/resources/nftClient.test.ts index 11350150f..6a8683fa3 100644 --- a/packages/core-sdk/test/unit/resources/nftClient.test.ts +++ b/packages/core-sdk/test/unit/resources/nftClient.test.ts @@ -5,6 +5,8 @@ import { PublicClient, WalletClient } from "viem"; import { NftClient } from "../../../src"; import { createMock } from "../testUtils"; +import { mockERC20 } from "../mockData"; +import { SpgnftImplReadOnlyClient } from "../../../src/abi/generated"; chai.use(chaiAsPromised); @@ -128,4 +130,20 @@ describe("Test NftClient", () => { expect(result.encodedTxData?.data).to.be.a("string").and.not.empty; }); }); + + describe("test for getMintFeeToken", () => { + it("should successfully get mint fee token", async () => { + sinon.stub(SpgnftImplReadOnlyClient.prototype, "mintFeeToken").resolves(mockERC20); + const mintFeeToken = await nftClient.getMintFeeToken(mockERC20); + expect(mintFeeToken).equal(mockERC20); + }); + }); + + describe("test for getMintFee", () => { + it("should successfully get mint fee", async () => { + sinon.stub(SpgnftImplReadOnlyClient.prototype, "mintFee").resolves(1n); + const mintFee = await nftClient.getMintFee(mockERC20); + expect(mintFee).equal(1n); + }); + }); }); diff --git a/packages/core-sdk/test/unit/resources/royalty.test.ts b/packages/core-sdk/test/unit/resources/royalty.test.ts index 3f65b8c00..c640b8886 100644 --- a/packages/core-sdk/test/unit/resources/royalty.test.ts +++ b/packages/core-sdk/test/unit/resources/royalty.test.ts @@ -4,8 +4,15 @@ import * as sinon from "sinon"; import { PublicClient, WalletClient, Account } from "viem"; import chaiAsPromised from "chai-as-promised"; import { RoyaltyClient } from "../../../src/resources/royalty"; +import { + IpAccountImplClient, + IpRoyaltyVaultImplReadOnlyClient, + erc20Address, +} from "../../../src/abi/generated"; +import { aeneid } from "../../integration/utils/util"; +import { ERC20Client, WipTokenClient } from "../../../src/utils/token"; +import { ipId, mockAddress, walletAddress } from "../mockData"; import { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; -const { IpRoyaltyVaultImplReadOnlyClient } = require("../../../src/abi/generated"); chai.use(chaiAsPromised); const expect = chai.expect; const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; @@ -22,7 +29,6 @@ describe("Test RoyaltyClient", () => { accountMock.address = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; walletMock.account = accountMock; royaltyClient = new RoyaltyClient(rpcMock, walletMock); - sinon.stub(); }); afterEach(() => { @@ -30,6 +36,16 @@ describe("Test RoyaltyClient", () => { }); describe("Test royaltyClient.payRoyaltyOnBehalf", async () => { + beforeEach(() => { + sinon.stub(ERC20Client.prototype, "balanceOf").resolves(1n); + sinon.stub(ERC20Client.prototype, "allowance").resolves(10000n); + sinon.stub(ERC20Client.prototype, "approve").resolves(txHash); + sinon.stub(WipTokenClient.prototype, "balanceOf").resolves(1n); + sinon.stub(WipTokenClient.prototype, "allowance").resolves(1n); + sinon.stub(WipTokenClient.prototype, "approve").resolves(txHash); + sinon.stub(WipTokenClient.prototype, "address").get(() => WIP_TOKEN_ADDRESS); + }); + it("should throw receiverIpId error when call payRoyaltyOnBehalf given receiverIpId is not registered", async () => { sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(false); @@ -46,7 +62,20 @@ describe("Test RoyaltyClient", () => { ); } }); - + it("should throw error when call payRoyaltyOnBehalf given amount is 0", async () => { + try { + await royaltyClient.payRoyaltyOnBehalf({ + receiverIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + payerIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + token: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + amount: 0, + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to pay royalty on behalf: The amount to pay must be number greater than 0.", + ); + } + }); it("should throw payerIpId error when call payRoyaltyOnBehalf given payerIpId is not registered", async () => { sinon .stub(royaltyClient.ipAssetRegistryClient, "isRegistered") @@ -69,24 +98,21 @@ describe("Test RoyaltyClient", () => { } }); - it("should return txHash when call payRoyaltyOnBehalf given correct args", async () => { + it("should return txHash when call payRoyaltyOnBehalf given correct args with erc20", async () => { sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); sinon.stub(royaltyClient.royaltyModuleClient, "payRoyaltyOnBehalf").resolves(txHash); - const result = await royaltyClient.payRoyaltyOnBehalf({ receiverIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", payerIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", - token: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + token: erc20Address[aeneid], amount: 1, }); expect(result.txHash).equals(txHash); }); - it("should convert IP to WIP when paying WIP via payRoyaltyOnBehalf", async () => { sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); - sinon.stub(royaltyClient.wipClient, "balanceOf").resolves({ result: 0n }); - sinon.stub(royaltyClient.wipClient, "allowance").resolves({ result: 200n }); + rpcMock.getBalance = sinon.stub().resolves(150n); const simulateContractStub = sinon.stub().resolves({ request: {} }); rpcMock.simulateContract = simulateContractStub; @@ -104,21 +130,6 @@ describe("Test RoyaltyClient", () => { expect(calls.length).to.equal(2); // deposit and payRoyaltyOnBehalf }); - it("should return txHash when call payRoyaltyOnBehalf given given correct args and waitForTransaction is true", async () => { - sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); - sinon.stub(royaltyClient.royaltyModuleClient, "payRoyaltyOnBehalf").resolves(txHash); - - const result = await royaltyClient.payRoyaltyOnBehalf({ - receiverIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", - payerIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", - token: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", - amount: 1, - txOptions: { waitForTransaction: true }, - }); - - expect(result.txHash).equals(txHash); - }); - it("should return encodedData when call payRoyaltyOnBehalf given correct args and encodedTxDataOnly is true", async () => { sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); sinon.stub(royaltyClient.royaltyModuleClient, "payRoyaltyOnBehalfEncode").returns({ @@ -178,14 +189,405 @@ describe("Test RoyaltyClient", () => { sinon .stub(royaltyClient.royaltyModuleClient, "ipRoyaltyVaults") .resolves("0x73fcb515cee99e4991465ef586cfe2b072ebb512"); - sinon.stub(IpRoyaltyVaultImplReadOnlyClient.prototype, "claimableRevenue").resolves(1); + sinon.stub(IpRoyaltyVaultImplReadOnlyClient.prototype, "claimableRevenue").resolves(1n); const result = await royaltyClient.claimableRevenue({ royaltyVaultIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", claimer: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", token: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", }); - expect(result).equals(1); + expect(result).equals(1n); + }); + }); + + describe("Test royaltyClient.claimAllRevenue", async () => { + it("should throw error when call claimAllRevenue given claimer address is wrong", async () => { + try { + await royaltyClient.claimAllRevenue({ + ancestorIpId: ipId, + claimer: "0x", + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }); + } catch (err) { + expect((err as Error).message).equals("Failed to claim all revenue: Invalid address: 0x."); + } + }); + it("should not return `claimedTokens` when call claimAllRevenue given claimer is neither an IP owned by the wallet nor the wallet address itself", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(mockAddress); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + const result = await royaltyClient.claimAllRevenue({ + ancestorIpId: ipId, + claimer: ipId, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }); + expect(result.claimedTokens).to.be.undefined; + expect(result.txHashes).to.be.an("array").and.lengthOf(1); + expect(result.receipt).to.be.an("object"); + }); + + it("should call transfer and unwrap method when call claimAllRevenue given claimer is an IP owned by the wallet", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { + token: WIP_TOKEN_ADDRESS, + amount: 1n, + claimer: ipId, + }, + ]); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + const result = await royaltyClient.claimAllRevenue({ + ancestorIpId: ipId, + claimer: ipId, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }); + expect(executeStub.calledOnce).to.be.true; + expect(withdrawStub.calledOnce).to.be.true; + expect(result.txHashes).to.be.an("array").and.lengthOf(3); + }); + + it("should only unwrap token when call claimAllRevenue given autoTransfer is false", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { + token: WIP_TOKEN_ADDRESS, + amount: 1n, + claimer: ipId, + }, + ]); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.claimAllRevenue({ + ancestorIpId: ipId, + claimer: ipId, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + claimOptions: { autoTransferAllClaimedTokensFromIp: false }, + }); + expect(result.txHashes).to.be.an("array").and.lengthOf(2); + expect(executeStub.calledOnce).to.be.false; + expect(withdrawStub.calledOnce).to.be.true; + }); + + it("should not unwrap token when call claimAllRevenue given autoUnwrapIpTokens is false", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { + token: WIP_TOKEN_ADDRESS, + amount: 1n, + claimer: ipId, + }, + ]); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.claimAllRevenue({ + ancestorIpId: ipId, + claimer: ipId, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + claimOptions: { autoTransferAllClaimedTokensFromIp: true, autoUnwrapIpTokens: false }, + }); + expect(result.txHashes).to.be.an("array").and.lengthOf(2); + expect(withdrawStub.calledOnce).to.be.false; + expect(executeStub.calledOnce).to.be.true; + }); + }); + + describe("Test royaltyClient.batchClaimAllRevenue", async () => { + it("should throw error when call batchClaimAllRevenue given claimer address is wrong", async () => { + try { + await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: "0x", + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + }); + } catch (err) { + expect((err as Error).message).equals( + "Failed to batch claim all revenue: Invalid address: 0x.", + ); + } + }); + + it("should directly call claimAllRevenue when call batchClaimAllRevenue given only one ancestorIp", async () => { + const claimAllRevenueStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue") + .resolves(txHash); + const multicallStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "multicall") + .resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { + token: WIP_TOKEN_ADDRESS, + amount: 1n, + claimer: ipId, + }, + ]); + sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + }); + expect(claimAllRevenueStub.calledOnce).to.be.true; + expect(multicallStub.calledOnce).to.be.false; + }); + + it("should directly call claimAllRevenue when call batchClaimAllRevenue given useMulticallWhenPossible is false", async () => { + const claimAllRevenueStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue") + .resolves(txHash); + const multicallStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "multicall") + .resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { + token: WIP_TOKEN_ADDRESS, + amount: 1n, + claimer: ipId, + }, + ]); + sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + options: { useMulticallWhenPossible: false }, + }); + expect(claimAllRevenueStub.calledTwice).to.be.true; + expect(multicallStub.calledOnce).to.be.false; + }); + + it("should not return claimedTokens when call batchClaimAllRevenue given claimer is neither an IP owned by the wallet nor the wallet address itself", async () => { + const claimAllRevenueStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "claimAllRevenue") + .resolves(txHash); + const multicallStub = sinon + .stub(royaltyClient.royaltyWorkflowsClient, "multicall") + .resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(mockAddress); + sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([{ token: WIP_TOKEN_ADDRESS, amount: 1n, claimer: ipId }]); + sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + }); + expect(result.txHashes).to.be.an("array").and.lengthOf(1); + expect(result.receipts).to.be.an("array").and.lengthOf(1); + expect(claimAllRevenueStub.calledOnce).to.be.false; + expect(multicallStub.calledOnce).to.be.true; + }); + + it("should not call transfer when call batchClaimAllRevenue given autoTransferAllClaimedTokensFromIp is false", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "multicall").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([{ token: WIP_TOKEN_ADDRESS, amount: 1n, claimer: ipId }]); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + claimOptions: { autoTransferAllClaimedTokensFromIp: false }, + }); + expect(result.txHashes).to.be.an("array").and.lengthOf(2); + expect(result.receipts).to.be.an("array").and.lengthOf(1); + expect(result.claimedTokens).to.be.deep.equal([ + { token: WIP_TOKEN_ADDRESS, amount: 1n, claimer: ipId }, + ]); + expect(executeStub.calledOnce).to.be.false; + expect(withdrawStub.calledOnce).to.be.true; + }); + + it("should not unwrap token when call batchClaimAllRevenue given autoUnwrapIpTokens is false", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "multicall").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { token: WIP_TOKEN_ADDRESS, amount: 1n, claimer: ipId }, + { token: mockAddress, amount: 0n, claimer: ipId }, + { token: mockAddress, amount: 1n, claimer: walletAddress }, + ]); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: ipId, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + claimOptions: { autoUnwrapIpTokens: false }, + }); + expect(result.txHashes).to.be.an("array").and.lengthOf(3); + expect(result.receipts).to.be.an("array").and.lengthOf(1); + expect(executeStub.calledTwice).to.be.true; + expect(withdrawStub.calledOnce).to.be.false; + }); + + it("should return claimedTokens when call batchClaimAllRevenue", async () => { + sinon.stub(royaltyClient.royaltyWorkflowsClient, "multicall").resolves(txHash); + sinon.stub(IpAccountImplClient.prototype, "owner").resolves(walletAddress); + const executeStub = sinon.stub(IpAccountImplClient.prototype, "execute").resolves(txHash); + sinon.stub(royaltyClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon + .stub(royaltyClient.ipRoyaltyVaultImplEventClient, "parseTxRevenueTokenClaimedEvent") + .returns([ + { token: mockAddress, amount: 1n, claimer: walletAddress }, + { token: WIP_TOKEN_ADDRESS, amount: 0n, claimer: walletAddress }, + { token: mockAddress, amount: 1n, claimer: walletAddress }, + ]); + const withdrawStub = sinon.stub(royaltyClient.wrappedIpClient, "withdraw").resolves(txHash); + + const result = await royaltyClient.batchClaimAllRevenue({ + ancestorIps: [ + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + { + ipId, + claimer: walletAddress, + childIpIds: [ipId], + royaltyPolicies: [mockAddress], + currencyTokens: [WIP_TOKEN_ADDRESS], + }, + ], + }); + expect(result.claimedTokens).to.be.deep.equal([ + { token: mockAddress, amount: 2n, claimer: walletAddress }, + { token: WIP_TOKEN_ADDRESS, amount: 0n, claimer: walletAddress }, + ]); + expect(executeStub.calledOnce).to.be.true; + // withdraw is not called because amount is 0 + expect(withdrawStub.calledOnce).to.be.false; + expect(result.txHashes).to.be.an("array").and.lengthOf(2); + expect(result.receipts).to.be.an("array").and.lengthOf(1); }); }); }); diff --git a/packages/core-sdk/test/unit/resources/wip.test.ts b/packages/core-sdk/test/unit/resources/wip.test.ts new file mode 100644 index 000000000..02950ca2a --- /dev/null +++ b/packages/core-sdk/test/unit/resources/wip.test.ts @@ -0,0 +1,225 @@ +import { PublicClient, WalletClient } from "viem"; +import { WipClient } from "../../../src/resources/wip"; +import chaiAsPromised from "chai-as-promised"; +import chai from "chai"; +import * as sinon from "sinon"; +import { createMock } from "../testUtils"; +chai.use(chaiAsPromised); +const expect = chai.expect; +const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; + +describe("WIP Functions", () => { + let wipClient: WipClient; + let rpcMock: PublicClient; + let walletMock: WalletClient; + + before(async () => { + rpcMock = createMock(); + walletMock = createMock(); + wipClient = new WipClient(rpcMock, walletMock); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("deposit", () => { + before(() => { + rpcMock.simulateContract = sinon.stub().resolves({ request: {} }); + walletMock.writeContract = sinon.stub().resolves(txHash); + wipClient = new WipClient(rpcMock, walletMock); + }); + after(() => { + sinon.restore(); + }); + + it("should throw an error when call deposit give amount is less than 0", async () => { + try { + await wipClient.deposit({ + amount: 0, + txOptions: { waitForTransaction: true }, + }); + } catch (error) { + expect((error as Error).message).equals( + "Failed to deposit IP for WIP: WIP deposit amount must be greater than 0.", + ); + } + }); + it("should deposit successfully when call deposit given amount is 1 ", async () => { + const rsp = await wipClient.deposit({ + amount: 1, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + + it("should deposit successfully when call deposit given amount is 1 and waitForTransaction is true", async () => { + const rsp = await wipClient.deposit({ + amount: 1, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + }); + + describe("withdraw", () => { + it("should throw an error when call withdraw given amount is less than 0", async () => { + try { + await wipClient.withdraw({ + amount: 0, + txOptions: { waitForTransaction: true }, + }); + } catch (error) { + expect((error as Error).message).equals( + "Failed to withdraw WIP: WIP withdraw amount must be greater than 0.", + ); + } + }); + + it("should withdraw successfully when call withdraw given amount is 1", async () => { + sinon.stub(wipClient.wrappedIpClient, "withdraw").resolves(txHash); + const rsp = await wipClient.withdraw({ + amount: 1, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + + it("should withdraw successfully when call withdraw given amount is 1 and waitForTransaction is true", async () => { + sinon.stub(wipClient.wrappedIpClient, "withdraw").resolves(txHash); + const rsp = await wipClient.withdraw({ + amount: 1, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + }); + + describe("approve", () => { + it("should throw an error when call approve given amount is 0", async () => { + try { + await wipClient.approve({ + amount: 0, + spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + } catch (error) { + expect((error as Error).message).equals( + "Failed to approve WIP: WIP approve amount must be greater than 0.", + ); + } + }); + + it("should approve successfully when call approve given amount is 1", async () => { + sinon.stub(wipClient.wrappedIpClient, "approve").resolves(txHash); + const rsp = await wipClient.approve({ + amount: 1, + spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + + it("should approve successfully when call approve given amount is 1 and waitForTransaction is true", async () => { + sinon.stub(wipClient.wrappedIpClient, "approve").resolves(txHash); + const rsp = await wipClient.approve({ + amount: 1, + spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + }); + + describe("getBalance", () => { + it("should throw an error when call getBalance given address is invalid", async () => { + try { + await wipClient.balanceOf("0x"); + } catch (error) { + expect((error as Error).message).equals("Invalid address: 0x."); + } + }); + + it("should get balance successfully when call getBalance given address is valid", async () => { + sinon.stub(wipClient.wrappedIpClient, "balanceOf").resolves({ result: 0n }); + const rsp = await wipClient.balanceOf("0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91"); + expect(rsp).to.be.a("bigint"); + }); + }); + + describe("transfer", () => { + it("should throw an error when call transfer given amount is 0", async () => { + try { + await wipClient.transfer({ + amount: 0, + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + } catch (error) { + expect((error as Error).message).equals( + "Failed to transfer WIP: WIP transfer amount must be greater than 0.", + ); + } + }); + + it("should transfer successfully when call transfer given amount is 1", async () => { + sinon.stub(wipClient.wrappedIpClient, "transfer").resolves(txHash); + const rsp = await wipClient.transfer({ + amount: 1, + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + + expect(rsp.txHash).to.be.a("string"); + }); + + it("should transfer successfully when call transfer given amount is 1 and waitForTransaction is true", async () => { + sinon.stub(wipClient.wrappedIpClient, "transfer").resolves(txHash); + const rsp = await wipClient.transfer({ + amount: 1, + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + }); + + describe("transferFrom", () => { + it("should throw an error when call transferFrom given amount is 0", async () => { + try { + await wipClient.transferFrom({ + amount: 0, + from: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + } catch (error) { + expect((error as Error).message).equals( + "Failed to transfer WIP: WIP transfer amount must be greater than 0.", + ); + } + }); + + it("should transfer successfully when call transferFrom given amount is 1", async () => { + sinon.stub(wipClient.wrappedIpClient, "transferFrom").resolves(txHash); + const rsp = await wipClient.transferFrom({ + amount: 1, + from: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + + it("should transfer successfully when call transferFrom given amount is 1 and waitForTransaction is true", async () => { + sinon.stub(wipClient.wrappedIpClient, "transferFrom").resolves(txHash); + const rsp = await wipClient.transferFrom({ + amount: 1, + from: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + }); + }); +}); diff --git a/packages/core-sdk/test/unit/testUtils.ts b/packages/core-sdk/test/unit/testUtils.ts index 58c8a976f..367c65e26 100644 --- a/packages/core-sdk/test/unit/testUtils.ts +++ b/packages/core-sdk/test/unit/testUtils.ts @@ -1,12 +1,13 @@ import { randomBytes } from "crypto"; import sinon from "sinon"; -import { Address, Hex, keccak256, toBytes } from "viem"; +import { Address, Hex, keccak256 } from "viem"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { walletAddress } from "./mockData"; export function createMock(obj = {}): T { const mockObj: any = obj; mockObj.waitForTransactionReceipt = sinon.stub().resolves({}); - mockObj.address = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; + mockObj.address = walletAddress; mockObj.multicall = sinon.stub().returns([{ error: "", status: "success" }]); mockObj.getBlock = sinon.stub().resolves({ timestamp: 1629820800n }); return mockObj; diff --git a/packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts b/packages/core-sdk/test/unit/utils/feeUtils.test.ts similarity index 56% rename from packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts rename to packages/core-sdk/test/unit/utils/feeUtils.test.ts index 1527c84f3..850d65797 100644 --- a/packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts +++ b/packages/core-sdk/test/unit/utils/feeUtils.test.ts @@ -3,26 +3,26 @@ import * as sinon from "sinon"; import chaiAsPromised from "chai-as-promised"; import { Address, LocalAccount, PublicClient, WalletClient, maxUint256, parseEther } from "viem"; import { - Multicall3Client, royaltyModuleAddress, derivativeWorkflowsAddress, - WrappedIpClient, + wrappedIpAddress, + multicall3Address, + erc20Address, } from "../../../src/abi/generated"; import { createMock, generateRandomAddress, generateRandomHash } from "../testUtils"; -import { contractCallWithWipFees } from "../../../src/utils/wipFeeUtils"; -import { ContractCallWithWipFees } from "../../../src/types/utils/wip"; -import { TEST_WALLET_ADDRESS, aeneid } from "../../integration/utils/util"; +import { aeneid, txHash } from "../mockData"; +import { TEST_WALLET_ADDRESS } from "../../integration/utils/util"; import { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; -import { WipClient } from "../../../src/resources/wip"; +import { ContractCallWithFees } from "../../../src/types/utils/wip"; +import { ERC20Client, WipTokenClient } from "../../../src/utils/token"; +import { contractCallWithFees } from "../../../src/utils/feeUtils"; chai.use(chaiAsPromised); const expect = chai.expect; -describe("WIP Fee Utilities", () => { - let wipClient: WrappedIpClient; +describe("Erc20 Token Fee Utilities", () => { let rpcMock: PublicClient; let walletMock: WalletClient; - let multicall3Client: Multicall3Client; let contractCallMock: sinon.SinonStub; let rpcWaitForTxMock: sinon.SinonStub; let walletBalanceMock: sinon.SinonStub; @@ -35,79 +35,75 @@ describe("WIP Fee Utilities", () => { const accountMock = createMock(); walletMock.account = accountMock; walletMock.writeContract = sinon.stub().resolves(generateRandomHash()); - wipClient = createMock(); - multicall3Client = createMock(); rpcWaitForTxMock = rpcMock.waitForTransactionReceipt as sinon.SinonStub; + sinon.stub(WipTokenClient.prototype, "address").get(() => wrappedIpAddress[aeneid]); }); afterEach(() => { sinon.restore(); }); - function getDefaultParams(overrides: Partial): ContractCallWithWipFees { + function getDefaultParams(overrides: Partial): ContractCallWithFees { const hash = generateRandomHash(); contractCallMock = sinon.stub().resolves(hash); return { rpcClient: rpcMock, wallet: walletMock, - multicall3Client: multicall3Client, - wipClient: wipClient, + multicall3Address: multicall3Address[aeneid], totalFees: 0n, - wipSpenders: [], + tokenSpenders: [], contractCall: contractCallMock, encodedTxs: [ { to: generateRandomAddress(), - data: "0x", + data: txHash, }, ], sender: TEST_WALLET_ADDRESS, + options: { + wipOptions: {}, + }, ...overrides, }; } - - describe("contractCallWithWipFees", () => { - let approveMock: sinon.SinonStub; - - beforeEach(() => { - approveMock = sinon.stub().resolves(); - wipClient.approve = approveMock; + describe("No Fees", () => { + it("should call contract directly if no fees", async () => { + const params = getDefaultParams({ totalFees: 0n }); + const { txHash } = await contractCallWithFees(params); + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + expect(txHash).not.to.be.empty; }); - describe("No Fees", () => { - it("should call contract directly if no fees", async () => { - const params = getDefaultParams({ totalFees: 0n }); - const { txHash } = await contractCallWithWipFees(params); - expect(contractCallMock.calledOnce).to.be.true; - expect(rpcWaitForTxMock.notCalled).to.be.true; - expect(txHash).not.to.be.empty; + it("should support wait for tx", async () => { + const params = getDefaultParams({ + totalFees: 0n, + txOptions: { waitForTransaction: true }, }); + const { txHash } = await contractCallWithFees(params); + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.calledOnce).to.be.true; + expect(txHash).not.to.be.empty; + }); + }); - it("should support wait for tx", async () => { - const params = getDefaultParams({ - totalFees: 0n, - txOptions: { waitForTransaction: true }, - }); - const { txHash } = await contractCallWithWipFees(params); - expect(contractCallMock.calledOnce).to.be.true; - expect(rpcWaitForTxMock.calledOnce).to.be.true; - expect(txHash).not.to.be.empty; - }); + describe("contractCallWithFees with wip", () => { + let approveMock: sinon.SinonStub; + + beforeEach(() => { + approveMock = sinon.stub(WipTokenClient.prototype, "approve").resolves(txHash); }); describe("Enough WIP", () => { beforeEach(() => { - wipClient.balanceOf = sinon.stub().resolves({ - result: 200n, - }); + sinon.stub(WipTokenClient.prototype, "balanceOf").resolves(200n); }); - it("should not call approval if disabled via enableAutoApprove", async () => { const params = getDefaultParams({ totalFees: 100n, - wipOptions: { enableAutoApprove: false }, + options: { erc20Options: { enableAutoApprove: false } }, }); - const { txHash, receipt } = await contractCallWithWipFees(params); + const { txHash, receipt } = await contractCallWithFees(params); expect(receipt).to.be.undefined; expect(contractCallMock.calledOnce).to.be.true; expect(rpcWaitForTxMock.notCalled).to.be.true; @@ -118,7 +114,7 @@ describe("WIP Fee Utilities", () => { it("should skip approvals if all spenders have enough allowance", async () => { const params = getDefaultParams({ totalFees: 100n, - wipSpenders: [ + tokenSpenders: [ { address: royaltyModuleAddress[aeneid], amount: 50n, @@ -130,25 +126,17 @@ describe("WIP Fee Utilities", () => { ], txOptions: { waitForTransaction: false }, }); - const allowanceMock = sinon.stub().resolves({ - result: 50n, - }); - wipClient.allowance = allowanceMock; + const allowanceMock = sinon.stub(WipTokenClient.prototype, "allowance").resolves(50n); - const { txHash, receipt } = await contractCallWithWipFees(params); + const { txHash, receipt } = await contractCallWithFees(params); expect(receipt).to.be.undefined; expect(allowanceMock.calledTwice).to.be.true; expect( - allowanceMock.firstCall.calledWith({ - owner: TEST_WALLET_ADDRESS, - spender: params.wipSpenders[0].address, - }), + allowanceMock.firstCall.calledWith(TEST_WALLET_ADDRESS, params.tokenSpenders[0].address), ).to.be.true; + expect( - allowanceMock.secondCall.calledWith({ - owner: TEST_WALLET_ADDRESS, - spender: params.wipSpenders[1].address, - }), + allowanceMock.secondCall.calledWith(TEST_WALLET_ADDRESS, params.tokenSpenders[1].address), ).to.be.true; expect(contractCallMock.calledOnce).to.be.true; expect(rpcWaitForTxMock.notCalled).to.be.true; @@ -159,7 +147,7 @@ describe("WIP Fee Utilities", () => { it("should call separate approvals for each spender address if not enough allowance", async () => { const params = getDefaultParams({ totalFees: 100n, - wipSpenders: [ + tokenSpenders: [ { address: royaltyModuleAddress[aeneid], amount: 10n, @@ -171,20 +159,13 @@ describe("WIP Fee Utilities", () => { ], txOptions: { waitForTransaction: true }, }); - const allowanceMock = sinon.stub().resolves({ - result: 15n, - }); - wipClient.allowance = allowanceMock; - const { txHash, receipt } = await contractCallWithWipFees(params); + sinon.stub(WipTokenClient.prototype, "allowance").resolves(15n); + const { txHash, receipt } = await contractCallWithFees(params); expect(receipt).not.to.be.undefined; expect(contractCallMock.calledOnce).to.be.true; expect(approveMock.calledOnce).to.be.true; - expect( - approveMock.firstCall.calledWith({ - spender: derivativeWorkflowsAddress[aeneid], - amount: maxUint256, - }), - ).to.be.true; + expect(approveMock.firstCall.calledWith(derivativeWorkflowsAddress[aeneid], maxUint256)).to + .be.true; expect(rpcWaitForTxMock.callCount).to.equal(2); // 1 approval + 1 contract call expect(txHash).not.to.be.empty; }); @@ -192,19 +173,17 @@ describe("WIP Fee Utilities", () => { describe("Enough IP, not enough WIP", () => { let simulateContractMock: sinon.SinonStub; - let params: ContractCallWithWipFees; + let params: ContractCallWithFees; beforeEach(() => { - wipClient.balanceOf = sinon.stub().resolves({ - result: 1n, - }); + sinon.stub(WipTokenClient.prototype, "balanceOf").resolves(1n); walletBalanceMock.resolves(1_000); simulateContractMock = sinon.stub().resolves({ request: {} }); rpcMock.simulateContract = simulateContractMock; rpcMock; params = getDefaultParams({ totalFees: 100n, - wipSpenders: [ + tokenSpenders: [ { address: royaltyModuleAddress[aeneid], amount: 20n, @@ -215,26 +194,23 @@ describe("WIP Fee Utilities", () => { }, ], }); - const allowanceMock = sinon.stub().resolves({ - result: 50n, - }); - wipClient.allowance = allowanceMock; + sinon.stub(WipTokenClient.prototype, "allowance").resolves(50n); }); it("should error if enableAutoWrapIp is false", async () => { await expect( - contractCallWithWipFees({ + contractCallWithFees({ ...params, - wipOptions: { enableAutoWrapIp: false }, + options: { wipOptions: { enableAutoWrapIp: false } }, }), ).to.be.rejectedWith(/^Wallet does not have enough WIP to pay for fees./); }); describe("no multicall", () => { it("should deposit, approve, and call contract separately", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipOptions: { useMulticallWhenPossible: false }, + options: { wipOptions: { useMulticallWhenPossible: false } }, }); expect(receipt).to.be.undefined; expect(simulateContractMock.calledOnce).to.be.true; @@ -256,9 +232,11 @@ describe("WIP Fee Utilities", () => { }); it("should support wait for tx", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipOptions: { useMulticallWhenPossible: false }, + options: { + wipOptions: { useMulticallWhenPossible: false }, + }, txOptions: { waitForTransaction: true }, }); expect(receipt).not.to.be.undefined; @@ -270,9 +248,11 @@ describe("WIP Fee Utilities", () => { }); it("should not call approval if enableAutoApprove is false", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipOptions: { enableAutoApprove: false, useMulticallWhenPossible: false }, + options: { + wipOptions: { enableAutoApprove: false, useMulticallWhenPossible: false }, + }, }); expect(receipt).to.be.undefined; expect(txHash).not.to.be.empty; @@ -283,9 +263,9 @@ describe("WIP Fee Utilities", () => { }); it("should not call approval if spender has enough allowance", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipSpenders: [ + tokenSpenders: [ { address: royaltyModuleAddress[aeneid], amount: 20n, @@ -295,7 +275,9 @@ describe("WIP Fee Utilities", () => { amount: 10n, }, ], - wipOptions: { useMulticallWhenPossible: false }, + options: { + wipOptions: { useMulticallWhenPossible: false }, + }, }); expect(receipt).to.be.undefined; expect(txHash).not.to.be.empty; @@ -311,20 +293,18 @@ describe("WIP Fee Utilities", () => { let approveEncodeMock: sinon.SinonStub; beforeEach(() => { - depositEncodeMock = sinon.stub().returns({ + depositEncodeMock = sinon.stub(WipTokenClient.prototype, "depositEncode").returns({ to: generateRandomAddress(), - data: "", + data: txHash, }); - wipClient.depositEncode = depositEncodeMock; - approveEncodeMock = sinon.stub().returns({ + approveEncodeMock = sinon.stub(WipTokenClient.prototype, "approveEncode").returns({ to: generateRandomAddress(), - data: "", + data: txHash, }); - wipClient.approveEncode = approveEncodeMock; }); it("should deposit, approve, and call contract in one multicall", async () => { - const { txHash, receipt } = await contractCallWithWipFees(params); + const { txHash, receipt } = await contractCallWithFees(params); expect(receipt).to.be.undefined; expect(txHash).not.to.be.empty; expect(depositEncodeMock.calledOnce).to.be.true; @@ -345,7 +325,7 @@ describe("WIP Fee Utilities", () => { }); it("should support wait for tx", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, txOptions: { waitForTransaction: true }, }); @@ -358,9 +338,11 @@ describe("WIP Fee Utilities", () => { }); it("should not include approvals if enableAutoApprove is false", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipOptions: { enableAutoApprove: false }, + options: { + wipOptions: { enableAutoApprove: false }, + }, }); expect(receipt).to.be.undefined; expect(txHash).not.to.be.empty; @@ -372,9 +354,9 @@ describe("WIP Fee Utilities", () => { }); it("should only include approves if enough allowances", async () => { - const { txHash, receipt } = await contractCallWithWipFees({ + const { txHash, receipt } = await contractCallWithFees({ ...params, - wipSpenders: [ + tokenSpenders: [ { address: royaltyModuleAddress[aeneid], amount: 20n, @@ -400,18 +382,117 @@ describe("WIP Fee Utilities", () => { const totalFees = parseEther("1"); beforeEach(() => { - wipClient.balanceOf = sinon.stub().resolves({ - result: parseEther("0.1"), - }); walletBalanceMock.resolves(parseEther("0.1")); + sinon.stub(WipTokenClient.prototype, "balanceOf").resolves(0n); }); - it("should throw error indicating not enough funds to complete", async () => { + it("should throw error indicating not enough wip funds to complete given token is wip", async () => { const params = getDefaultParams({ totalFees }); - await expect(contractCallWithWipFees(params)).to.be.rejectedWith( + await expect(contractCallWithFees(params)).to.be.rejectedWith( "Wallet does not have enough IP to wrap to WIP and pay for fees. Total fees: 1IP, balance: 0.1IP", ); }); }); }); + + describe("contractCallWithFees with erc20 token", () => { + let approveMock: sinon.SinonStub; + let allowanceMock: sinon.SinonStub; + + beforeEach(() => { + approveMock = sinon.stub(ERC20Client.prototype, "approve").resolves(txHash); + allowanceMock = sinon.stub(ERC20Client.prototype, "allowance").resolves(0n); + sinon.stub(ERC20Client.prototype, "balanceOf").resolves(100n); + }); + + it("should not call approval if disabled via enableAutoApprove and enough erc20 token", async () => { + const params = getDefaultParams({ + totalFees: 100n, + token: erc20Address[aeneid], + options: { + erc20Options: { enableAutoApprove: false }, + }, + }); + const { txHash, receipt } = await contractCallWithFees(params); + expect(receipt).to.be.undefined; + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + expect(approveMock.notCalled).to.be.true; + expect(txHash).not.to.be.empty; + }); + + it("should skip approvals if all spenders have sufficient allowance and enough erc20 token", async () => { + const params = getDefaultParams({ + totalFees: 100n, + token: erc20Address[aeneid], + tokenSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 50n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 50n, + }, + ], + txOptions: { waitForTransaction: false }, + }); + allowanceMock.resolves(1001n); + + const { txHash, receipt } = await contractCallWithFees(params); + expect(receipt).to.be.undefined; + expect(allowanceMock.calledTwice).to.be.true; + expect( + allowanceMock.firstCall.calledWith(TEST_WALLET_ADDRESS, params.tokenSpenders[0].address), + ).to.be.true; + + expect( + allowanceMock.secondCall.calledWith(TEST_WALLET_ADDRESS, params.tokenSpenders[1].address), + ).to.be.true; + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + expect(approveMock.notCalled).to.be.true; + expect(txHash).not.to.be.empty; + }); + + it("should call separate approvals for each spender address if not enough allowance and enough erc20 token", async () => { + const params = getDefaultParams({ + totalFees: 100n, + token: erc20Address[aeneid], + tokenSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 20n, + }, + { + address: royaltyModuleAddress[aeneid], + amount: 22n, + }, + { + address: multicall3Address[aeneid], + amount: 22n, + }, + ], + txOptions: { waitForTransaction: true }, + }); + allowanceMock.resolves(15n); + const { txHash, receipt } = await contractCallWithFees(params); + expect(receipt).not.to.be.undefined; + expect(contractCallMock.calledOnce).to.be.true; + expect(approveMock.calledTwice).to.be.true; + expect(approveMock.firstCall.calledWith(royaltyModuleAddress[aeneid], maxUint256)).to.be.true; + expect(rpcWaitForTxMock.callCount).to.equal(3); // 2 approval + 1 contract call + expect(txHash).not.to.be.empty; + }); + + it("should throw not enough erc20 token when call contractCallWithFees given erc20 token is not enough", async () => { + const params = getDefaultParams({ + totalFees: 101n, + token: erc20Address[aeneid], + }); + await expect(contractCallWithFees(params)).to.be.rejectedWith( + "Wallet does not have enough erc20 token to pay for fees. Total fees: 0.000000000000000101IP, balance: 0.0000000000000001IP.", + ); + }); + }); }); diff --git a/packages/core-sdk/test/unit/utils/licenseTermsHelper.test.ts b/packages/core-sdk/test/unit/utils/licenseTermsHelper.test.ts index 787973a10..c694f55a8 100644 --- a/packages/core-sdk/test/unit/utils/licenseTermsHelper.test.ts +++ b/packages/core-sdk/test/unit/utils/licenseTermsHelper.test.ts @@ -5,13 +5,31 @@ import { getRevenueShare, validateLicenseTerms, } from "../../../src/utils/licenseTermsHelper"; -import { expect } from "chai"; -import { MockERC20 } from "../../integration/utils/mockERC20"; +import chai from "chai"; import sinon from "sinon"; import { createMock } from "../testUtils"; -const { RoyaltyModuleReadOnlyClient } = require("../../../src/abi/generated"); +import chaiAsPromised from "chai-as-promised"; +import { RoyaltyModuleReadOnlyClient } from "../../../src/abi/generated"; + +chai.use(chaiAsPromised); +const expect = chai.expect; describe("License Terms Helper", () => { + let isWhitelistedRoyaltyTokenStub: sinon.SinonStub; + let isWhitelistedRoyaltyPolicyStub: sinon.SinonStub; + beforeEach(() => { + isWhitelistedRoyaltyTokenStub = sinon + .stub(RoyaltyModuleReadOnlyClient.prototype, "isWhitelistedRoyaltyToken") + .resolves(true); + isWhitelistedRoyaltyPolicyStub = sinon + .stub(RoyaltyModuleReadOnlyClient.prototype, "isWhitelistedRoyaltyPolicy") + .resolves(true); + }); + + afterEach(() => { + sinon.restore(); + }); + describe("getLicenseTermByType", () => { it("it should return no commercial license terms when call getLicenseTermByType given NON_COMMERCIAL_REMIX", async () => { const result = getLicenseTermByType(PIL_TYPE.NON_COMMERCIAL_REMIX); @@ -32,7 +50,7 @@ describe("License Terms Helper", () => { expiration: 0n, defaultMintingFee: 0n, royaltyPolicy: "0x0000000000000000000000000000000000000000", - uri: "", + uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json", }); }); @@ -61,18 +79,6 @@ describe("License Terms Helper", () => { ).to.throw("DefaultMintingFee, currency are required for commercial use PIL."); }); - it("it should throw when call getLicenseTermByType given COMMERCIAL_USE and wrong royaltyAddress", async () => { - expect(() => - getLicenseTermByType(PIL_TYPE.COMMERCIAL_USE, { - royaltyPolicyAddress: "wrong" as Hex, - defaultMintingFee: "1", - currency: zeroAddress, - }), - ).to.throw( - `term.royaltyPolicyLAPAddress address is invalid: wrong, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.`, - ); - }); - it("it should return commercial license terms when call getLicenseTermByType given COMMERCIAL_USE and correct args", async () => { const result = getLicenseTermByType(PIL_TYPE.COMMERCIAL_USE, { royaltyPolicyAddress: zeroAddress, @@ -88,15 +94,15 @@ describe("License Terms Helper", () => { commercializerCheckerData: "0x0000000000000000000000000000000000000000", currency: "0x0000000000000000000000000000000000000000", derivativeRevCeiling: 0n, - derivativesAllowed: true, + derivativesAllowed: false, derivativesApproval: false, - derivativesAttribution: true, + derivativesAttribution: false, derivativesReciprocal: false, expiration: 0n, defaultMintingFee: 1n, royaltyPolicy: "0x0000000000000000000000000000000000000000", transferable: true, - uri: "", + uri: "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json", }); }); }); @@ -132,19 +138,6 @@ describe("License Terms Helper", () => { ); }); - it("it should throw when call getLicenseTermByType given COMMERCIAL_REMIX and wrong royaltyAddress", async () => { - expect(() => - getLicenseTermByType(PIL_TYPE.COMMERCIAL_REMIX, { - royaltyPolicyAddress: "wrong" as Hex, - defaultMintingFee: "1", - currency: zeroAddress, - commercialRevShare: 100, - }), - ).to.throw( - `term.royaltyPolicyLAPAddress address is invalid: wrong, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.`, - ); - }); - it("it should throw when call getLicenseTermByType given COMMERCIAL_REMIX without commercialRevShare ", async () => { expect(() => getLicenseTermByType(PIL_TYPE.COMMERCIAL_REMIX, { @@ -181,7 +174,7 @@ describe("License Terms Helper", () => { defaultMintingFee: 1n, royaltyPolicy: "0x0000000000000000000000000000000000000000", transferable: true, - uri: "", + uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json", }); }); it("it throw commercialRevShare error when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is less than 0 ", async () => { @@ -192,7 +185,7 @@ describe("License Terms Helper", () => { currency: zeroAddress, commercialRevShare: -8, }), - ).to.throw(`CommercialRevShare should be between 0 and 100.`); + ).to.throw(`CommercialRevShare must be between 0 and 100.`); }); it("it throw commercialRevShare error when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is greater than 100", async () => { @@ -203,7 +196,7 @@ describe("License Terms Helper", () => { currency: zeroAddress, commercialRevShare: 105, }), - ).to.throw(`CommercialRevShare should be between 0 and 100.`); + ).to.throw(`CommercialRevShare must be between 0 and 100.`); }); it("it get commercialRevShare correct value when call getLicenseTermByType given COMMERCIAL_REMIX and commercialRevShare is 10", async () => { @@ -254,9 +247,7 @@ describe("License Terms Helper", () => { }, rpcMock, ), - ).to.be.rejectedWith( - "params.royaltyPolicy address is invalid: 0x, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", - ); + ).to.be.rejectedWith("Invalid address: 0x"); }); it("should throw defaultMintingFee error when call validateLicenseTerms given defaultMintingFee is less than 0", async () => { await expect( @@ -283,9 +274,7 @@ describe("License Terms Helper", () => { ).to.be.rejectedWith("Royalty policy is required when defaultMintingFee is greater than 0."); }); it("should throw royalty whitelist error when call validateLicenseTerms with invalid royalty whitelist address", async () => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(false); + isWhitelistedRoyaltyPolicyStub.resolves(false); await expect( validateLicenseTerms( { @@ -298,9 +287,6 @@ describe("License Terms Helper", () => { }); it("should throw currency error when call validateLicenseTerms with invalid currency address", async () => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(true); await expect( validateLicenseTerms( { @@ -309,18 +295,11 @@ describe("License Terms Helper", () => { }, rpcMock, ), - ).to.be.rejectedWith( - "params.currency address is invalid: 0x, Address must be a hex value of 20 bytes (40 hex characters) and match its checksum counterpart.", - ); + ).to.be.rejectedWith("Invalid address: 0x"); }); it("should throw currency whitelist error when call validateLicenseTerms with invalid currency whitelist address", async () => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(true); - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyToken = sinon - .stub() - .resolves(false); + isWhitelistedRoyaltyTokenStub.resolves(false); await expect( validateLicenseTerms( { @@ -333,10 +312,6 @@ describe("License Terms Helper", () => { }); it("should throw royalty policy requires currency token error when call validateLicenseTerms given royaltyPolicy is not zero address and current is zero address", async () => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(true); - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyToken = sinon.stub().resolves(true); await expect( validateLicenseTerms( { @@ -350,14 +325,6 @@ describe("License Terms Helper", () => { }); describe("verify commercial use", () => { - beforeEach(() => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(true); - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyToken = sinon - .stub() - .resolves(true); - }); it("should throw commercialAttribution error when call validateLicenseTerms given commercialUse is false and commercialAttribution is true", async () => { await expect( validateLicenseTerms( @@ -463,14 +430,6 @@ describe("License Terms Helper", () => { }); describe("verify derivatives", () => { - beforeEach(() => { - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyPolicy = sinon - .stub() - .resolves(true); - RoyaltyModuleReadOnlyClient.prototype.isWhitelistedRoyaltyToken = sinon - .stub() - .resolves(true); - }); it("should throw derivativesAttribution error when call validateLicenseTerms given derivativesAllowed is false and derivativesAttribution is true", async () => { await expect( validateLicenseTerms( @@ -543,13 +502,11 @@ describe("License Terms Helper", () => { }); it("should throw error when call getRevenueShare given revShare is less than 0", async () => { - expect(() => getRevenueShare(-1)).to.throw("CommercialRevShare should be between 0 and 100."); + expect(() => getRevenueShare(-1)).to.throw("CommercialRevShare must be between 0 and 100."); }); it("should throw error when call getRevenueShare given revShare is greater than 100", async () => { - expect(() => getRevenueShare(101)).to.throw( - "CommercialRevShare should be between 0 and 100.", - ); + expect(() => getRevenueShare(101)).to.throw("CommercialRevShare must be between 0 and 100."); }); it("should return correct value when call getRevenueShare given revShare is 10", async () => { diff --git a/packages/core-sdk/test/unit/utils/validateLicenseConfig.test.ts b/packages/core-sdk/test/unit/utils/validateLicenseConfig.test.ts index 9f265428f..cc515b587 100644 --- a/packages/core-sdk/test/unit/utils/validateLicenseConfig.test.ts +++ b/packages/core-sdk/test/unit/utils/validateLicenseConfig.test.ts @@ -77,4 +77,18 @@ describe("validateLicenseConfig", () => { "The mintingFee must be greater than 0.", ); }); + + it("should throw default value when licensingConfig is not provided", () => { + const result = validateLicenseConfig(); + expect(result).to.deep.equal({ + isSet: false, + mintingFee: 0n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }); + }); }); diff --git a/packages/wagmi-generator/wagmi.config.ts b/packages/wagmi-generator/wagmi.config.ts index a7a481046..017a072f8 100644 --- a/packages/wagmi-generator/wagmi.config.ts +++ b/packages/wagmi-generator/wagmi.config.ts @@ -185,8 +185,11 @@ export default defineConfig(async () => { [mainnetChainId]: "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89", }, }, + // IMPORTANT: This ERC20 contract must have an ABI that exactly matches Viem's ERC20 ABI implementation. + // This contract's ABI will be used as the standard ERC20 ABI throughout the application for + // common operations like balanceOf, allowance, approve, etc. { - name: "MockERC20", + name: "ERC20", address: { [aeneidChainId]: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", [mainnetChainId]: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", @@ -238,6 +241,7 @@ export default defineConfig(async () => { "raiseDispute", "resolveDispute", "isWhitelistedDisputeTag", + "tagIfRelatedIpInfringed", ], IPAccountImpl: [ "execute", @@ -287,7 +291,7 @@ export default defineConfig(async () => { ], RoyaltyPolicyLAP: ["onRoyaltyPayment", "getRoyaltyData"], LicenseToken: ["ownerOf"], - SPG: ["CollectionCreated"], + SPG: ["CollectionCreated", "mintFee", "mintFeeToken"], GroupingWorkflows: [ "mintAndRegisterIpAndAttachLicenseAndAddToGroup", "registerIpAndAttachLicenseAndAddToGroup", @@ -315,7 +319,7 @@ export default defineConfig(async () => { "mintAndRegisterIpAndAttachPILTerms", "multicall", ], - RoyaltyWorkflows: ["claimAllRevenue"], + RoyaltyWorkflows: ["claimAllRevenue", "multicall"], Multicall3: ["aggregate3"], RoyaltyTokenDistributionWorkflows: [ "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", @@ -324,8 +328,23 @@ export default defineConfig(async () => { "distributeRoyaltyTokens", "registerIpAndMakeDerivativeAndDeployRoyaltyVault", ], - ArbitrationPolicyUMA: ["maxBonds", "maxLiveness", "minLiveness"], - AA: ["deposit", "approve"], + ArbitrationPolicyUMA: [ + "maxBonds", + "maxLiveness", + "minLiveness", + "disputeIdToAssertionId", + "disputeAssertion", + ], + WrappedIP: [ + "deposit", + "approve", + "transferFrom", + "transfer", + "balanceOf", + "withdraw", + "allowance", + ], + ERC20: ["approve", "balanceOf", "allowance", "transferFrom", "mint"], }, }), ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26803fecc..6a3d2a61f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12490,4 +12490,4 @@ packages: dependencies: react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1) - dev: true + dev: true \ No newline at end of file