diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 476acd57..1bab326f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @LeoHChen @DonFungible @edisonz0718 @jacob-tucker @AndyBoWu @allenchuang +* @LeoHChen @DonFungible @edisonz0718 @jacob-tucker @AndyBoWu @allenchuang @bpolania diff --git a/packages/core-sdk/.vscode/launch.json b/packages/core-sdk/.vscode/launch.json new file mode 100644 index 00000000..c12ad042 --- /dev/null +++ b/packages/core-sdk/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run Test File", + "outputCapture": "std", + "program": "${workspaceRoot}/node_modules/mocha/bin/mocha.js", + "args": ["-r", "ts-node/register", "${relativeFile}"], + "console": "integratedTerminal", + "env": { + "TS_NODE_PROJECT": "./tsconfig.test.json" + } + } + ] +} diff --git a/packages/core-sdk/package.json b/packages/core-sdk/package.json index 083da22c..d017a31b 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.1", + "version": "1.3.0-beta.2", "description": "Story Protocol Core SDK", "main": "dist/story-protocol-core-sdk.cjs.js", "module": "dist/story-protocol-core-sdk.esm.js", @@ -19,7 +19,7 @@ "build": "pnpm run fix && preconstruct build", "test": "pnpm run test:unit", "test:unit": "TS_NODE_PROJECT='./tsconfig.test.json' c8 --all --src ./src mocha -r ts-node/register './test/unit/**/*.test.ts'", - "test:integration": "TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register './test/integration/**/*.test.ts' --timeout 240000", + "test:integration": "TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register './test/integration/**/*.test.ts' --timeout 300000", "fix": "pnpm run format:fix && pnpm run lint:fix", "format": "prettier --check .", "format:fix": "prettier --write .", diff --git a/packages/core-sdk/src/abi/generated.ts b/packages/core-sdk/src/abi/generated.ts index 1faaacd1..11b5d3a5 100644 --- a/packages/core-sdk/src/abi/generated.ts +++ b/packages/core-sdk/src/abi/generated.ts @@ -344,7 +344,7 @@ export const accessControllerAbi = [ ], }, ], - name: "setBatchPermissions", + name: "setBatchTransientPermissions", outputs: [], stateMutability: "nonpayable", }, @@ -357,7 +357,7 @@ export const accessControllerAbi = [ { name: "func", internalType: "bytes4", type: "bytes4" }, { name: "permission", internalType: "uint8", type: "uint8" }, ], - name: "setPermission", + name: "setTransientPermission", outputs: [], stateMutability: "nonpayable", }, @@ -15270,7 +15270,7 @@ export class AccessControllerClient extends AccessControllerEventClient { const { request: call } = await this.rpcClient.simulateContract({ abi: accessControllerAbi, address: this.address, - functionName: "setBatchPermissions", + functionName: "setBatchTransientPermissions", account: this.wallet.account, args: [request.permissions], }); @@ -15290,7 +15290,7 @@ export class AccessControllerClient extends AccessControllerEventClient { to: this.address, data: encodeFunctionData({ abi: accessControllerAbi, - functionName: "setBatchPermissions", + functionName: "setBatchTransientPermissions", args: [request.permissions], }), }; @@ -15308,7 +15308,7 @@ export class AccessControllerClient extends AccessControllerEventClient { const { request: call } = await this.rpcClient.simulateContract({ abi: accessControllerAbi, address: this.address, - functionName: "setPermission", + functionName: "setTransientPermission", account: this.wallet.account, args: [request.ipAccount, request.signer, request.to, request.func, request.permission], }); @@ -15326,7 +15326,7 @@ export class AccessControllerClient extends AccessControllerEventClient { to: this.address, data: encodeFunctionData({ abi: accessControllerAbi, - functionName: "setPermission", + functionName: "setTransientPermission", args: [request.ipAccount, request.signer, request.to, request.func, request.permission], }), }; @@ -20672,6 +20672,14 @@ export class IpAccountImplReadOnlyClient { functionName: "token", }); } + + public async owner(): Promise
{ + return await this.rpcClient.readContract({ + abi: ipAccountImplAbi, + address: this.address, + functionName: "owner", + }); + } } /** diff --git a/packages/core-sdk/src/client.ts b/packages/core-sdk/src/client.ts index 48c5fae1..848b1201 100644 --- a/packages/core-sdk/src/client.ts +++ b/packages/core-sdk/src/client.ts @@ -12,11 +12,12 @@ import { PermissionClient } from "./resources/permission"; import { LicenseClient } from "./resources/license"; import { DisputeClient } from "./resources/dispute"; import { IPAccountClient } from "./resources/ipAccount"; -import { chain, chainStringToViemChain } from "./utils/utils"; +import { chain, chainStringToViemChain, validateAddress } from "./utils/utils"; import { RoyaltyClient } from "./resources/royalty"; import { NftClient } from "./resources/nftClient"; import { GroupClient } from "./resources/group"; import { SimpleWalletClient } from "./abi/generated"; +import { WipClient } from "./resources/wip"; if (typeof process !== "undefined") { dotenv.config(); @@ -36,6 +37,7 @@ export class StoryClient { private _royalty: RoyaltyClient | null = null; private _nftClient: NftClient | null = null; private _group: GroupClient | null = null; + private _wip: WipClient | null = null; /** * @param config - the configuration for the SDK client @@ -43,7 +45,7 @@ export class StoryClient { private constructor(config: StoryConfig) { this.config = { ...config, - chainId: chain[config.chainId || "homer"], + chainId: chain[config.chainId || "aeneid"], }; if (!this.config.transport) { throw new Error( @@ -218,4 +220,25 @@ export class StoryClient { return this._group; } + + public get wipClient(): WipClient { + if (this._wip === null) { + this._wip = new WipClient(this.rpcClient, this.wallet); + } + return this._wip; + } + + public async getWalletBalance(): Promise { + if (!this.wallet.account) { + throw new Error("No account found in wallet"); + } + return await this.getBalance(this.wallet.account.address); + } + + public async getBalance(address: string): Promise { + const validAddress = validateAddress(address); + return await this.rpcClient.getBalance({ + address: validAddress, + }); + } } diff --git a/packages/core-sdk/src/constants/common.ts b/packages/core-sdk/src/constants/common.ts index c3e49a5a..1af4b6e7 100644 --- a/packages/core-sdk/src/constants/common.ts +++ b/packages/core-sdk/src/constants/common.ts @@ -3,5 +3,8 @@ import { Hex } from "viem"; export const AddressZero = "0x0000000000000000000000000000000000000000"; export const HashZero = "0x0000000000000000000000000000000000000000000000000000000000000000"; export const defaultFunctionSelector: Hex = "0x00000000"; -export const royaltySharesTotalSupply: number = 100000000; -export const MAX_ROYALTY_TOKEN = 100000000; +export const royaltySharesTotalSupply: number = 100_000_000; +export const MAX_ROYALTY_TOKEN = 100_000_000; + +/** Address for the WIP contract. This address is fixed */ +export const WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000"; diff --git a/packages/core-sdk/src/index.ts b/packages/core-sdk/src/index.ts index b61a9de7..b70811f2 100644 --- a/packages/core-sdk/src/index.ts +++ b/packages/core-sdk/src/index.ts @@ -1,6 +1,6 @@ export { StoryClient } from "./client"; export { AddressZero, HashZero } from "./constants/common"; -export { homer } from "./utils/chain"; +export { aeneid } from "./utils/chain"; export { IPAssetClient } from "./resources/ipAsset"; export { PermissionClient } from "./resources/permission"; export { LicenseClient } from "./resources/license"; diff --git a/packages/core-sdk/src/resources/ipAsset.ts b/packages/core-sdk/src/resources/ipAsset.ts index 35c10b37..e28d1936 100644 --- a/packages/core-sdk/src/resources/ipAsset.ts +++ b/packages/core-sdk/src/resources/ipAsset.ts @@ -60,6 +60,7 @@ import { InternalDerivativeData, LicenseTermsData, DerivativeData, + CommonRegistrationHandlerParams, } from "../types/resources/ipAsset"; import { AccessControllerClient, @@ -69,6 +70,7 @@ import { DerivativeWorkflowsMintAndRegisterIpAndMakeDerivativeWithLicenseTokensRequest, DerivativeWorkflowsRegisterIpAndMakeDerivativeRequest, DerivativeWorkflowsRegisterIpAndMakeDerivativeWithLicenseTokensRequest, + Erc20TokenClient, IpAccountImplClient, IpAssetRegistryClient, IpRoyaltyVaultImplReadOnlyClient, @@ -86,9 +88,11 @@ import { RegistrationWorkflowsRegisterIpRequest, RoyaltyModuleEventClient, RoyaltyTokenDistributionWorkflowsClient, + RoyaltyTokenDistributionWorkflowsMintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensRequest, RoyaltyTokenDistributionWorkflowsMintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest, RoyaltyTokenDistributionWorkflowsRegisterIpAndMakeDerivativeAndDeployRoyaltyVaultRequest, SimpleWalletClient, + SpgnftImplReadOnlyClient, coreMetadataModuleAbi, ipAccountImplAbi, ipRoyaltyVaultImplAbi, @@ -107,6 +111,12 @@ import { getFunctionSignature } from "../utils/getFunctionSignature"; import { LicensingConfig } 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"; export class IPAssetClient { public licensingModuleClient: LicensingModuleClient; @@ -122,10 +132,13 @@ export class IPAssetClient { public multicall3Client: Multicall3Client; public royaltyTokenDistributionWorkflowsClient: RoyaltyTokenDistributionWorkflowsClient; public royaltyModuleEventClient: RoyaltyModuleEventClient; + public wipClient: Erc20TokenClient; + public spgNftClient: SpgnftImplReadOnlyClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; private readonly chainId: SupportedChainIds; + private readonly walletAddress: Address; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: SupportedChainIds) { this.licensingModuleClient = new LicensingModuleClient(rpcClient, wallet); @@ -143,10 +156,13 @@ export class IPAssetClient { wallet, ); this.royaltyModuleEventClient = new RoyaltyModuleEventClient(rpcClient); + this.wipClient = new Erc20TokenClient(rpcClient, wallet); this.multicall3Client = new Multicall3Client(rpcClient, wallet); + this.spgNftClient = new SpgnftImplReadOnlyClient(rpcClient); this.rpcClient = rpcClient; this.wallet = wallet; this.chainId = chainId; + this.walletAddress = this.wallet.account!.address; } /** @@ -394,6 +410,11 @@ export class IPAssetClient { allowFailure: false, callData: encodedTxData, }); + if (isSpg) { + // todo(bonnie): update this to use multicall from the spg instead of + // multicall3 client since SPG now requires the sender to the signature signer + throw new Error("Batch register IP with metadata is not supported."); + } } const txHash = await this.multicall3Client.aggregate3({ calls: contracts }); if (request.txOptions?.waitForTransaction) { @@ -669,28 +690,29 @@ export class IPAssetClient { allowDuplicates: request.allowDuplicates, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), }; + + const encodedTxData = + this.licenseAttachmentWorkflowsClient.mintAndRegisterIpAndAttachPilTermsEncode(object); if (request.txOptions?.encodedTxDataOnly) { - return { - encodedTxData: - this.licenseAttachmentWorkflowsClient.mintAndRegisterIpAndAttachPilTermsEncode(object), - }; + return { encodedTxData }; + } + const contractCall = () => { + return this.licenseAttachmentWorkflowsClient.mintAndRegisterIpAndAttachPilTerms(object); + }; + const rsp = await this.commonRegistrationHandler({ + wipOptions: request.wipOptions, + sender: this.walletAddress, + spgNftContract: object.spgNftContract, + spgSpenderAddress: this.royaltyTokenDistributionWorkflowsClient.address, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); + if (rsp.receipt) { + const licenseTermsIds = await this.getLicenseTermsId(licenseTerms); + return { ...rsp, licenseTermsIds }; } else { - const txHash = - await this.licenseAttachmentWorkflowsClient.mintAndRegisterIpAndAttachPilTerms(object); - if (request.txOptions?.waitForTransaction) { - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const ipIdAndTokenId = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0]; - const licenseTermsIds = await this.getLicenseTermsId(licenseTerms); - return { - txHash, - ...ipIdAndTokenId, - licenseTermsIds, - }; - } - return { txHash }; + return rsp; } } catch (error) { handleError(error, "Failed to mint and register IP and attach PIL terms"); @@ -988,28 +1010,32 @@ export class IPAssetClient { tokenId: BigInt(request.tokenId), derivData, sigMetadataAndRegister: { - signer: getAddress(this.wallet.account!.address, "wallet.account.address"), + signer: this.walletAddress, deadline: calculatedDeadline, signature, }, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), }; + const encodedTxData = + this.derivativeWorkflowsClient.registerIpAndMakeDerivativeEncode(object); if (request.txOptions?.encodedTxDataOnly) { - return { - encodedTxData: this.derivativeWorkflowsClient.registerIpAndMakeDerivativeEncode(object), - }; - } else { - const txHash = await this.derivativeWorkflowsClient.registerIpAndMakeDerivative(object); - if (request.txOptions?.waitForTransaction) { - const receipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const log = this.getIpIdAndTokenIdsFromEvent(receipt)[0]; - return { txHash, ...log }; - } - return { txHash }; + return { encodedTxData }; } + const contractCall = () => { + return this.derivativeWorkflowsClient.registerIpAndMakeDerivative(object); + }; + return this.commonRegistrationHandler({ + wipOptions: { + ...request.wipOptions, + useMulticallWhenPossible: false, + }, + sender: this.walletAddress, + spgSpenderAddress: this.derivativeWorkflowsClient.address, + derivData, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); } catch (error) { handleError(error, "Failed to register derivative IP"); } @@ -1042,35 +1068,36 @@ export class IPAssetClient { ): Promise { try { const derivData = await this.validateDerivativeData(request.derivData); + const recipient = + (request.recipient && getAddress(request.recipient, "request.recipient")) || + this.walletAddress; + const spgNftContract = getAddress(request.spgNftContract, "spgNftContract"); const object: DerivativeWorkflowsMintAndRegisterIpAndMakeDerivativeRequest = { ...request, derivData, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), - recipient: - (request.recipient && getAddress(request.recipient, "request.recipient")) || - this.wallet.account!.address, + recipient, allowDuplicates: request.allowDuplicates, + spgNftContract, }; - + const encodedTxData = + this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeEncode(object); if (request.txOptions?.encodedTxDataOnly) { - return { - encodedTxData: - this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeEncode(object), - }; - } else { - const txHash = await this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivative( - object, - ); - if (request.txOptions?.waitForTransaction) { - const receipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const log = this.getIpIdAndTokenIdsFromEvent(receipt)[0]; - return { txHash, childIpId: log.ipId, tokenId: log.tokenId }; - } - return { txHash }; + return { encodedTxData }; } + const contractCall = () => { + return this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivative(object); + }; + return this.commonRegistrationHandler({ + wipOptions: request.wipOptions, + sender: this.walletAddress, + spgSpenderAddress: this.derivativeWorkflowsClient.address, + spgNftContract, + derivData, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); } catch (error) { handleError(error, "Failed to mint and register IP and make derivative"); } @@ -1316,7 +1343,7 @@ export class IPAssetClient { spgNftContract: getAddress(request.spgNftContract, "request.spgNftContract"), recipient: (request.recipient && getAddress(request.recipient, "request.recipient")) || - this.wallet.account!.address, + this.walletAddress, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), licenseTokenIds: licenseTokenIds, royaltyContext: zeroAddress, @@ -1324,28 +1351,33 @@ export class IPAssetClient { allowDuplicates: request.allowDuplicates, }; this.validateMaxRts(object.maxRts); + + const encodedTxData = + this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokensEncode( + object, + ); if (request.txOptions?.encodedTxDataOnly) { - return { - encodedTxData: - this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokensEncode( - object, - ), - }; - } else { - const txHash = - await this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( - object, - ); - if (request.txOptions?.waitForTransaction) { - const receipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const log = this.getIpIdAndTokenIdsFromEvent(receipt)[0]; - return { txHash, ...log }; - } - return { txHash }; + return { encodedTxData }; } + const contractCall = async () => { + return this.derivativeWorkflowsClient.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens( + object, + ); + }; + return this.commonRegistrationHandler({ + wipOptions: { + ...request.wipOptions, + // need to disable multicall to avoid needing to transfer the license + // token to the multicall contract. + useMulticallWhenPossible: false, + }, + sender: this.walletAddress, + spgNftContract: object.spgNftContract, + spgSpenderAddress: this.derivativeWorkflowsClient.address, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); } catch (error) { handleError(error, "Failed to mint and register IP and make derivative with license tokens"); } @@ -1655,7 +1687,7 @@ export class IPAssetClient { ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), derivData, sigMetadataAndRegister: { - signer: this.wallet.account!.address, + signer: this.walletAddress, deadline: calculatedDeadline, signature: signature, }, @@ -1665,17 +1697,32 @@ export class IPAssetClient { if (isRegistered) { throw new Error(`The NFT with id ${request.tokenId} is already registered as IP.`); } - const txHash = - await this.royaltyTokenDistributionWorkflowsClient.registerIpAndMakeDerivativeAndDeployRoyaltyVault( + const encodedTxData = + this.royaltyTokenDistributionWorkflowsClient.registerIpAndMakeDerivativeAndDeployRoyaltyVaultEncode( object, ); - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, + const contractCall = () => { + return this.royaltyTokenDistributionWorkflowsClient.registerIpAndMakeDerivativeAndDeployRoyaltyVault( + object, + ); + }; + const { txHash, ipId, tokenId, receipt } = await this.commonRegistrationHandler({ + wipOptions: { + ...request.wipOptions, + useMulticallWhenPossible: false, + }, + sender: this.walletAddress, + spgSpenderAddress: this.royaltyTokenDistributionWorkflowsClient.address, + derivData, + encodedTxs: [encodedTxData], + contractCall, + txOptions: { ...request.txOptions, waitForTransaction: true }, }); - const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0]; + if (tokenId === undefined || ipId === undefined) { + throw new Error("Failed to register derivative ip and deploy royalty vault"); + } const { ipRoyaltyVault } = this.royaltyModuleEventClient - .parseTxIpRoyaltyVaultDeployedEvent(txReceipt) + .parseTxIpRoyaltyVaultDeployedEvent(receipt) .filter((item) => item.ipId === ipId)[0]; const distributeRoyaltyTokensTxHash = await this.distributeRoyaltyTokens({ ipId, @@ -1762,37 +1809,48 @@ export class IPAssetClient { request.licenseTermsData, ); const { royaltyShares } = this.getRoyaltyShares(request.royaltyShares); - const txHash = - await this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens( - { - spgNftContract: getAddress(request.spgNftContract, "request.spgNftContract"), - recipient: - (request.recipient && getAddress(request.recipient, "request.recipient")) || - this.wallet.account!.address, - ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), - licenseTermsData, - royaltyShares, - allowDuplicates: request.allowDuplicates, - }, - ); - if (request.txOptions?.waitForTransaction) { - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0]; - const licenseTermsIds = await this.getLicenseTermsId(licenseTerms); - const { ipRoyaltyVault } = - this.royaltyModuleEventClient.parseTxIpRoyaltyVaultDeployedEvent(txReceipt)[0]; - return { - txHash, - ipId, - licenseTermsIds, - ipRoyaltyVault, - tokenId, + const object: RoyaltyTokenDistributionWorkflowsMintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensRequest = + { + spgNftContract: getAddress(request.spgNftContract, "request.spgNftContract"), + recipient: + (request.recipient && getAddress(request.recipient, "request.recipient")) || + this.walletAddress, + ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), + licenseTermsData, + royaltyShares, + allowDuplicates: request.allowDuplicates, }; + const encodedTxData = + this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokensEncode( + object, + ); + const contractCall = () => { + return this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens( + object, + ); + }; + const { txHash, ipId, tokenId, receipt } = await this.commonRegistrationHandler({ + wipOptions: request.wipOptions, + sender: this.walletAddress, + spgNftContract: object.spgNftContract, + spgSpenderAddress: this.royaltyTokenDistributionWorkflowsClient.address, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); + if (!receipt) { + return { txHash }; } - return { txHash }; + const licenseTermsIds = await this.getLicenseTermsId(licenseTerms); + const { ipRoyaltyVault } = + this.royaltyModuleEventClient.parseTxIpRoyaltyVaultDeployedEvent(receipt)[0]; + return { + txHash, + ipId, + licenseTermsIds, + ipRoyaltyVault, + tokenId, + }; } catch (error) { handleError( error, @@ -1829,33 +1887,40 @@ export class IPAssetClient { request: MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest, ): Promise { try { + const nftRecipient = + (request.recipient && getAddress(request.recipient, "request.recipient")) || + this.walletAddress; const { royaltyShares } = this.getRoyaltyShares(request.royaltyShares); const derivData = await this.validateDerivativeData(request.derivData); const object: RoyaltyTokenDistributionWorkflowsMintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest = { spgNftContract: getAddress(request.spgNftContract, "request.spgNftContract"), - recipient: - (request.recipient && getAddress(request.recipient, "request.recipient")) || - this.wallet.account!.address, + recipient: nftRecipient, ipMetadata: getIpMetadataForWorkflow(request.ipMetadata), derivData, royaltyShares: royaltyShares, allowDuplicates: request.allowDuplicates, }; - const txHash = - await this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + const encodedTxData = + this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensEncode( object, ); - if (request.txOptions?.waitForTransaction) { - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0]; - return { txHash, ipId, tokenId }; - } - return { txHash }; + const contractCall = () => { + return this.royaltyTokenDistributionWorkflowsClient.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + object, + ); + }; + return this.commonRegistrationHandler({ + spgNftContract: object.spgNftContract, + wipOptions: request.wipOptions, + sender: this.walletAddress, + spgSpenderAddress: this.royaltyTokenDistributionWorkflowsClient.address, + derivData, + encodedTxs: [encodedTxData], + contractCall, + txOptions: request.txOptions, + }); } catch (error) { handleError( error, @@ -2093,4 +2158,80 @@ export class IPAssetClient { } return { licenseTerms, licenseTermsData: processedLicenseTermsData }; } + + private async commonRegistrationHandler({ + sender, + derivData, + spgNftContract, + spgSpenderAddress, + txOptions, + wipOptions, + encodedTxs, + contractCall, + }: CommonRegistrationHandlerParams) { + let totalFees = 0n; + const wipSpenders: WipSpender[] = []; + + // get spg minting fee + if (spgNftContract) { + const nftMintFee = await calculateSPGWipMintFee( + new SpgnftImplReadOnlyClient(this.rpcClient, spgNftContract), + ); + totalFees += nftMintFee; + wipSpenders.push({ + address: spgNftContract, + amount: nftMintFee, + }); + } + + // get derivative minting fee + if (derivData) { + let totalDerivativeMintingFee = 0n; + for (let i = 0; i < derivData.parentIpIds.length; i++) { + const derivativeMintingFee = await calculateLicenseWipMintFee({ + multicall3Client: this.multicall3Client, + licenseTemplateClient: this.licenseTemplateClient, + licensingModuleClient: this.licensingModuleClient, + parentIpId: derivData.parentIpIds[i], + licenseTermsId: derivData.licenseTermsIds[i], + receiver: sender, + amount: 1n, + }); + totalDerivativeMintingFee += derivativeMintingFee; + } + totalFees += totalDerivativeMintingFee; + if (totalDerivativeMintingFee > 0) { + wipSpenders.push({ + address: spgSpenderAddress, + amount: totalDerivativeMintingFee, + }); + } + } + + if (totalFees < 0) { + throw new Error( + `Total fees for registering derivative should never be negative: ${totalFees}`, + ); + } + + const { txHash, receipt } = await contractCallWithWipFees({ + totalFees, + wipOptions, + multicall3Client: this.multicall3Client, + rpcClient: this.rpcClient, + wipClient: this.wipClient, + wipSpenders, + contractCall, + sender, + wallet: this.wallet, + txOptions, + encodedTxs, + }); + if (receipt) { + const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(receipt)[0]; + return { txHash, ipId, tokenId, receipt }; + } else { + return { txHash }; + } + } } diff --git a/packages/core-sdk/src/resources/license.ts b/packages/core-sdk/src/resources/license.ts index f34972c1..0873e992 100644 --- a/packages/core-sdk/src/resources/license.ts +++ b/packages/core-sdk/src/resources/license.ts @@ -1,6 +1,7 @@ -import { PublicClient, zeroAddress } from "viem"; +import { Address, PublicClient, zeroAddress } from "viem"; import { + Erc20TokenClient, IpAssetRegistryClient, LicenseRegistryEventClient, LicenseRegistryReadOnlyClient, @@ -10,6 +11,7 @@ import { LicensingModulePredictMintingLicenseFeeResponse, LicensingModuleSetLicensingConfigRequest, ModuleRegistryReadOnlyClient, + Multicall3Client, PiLicenseTemplateClient, PiLicenseTemplateGetLicenseTermsResponse, PiLicenseTemplateReadOnlyClient, @@ -42,6 +44,8 @@ import { } from "../utils/licenseTermsHelper"; import { chain, getAddress } from "../utils/utils"; import { SupportedChainIds } from "../types/config"; +import { calculateLicenseWipMintFee, contractCallWithWipFees } from "../utils/wipFeeUtils"; +import { WipSpender } from "../types/utils/wip"; export class LicenseClient { public licenseRegistryClient: LicenseRegistryEventClient; @@ -51,9 +55,12 @@ export class LicenseClient { public licenseTemplateClient: PiLicenseTemplateClient; public licenseRegistryReadOnlyClient: LicenseRegistryReadOnlyClient; public moduleRegistryReadOnlyClient: ModuleRegistryReadOnlyClient; + public multicall3Client: Multicall3Client; + public wipClient: Erc20TokenClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; private readonly chainId: SupportedChainIds; + private readonly walletAddress: Address; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: SupportedChainIds) { this.licensingModuleClient = new LicensingModuleClient(rpcClient, wallet); @@ -63,9 +70,12 @@ export class LicenseClient { this.licenseRegistryReadOnlyClient = new LicenseRegistryReadOnlyClient(rpcClient); this.ipAssetRegistryClient = new IpAssetRegistryClient(rpcClient, wallet); this.moduleRegistryReadOnlyClient = new ModuleRegistryReadOnlyClient(rpcClient); + this.multicall3Client = new Multicall3Client(rpcClient, wallet); + this.wipClient = new Erc20TokenClient(rpcClient, wallet); this.rpcClient = rpcClient; this.wallet = wallet; this.chainId = chainId; + this.walletAddress = wallet.account!.address; } /** * Registers new license terms and return the ID of the newly registered license terms. @@ -364,6 +374,9 @@ export class LicenseClient { request: MintLicenseTokensRequest, ): Promise { try { + const receiver = + (request.receiver && getAddress(request.receiver, "request.receiver")) || + this.walletAddress; const req: LicensingModuleMintLicenseTokensRequest = { licensorIpId: getAddress(request.licensorIpId, "request.licensorIpId"), licenseTemplate: @@ -372,9 +385,7 @@ export class LicenseClient { this.licenseTemplateClient.address, licenseTermsId: BigInt(request.licenseTermsId), amount: BigInt(request.amount || 1), - receiver: - (request.receiver && getAddress(request.receiver, "request.receiver")) || - this.wallet.account!.address, + receiver, royaltyContext: zeroAddress, maxMintingFee: BigInt(request.maxMintingFee), maxRevenueShare: getRevenueShare(request.maxRevenueShare), @@ -408,26 +419,54 @@ export class LicenseClient { `License terms id ${request.licenseTermsId} is not attached to the IP with id ${request.licensorIpId}.`, ); } + const encodedTxData = this.licensingModuleClient.mintLicenseTokensEncode(req); if (request.txOptions?.encodedTxDataOnly) { - return { encodedTxData: this.licensingModuleClient.mintLicenseTokensEncode(req) }; - } else { - const txHash = await this.licensingModuleClient.mintLicenseTokens(req); - if (request.txOptions?.waitForTransaction) { - const txReceipt = await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - const targetLogs = this.licensingModuleClient.parseTxLicenseTokensMintedEvent(txReceipt); - const startLicenseTokenId = targetLogs[0].startLicenseTokenId; - const licenseTokenIds = []; - for (let i = 0; i < req.amount; i++) { - licenseTokenIds.push(startLicenseTokenId + BigInt(i)); - } - return { txHash: txHash, licenseTokenIds: licenseTokenIds }; - } else { - return { txHash: txHash }; - } + return { encodedTxData }; + } + + // get license token minting fee + const licenseMintingFee = await calculateLicenseWipMintFee({ + multicall3Client: this.multicall3Client, + licenseTemplateClient: this.licenseTemplateClient, + licensingModuleClient: this.licensingModuleClient, + parentIpId: req.licensorIpId, + licenseTermsId: req.licenseTermsId, + receiver, + amount: req.amount, + }); + + const wipSpenders: WipSpender[] = []; + if (licenseMintingFee > 0n) { + wipSpenders.push({ + address: this.licensingModuleClient.address, + amount: licenseMintingFee, + }); + } + const { txHash, receipt } = await contractCallWithWipFees({ + totalFees: licenseMintingFee, + wipOptions: request.wipOptions, + multicall3Client: this.multicall3Client, + rpcClient: this.rpcClient, + wipClient: this.wipClient, + wipSpenders, + contractCall: () => { + return this.licensingModuleClient.mintLicenseTokens(req); + }, + wallet: this.wallet, + sender: this.walletAddress, + txOptions: request.txOptions, + encodedTxs: [encodedTxData], + }); + if (!receipt) { + return { txHash }; + } + const targetLogs = this.licensingModuleClient.parseTxLicenseTokensMintedEvent(receipt); + const startLicenseTokenId = targetLogs[0].startLicenseTokenId; + const licenseTokenIds = []; + for (let i = 0; i < req.amount; i++) { + licenseTokenIds.push(startLicenseTokenId + BigInt(i)); } + return { txHash, licenseTokenIds: licenseTokenIds, receipt }; } catch (error) { handleError(error, "Failed to mint license tokens"); } diff --git a/packages/core-sdk/src/resources/permission.ts b/packages/core-sdk/src/resources/permission.ts index 49cea69a..5e1981e1 100644 --- a/packages/core-sdk/src/resources/permission.ts +++ b/packages/core-sdk/src/resources/permission.ts @@ -111,7 +111,7 @@ export class PermissionClient { const ipAccountClient = new IpAccountImplClient(this.rpcClient, this.wallet, ipId); const data = encodeFunctionData({ abi: accessControllerAbi, - functionName: "setPermission", + functionName: "setTransientPermission", args: [ ipId, getAddress(signer, "request.signer"), @@ -279,7 +279,7 @@ export class PermissionClient { const ipAccountClient = new IpAccountImplClient(this.rpcClient, this.wallet, ipId); const data = encodeFunctionData({ abi: accessControllerAbi, - functionName: "setBatchPermissions", + functionName: "setBatchTransientPermissions", args: [ permissions.map((permission) => ({ ipAccount: permission.ipId, diff --git a/packages/core-sdk/src/resources/royalty.ts b/packages/core-sdk/src/resources/royalty.ts index f2fcadeb..d97b0636 100644 --- a/packages/core-sdk/src/resources/royalty.ts +++ b/packages/core-sdk/src/resources/royalty.ts @@ -1,21 +1,44 @@ -import { Address, Hex, PublicClient, zeroAddress } from "viem"; +import { + Address, + decodeEventLog, + encodeFunctionData, + erc20Abi, + Hex, + PublicClient, + TransactionReceipt, + zeroAddress, +} from "viem"; import { handleError } from "../utils/errors"; import { ClaimableRevenueRequest, ClaimableRevenueResponse, + ClaimAllRevenueRequest, + ClaimAllRevenueResponse, + ClaimedToken, PayRoyaltyOnBehalfRequest, PayRoyaltyOnBehalfResponse, + TransferClaimedTokensFromIpToWalletParams, } from "../types/resources/royalty"; import { + Erc20TokenClient, + IpAccountImplClient, IpAssetRegistryClient, + ipRoyaltyVaultImplAbi, IpRoyaltyVaultImplEventClient, IpRoyaltyVaultImplReadOnlyClient, + Multicall3Client, RoyaltyModuleClient, + royaltyWorkflowsAbi, + royaltyWorkflowsAddress, SimpleWalletClient, } from "../abi/generated"; import { IPAccountClient } from "./ipAccount"; -import { getAddress } from "../utils/utils"; +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"; export class RoyaltyClient { public royaltyModuleClient: RoyaltyModuleClient; @@ -23,8 +46,11 @@ export class RoyaltyClient { public ipAccountClient: IPAccountClient; public ipRoyaltyVaultImplReadOnlyClient: IpRoyaltyVaultImplReadOnlyClient; public ipRoyaltyVaultImplEventClient: IpRoyaltyVaultImplEventClient; + public multicall3Client: Multicall3Client; + public wipClient: Erc20TokenClient; private readonly rpcClient: PublicClient; private readonly wallet: SimpleWalletClient; + private readonly walletAddress: Address; constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { this.royaltyModuleClient = new RoyaltyModuleClient(rpcClient, wallet); @@ -32,8 +58,81 @@ export class RoyaltyClient { this.ipRoyaltyVaultImplReadOnlyClient = new IpRoyaltyVaultImplReadOnlyClient(rpcClient); this.ipRoyaltyVaultImplEventClient = new IpRoyaltyVaultImplEventClient(rpcClient); this.ipAccountClient = new IPAccountClient(rpcClient, wallet); + this.multicall3Client = new Multicall3Client(rpcClient, wallet); + this.wipClient = new Erc20TokenClient(rpcClient, wallet); this.rpcClient = rpcClient; this.wallet = wallet; + this.walletAddress = wallet.account!.address; + } + + public async claimAllRevenue(req: ClaimAllRevenueRequest): Promise { + try { + const ancestorIpId = validateAddress(req.ancestorIpId); + const claimer = validateAddress(req.claimer); + 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[] = []; + 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; + } + + // if wallet does not own the claimer then we cannot auto claim or unwrap + if (!ownsClaimer) { + return { receipt, txHashes }; + } + + const claimedTokens = this.getClaimedTokensFromReceipt(receipt!); + const skipTransfer = req.claimOptions?.autoTransferAllClaimedTokensFromIp === false; + const skipUnwrapIp = req.claimOptions?.autoUnwrapIpTokens === false; + + // transfer claimed tokens from IP to wallet if wallet owns IP + if (!skipTransfer && 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 }); + } + } + return { receipt, claimedTokens, txHashes }; + } catch (error) { + handleError(error, "Failed to claim all revenue"); + } } /** @@ -50,7 +149,12 @@ export class RoyaltyClient { request: PayRoyaltyOnBehalfRequest, ): Promise { try { - const { receiverIpId, payerIpId, token, amount } = request; + const { receiverIpId, payerIpId, token, amount, 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"), }); @@ -71,19 +175,39 @@ export class RoyaltyClient { token: getAddress(token, "request.token"), amount: BigInt(amount), }; + + const encodedTxData = this.royaltyModuleClient.payRoyaltyOnBehalfEncode(req); if (request.txOptions?.encodedTxDataOnly) { - return { encodedTxData: this.royaltyModuleClient.payRoyaltyOnBehalfEncode(req) }; + return { encodedTxData }; + } + 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 this.royaltyModuleClient.payRoyaltyOnBehalf(req); - if (request.txOptions?.waitForTransaction) { - await this.rpcClient.waitForTransactionReceipt({ - ...request.txOptions, - hash: txHash, - }); - return { txHash }; - } else { - return { txHash }; - } + const txHash = await contractCall(); + return { txHash }; } } catch (error) { handleError(error, "Failed to pay royalty on behalf"); @@ -129,4 +253,66 @@ 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[] = []; + const transferToken = async (token: Address, amount: bigint) => { + if (amount <= 0) { + return; + } + const hash = await ipAccount.execute({ + to: token, + value: BigInt(0), + operation: 0, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [this.walletAddress, amount], + }), + }); + 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; + } } diff --git a/packages/core-sdk/src/resources/wip.ts b/packages/core-sdk/src/resources/wip.ts new file mode 100644 index 00000000..b294a448 --- /dev/null +++ b/packages/core-sdk/src/resources/wip.ts @@ -0,0 +1,100 @@ +import { Address, Hex, PublicClient, WriteContractParameters } from "viem"; + +import { handleError } from "../utils/errors"; +import { Erc20TokenClient, SimpleWalletClient, erc20TokenAbi } from "../abi/generated"; +import { validateAddress } from "../utils/utils"; +import { WIP_TOKEN_ADDRESS } from "../constants/common"; +import { ApproveRequest, DepositRequest, WithdrawRequest } from "../types/resources/wip"; +import { handleTxOptions } from "../utils/txOptions"; + +export class WipClient { + public wipClient: Erc20TokenClient; + private readonly rpcClient: PublicClient; + private readonly wallet: SimpleWalletClient; + + constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { + this.wipClient = new Erc20TokenClient(rpcClient, wallet, WIP_TOKEN_ADDRESS); + this.rpcClient = rpcClient; + this.wallet = wallet; + } + + /** + * Wraps the selected amount of IP to WIP. + * The WIP will be deposited to the wallet that transferred the IP. + */ + public async deposit({ amount, txOptions }: DepositRequest) { + try { + if (amount <= 0) { + throw new Error("WIP deposit amount must be greater than 0."); + } + const { request: call } = await this.rpcClient.simulateContract({ + abi: erc20TokenAbi, + address: WIP_TOKEN_ADDRESS, + functionName: "deposit", + account: this.wallet.account, + value: BigInt(amount), + }); + const txHash = await this.wallet.writeContract(call as WriteContractParameters); + return handleTxOptions({ + txHash, + txOptions, + rpcClient: this.rpcClient, + }); + } catch (error) { + handleError(error, "Failed to deposit IP for WIP"); + } + } + + /** + * Unwraps the selected amount of WIP to IP. + */ + public async withdraw({ amount, txOptions }: WithdrawRequest) { + try { + const targetAmt = BigInt(amount); + if (targetAmt <= 0) { + throw new Error("WIP withdraw amount must be greater than 0."); + } + const txHash = await this.wipClient.withdraw({ value: targetAmt }); + return handleTxOptions({ + txHash, + txOptions, + rpcClient: this.rpcClient, + }); + } catch (error) { + handleError(error, "Failed to withdraw WIP"); + } + } + + /** + * Approve a spender to use the wallet's WIP balance. + */ + public async approve(req: ApproveRequest): Promise<{ txHash: Hex }> { + 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({ + spender, + amount, + }); + return handleTxOptions({ + txHash, + txOptions: req.txOptions, + rpcClient: this.rpcClient, + }); + } catch (error) { + handleError(error, "Failed to approve WIP"); + } + } + + /** + * Returns the balance of WIP for an address. + */ + public async balanceOf(addr: Address): Promise { + const owner = validateAddress(addr); + const ret = await this.wipClient.balanceOf({ owner }); + return ret.result; + } +} diff --git a/packages/core-sdk/src/types/common.ts b/packages/core-sdk/src/types/common.ts index 18eaf645..8de88073 100644 --- a/packages/core-sdk/src/types/common.ts +++ b/packages/core-sdk/src/types/common.ts @@ -1,6 +1,6 @@ import { Address, Hex } from "viem"; -import { TxOptions } from "./options"; +import { WithTxOptions } from "./options"; import { IpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow"; export type TypedData = { @@ -8,9 +8,8 @@ export type TypedData = { data: unknown[]; }; -export type IpMetadataAndTxOption = { +export type IpMetadataAndTxOptions = WithTxOptions & { ipMetadata?: Partial; - txOptions?: TxOptions; }; export type LicensingConfig = { @@ -29,3 +28,9 @@ export type InnerLicensingConfig = { 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; diff --git a/packages/core-sdk/src/types/config.ts b/packages/core-sdk/src/types/config.ts index e6f0e3d7..14a475d3 100644 --- a/packages/core-sdk/src/types/config.ts +++ b/packages/core-sdk/src/types/config.ts @@ -7,7 +7,7 @@ import { SimpleWalletClient } from "../abi/generated"; * * @public */ -export type SupportedChainIds = "1315" | "homer"; +export type SupportedChainIds = "1315" | "aeneid"; /** * Configuration for the SDK Client. diff --git a/packages/core-sdk/src/types/options.ts b/packages/core-sdk/src/types/options.ts index 083a8d16..e629b498 100644 --- a/packages/core-sdk/src/types/options.ts +++ b/packages/core-sdk/src/types/options.ts @@ -1,6 +1,6 @@ import { WaitForTransactionReceiptParameters } from "viem"; -export interface TxOptions extends Omit { +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). @@ -9,8 +9,8 @@ export interface TxOptions extends Omit = T & { +export type WithTxOptions = { txOptions?: TxOptions; }; diff --git a/packages/core-sdk/src/types/resources/group.ts b/packages/core-sdk/src/types/resources/group.ts index 6189c2ef..81f15e3a 100644 --- a/packages/core-sdk/src/types/resources/group.ts +++ b/packages/core-sdk/src/types/resources/group.ts @@ -2,7 +2,7 @@ import { Address } from "viem"; import { TxOptions } from "../options"; import { EncodedTxData } from "../../abi/generated"; -import { InnerLicensingConfig, IpMetadataAndTxOption, LicensingConfig } from "../common"; +import { InnerLicensingConfig, IpMetadataAndTxOptions, LicensingConfig } from "../common"; export type LicenseData = { licenseTermsId: string | bigint | number; @@ -24,7 +24,7 @@ export type MintAndRegisterIpAndAttachLicenseAndAddToGroupRequest = { licenseData: LicenseData[]; recipient?: Address; deadline?: string | number | bigint; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions; export type MintAndRegisterIpAndAttachLicenseAndAddToGroupResponse = { txHash?: string; @@ -50,7 +50,7 @@ export type RegisterIpAndAttachLicenseAndAddToGroupRequest = { deadline?: bigint; licenseData: LicenseData[]; maxAllowedRewardShare: number | string; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions; export type RegisterIpAndAttachLicenseAndAddToGroupResponse = { txHash?: string; diff --git a/packages/core-sdk/src/types/resources/ipAsset.ts b/packages/core-sdk/src/types/resources/ipAsset.ts index ab9ea4c7..879f5ebc 100644 --- a/packages/core-sdk/src/types/resources/ipAsset.ts +++ b/packages/core-sdk/src/types/resources/ipAsset.ts @@ -1,10 +1,11 @@ -import { Address, Hex } from "viem"; +import { Address, Hash, Hex, TransactionReceipt } from "viem"; import { TxOptions } from "../options"; import { RegisterPILTermsRequest } from "./license"; import { EncodedTxData } from "../../abi/generated"; -import { IpMetadataAndTxOption, LicensingConfig } from "../common"; +import { IpMetadataAndTxOptions, LicensingConfig } from "../common"; import { IpMetadataForWorkflow } from "../../utils/getIpMetadataForWorkflow"; +import { WithWipOptions } from "../utils/wip"; export type DerivativeData = { parentIpIds: Address[]; @@ -24,17 +25,14 @@ export type InternalDerivativeData = { licenseTemplate: Address; }; export type RegisterIpResponse = { - txHash?: Hex; encodedTxData?: EncodedTxData; - ipId?: Address; - tokenId?: bigint; -}; +} & CommonRegistrationResponse; export type RegisterRequest = { nftContract: Address; tokenId: string | number | bigint; deadline?: string | number | bigint; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions; export type RegisterDerivativeWithLicenseTokensRequest = { childIpId: Address; @@ -67,13 +65,15 @@ export type MintAndRegisterIpAssetWithPilTermsRequest = { licenseTermsData: LicenseTermsData[]; recipient?: Address; royaltyPolicyAddress?: Address; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions & + WithWipOptions; export type MintAndRegisterIpAssetWithPilTermsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; ipId?: Address; tokenId?: bigint; + receipt?: TransactionReceipt; licenseTermsIds?: bigint[]; }; @@ -87,13 +87,15 @@ export type RegisterIpAndMakeDerivativeRequest = { deadline: bigint | string | number; signature: Hex; }; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions & + WithWipOptions; export type RegisterIpAndMakeDerivativeResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; ipId?: Address; tokenId?: bigint; + receipt?: TransactionReceipt; }; export type RegisterIpAndAttachPilTermsRequest = { @@ -101,7 +103,7 @@ export type RegisterIpAndAttachPilTermsRequest = { tokenId: bigint | string | number; licenseTermsData: LicenseTermsData[]; deadline?: bigint | number | string; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions; export type RegisterIpAndAttachPilTermsResponse = { txHash?: Hex; @@ -116,14 +118,13 @@ export type MintAndRegisterIpAndMakeDerivativeRequest = { derivData: DerivativeData; recipient?: Address; allowDuplicates: boolean; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions & + WithWipOptions; export type MintAndRegisterIpAndMakeDerivativeResponse = { - txHash?: Hex; encodedTxData?: EncodedTxData; - childIpId?: Address; - tokenId?: bigint; -}; +} & CommonRegistrationResponse; + export type IpRelationship = { parentIpId: Address; type: string; @@ -220,7 +221,7 @@ export type MintAndRegisterIpRequest = { spgNftContract: Address; recipient?: Address; allowDuplicates: boolean; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions; export type RegisterPilTermsAndAttachRequest = { ipId: Address; @@ -241,7 +242,8 @@ export type MintAndRegisterIpAndMakeDerivativeWithLicenseTokensRequest = { recipient?: Address; maxRts: number | string; allowDuplicates: boolean; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions & + WithWipOptions; export type RegisterIpAndMakeDerivativeWithLicenseTokensRequest = { nftContract: Address; @@ -249,7 +251,8 @@ export type RegisterIpAndMakeDerivativeWithLicenseTokensRequest = { licenseTokenIds: string[] | bigint[] | number[]; deadline?: string | number | bigint; maxRts: number | string; -} & IpMetadataAndTxOption; +} & IpMetadataAndTxOptions & + WithWipOptions; export type BatchMintAndRegisterIpAssetWithPilTermsRequest = { args: Omit[]; @@ -333,7 +336,7 @@ export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensReq royaltyShares: RoyaltyShare[]; ipMetadata?: IpMetadataForWorkflow; txOptions?: Omit; -}; +} & WithWipOptions; export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse = { registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: Address; @@ -353,7 +356,8 @@ export type MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensRequest royaltyShares: RoyaltyShare[]; recipient?: Address; txOptions?: Omit; -} & IPMetadataInfo; +} & IPMetadataInfo & + WithWipOptions; export type MintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokensResponse = { txHash: Hex; @@ -369,10 +373,29 @@ export type MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensRequest allowDuplicates: boolean; recipient?: Address; txOptions?: Omit; -} & IPMetadataInfo; +} & IPMetadataInfo & + WithWipOptions; export type MintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokensResponse = { txHash: Hex; ipId?: Address; tokenId?: bigint; }; + +export type CommonRegistrationHandlerParams = WithWipOptions & { + contractCall: () => Promise; + encodedTxs: EncodedTxData[]; + spgNftContract?: Address; + /** the spg contract in which the minting fee is paid to */ + spgSpenderAddress: Address; + derivData?: InternalDerivativeData; + sender: Address; + txOptions?: TxOptions; +}; + +export type CommonRegistrationResponse = { + txHash?: Hex; + ipId?: Address; + tokenId?: bigint; + receipt?: TransactionReceipt; +}; diff --git a/packages/core-sdk/src/types/resources/license.ts b/packages/core-sdk/src/types/resources/license.ts index 0fa3e880..7c40c83c 100644 --- a/packages/core-sdk/src/types/resources/license.ts +++ b/packages/core-sdk/src/types/resources/license.ts @@ -1,8 +1,9 @@ -import { Address } from "viem"; +import { Address, TransactionReceipt } from "viem"; -import { TxOptions } from "../options"; +import { WithTxOptions, TxOptions } from "../options"; import { EncodedTxData } from "../../abi/generated"; import { LicensingConfig } from "../common"; +import { WithWipOptions } from "../utils/wip"; export type LicenseApiResponse = { data: License; @@ -39,7 +40,7 @@ export type LicenseTerms = { commercializerChecker: Address; /*The data to be passed to the commercializer checker contract.*/ commercializerCheckerData: Address; - /*Percentage of revenue that must be shared with the licensor.*/ + /**Percentage of revenue that must be shared with the licensor. Must be from 0-100.*/ commercialRevShare: number; /*The maximum revenue that can be generated from the commercial use of the work.*/ commercialRevCeiling: bigint; @@ -112,11 +113,12 @@ export type MintLicenseTokensRequest = { maxRevenueShare: number | string; amount?: number | string | bigint; receiver?: Address; - txOptions?: TxOptions; -}; +} & WithTxOptions & + WithWipOptions; export type MintLicenseTokensResponse = { licenseTokenIds?: bigint[]; + receipt?: TransactionReceipt; txHash?: string; encodedTxData?: EncodedTxData; }; diff --git a/packages/core-sdk/src/types/resources/royalty.ts b/packages/core-sdk/src/types/resources/royalty.ts index aaebebf3..4be34337 100644 --- a/packages/core-sdk/src/types/resources/royalty.ts +++ b/packages/core-sdk/src/types/resources/royalty.ts @@ -1,7 +1,9 @@ -import { Address } from "viem"; +import { Address, Hash, TransactionReceipt } from "viem"; -import { TxOptions } from "../options"; -import { EncodedTxData } from "../../abi/generated"; +import { TxOptions, WithTxOptions } from "../options"; +import { EncodedTxData, IpAccountImplClient } from "../../abi/generated"; +import { WithWipOptions } from "../utils/wip"; +import { TokenAmountInput } from "../common"; export type RoyaltyPolicyApiResponse = { data: RoyaltyPolicy; @@ -42,12 +44,13 @@ export type PayRoyaltyOnBehalfRequest = { receiverIpId: Address; payerIpId: Address; token: Address; - amount: string | number | bigint; - txOptions?: TxOptions; -}; + amount: TokenAmountInput; +} & WithTxOptions & + WithWipOptions; export type PayRoyaltyOnBehalfResponse = { txHash?: string; + receipt?: TransactionReceipt; encodedTxData?: EncodedTxData; }; @@ -131,3 +134,68 @@ export type SnapshotAndClaimBySnapshotBatchResponse = { 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 = { + /** The address of the ancestor IP from which the revenue is being claimed. */ + ancestorIpId: Address; + /** + * The address of the claimer of the currency (revenue) tokens. + * + * This is normally the ipId of the ancestor IP if the IP has all royalty tokens. + * Otherwise, this would be the address that is holding the ancestor IP royalty tokens. + */ + claimer: Address; + /** The addresses of the child IPs from which royalties are derived. */ + childIpIds: Address[]; + /** + * The addresses of the royalty policies, where + * royaltyPolicies[i] governs the royalty flow for childIpIds[i]. + */ + royaltyPolicies: Address[]; + /** The addresses of the currency tokens in which royalties will be claimed */ + currencyTokens: Address[]; + + claimOptions?: { + /** + * When enabled, all claimed tokens on the claimer are transferred to the + * wallet address if the wallet owns the IP. If the wallet is the claimer + * or if the claimer is not an IP owned by the wallet, then the tokens + * will not be transferred. + * Set to false to disable auto transferring claimed tokens from the claimer. + * + * @default true + */ + autoTransferAllClaimedTokensFromIp?: boolean; + + /** + * By default all claimed WIP tokens are converted back to IP after + * they are transferred. + * Set this to false to disable this behavior. + * + * @default false + */ + autoUnwrapIpTokens?: boolean; + }; +}; + +export type ClaimedToken = { + token: Address; + amount: bigint; +}; + +export type ClaimAllRevenueResponse = { + txHashes: Hash[]; + receipt?: TransactionReceipt; + claimedTokens?: ClaimedToken[]; +}; + +export type TransferClaimedTokensFromIpToWalletParams = { + ipAccount: IpAccountImplClient; + skipUnwrapIp: boolean; + claimedTokens: ClaimedToken[]; +}; diff --git a/packages/core-sdk/src/types/resources/wip.ts b/packages/core-sdk/src/types/resources/wip.ts new file mode 100644 index 00000000..21d6433f --- /dev/null +++ b/packages/core-sdk/src/types/resources/wip.ts @@ -0,0 +1,19 @@ +import { Address } from "viem"; + +import { TokenAmountInput } from "../common"; +import { WithTxOptions } from "../options"; + +export type ApproveRequest = WithTxOptions & { + /** The address that will use the WIP tokens */ + spender: Address; + /** The amount of WIP tokens to approve. */ + amount: TokenAmountInput; +}; + +export type DepositRequest = WithTxOptions & { + amount: TokenAmountInput; +}; + +export type WithdrawRequest = WithTxOptions & { + amount: TokenAmountInput; +}; diff --git a/packages/core-sdk/src/types/utils/contract.ts b/packages/core-sdk/src/types/utils/contract.ts new file mode 100644 index 00000000..d790552d --- /dev/null +++ b/packages/core-sdk/src/types/utils/contract.ts @@ -0,0 +1,11 @@ +import { PublicClient, SimulateContractParameters } from "viem"; + +import { SimpleWalletClient } from "../../abi/generated"; + +export type SimulateAndWriteContractParams = { + rpcClient: PublicClient; + wallet: SimpleWalletClient; + data: Exclude; + /** @default true */ + waitForTransaction?: boolean; +}; diff --git a/packages/core-sdk/src/types/utils/erc20.ts b/packages/core-sdk/src/types/utils/erc20.ts new file mode 100644 index 00000000..e04040b7 --- /dev/null +++ b/packages/core-sdk/src/types/utils/erc20.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..3cd33262 --- /dev/null +++ b/packages/core-sdk/src/types/utils/txOptions.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..d8b436ee --- /dev/null +++ b/packages/core-sdk/src/types/utils/wip.ts @@ -0,0 +1,102 @@ +import { Address, Hash, PublicClient } from "viem"; + +import { + Multicall3Aggregate3Request, + Multicall3Client, + Erc20TokenClient, + EncodedTxData, + SimpleWalletClient, + PiLicenseTemplateClient, + LicensingModuleClient, +} 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; + }; +}; + +export type Multicall3ValueCall = Multicall3Aggregate3Request["calls"][0] & { value: bigint }; + +export type WipSpender = { + address: Address; + /** + * Amount that the address will spend in WIP. + * If not provided, then unlimited amount is assumed. + */ + amount?: bigint; +}; + +export type WipApprovalCall = { + spenders: WipSpender[]; + client: Erc20TokenClient; + rpcClient: PublicClient; + /** owner is the address calling the approval */ + owner: Address; + /** when true, will return an array of {@link Multicall3ValueCall} */ + useMultiCall: boolean; +}; + +export type ContractCallWithWipFees = WithWipOptions & { + totalFees: bigint; + multicall3Client: Multicall3Client; + wipClient: Erc20TokenClient; + /** all possible spenders of the wip */ + wipSpenders: WipSpender[]; + contractCall: () => Promise; + encodedTxs: EncodedTxData[]; + rpcClient: PublicClient; + wallet: SimpleWalletClient; + sender: Address; + txOptions?: TxOptions; +}; + +export type MulticallWithWrapIp = WithWipOptions & { + calls: Multicall3ValueCall[]; + ipAmountToWrap: bigint; + contractCall: () => Promise; + wipSpenders: WipSpender[]; + multicall3Client: Multicall3Client; + wipClient: Erc20TokenClient; + rpcClient: PublicClient; + wallet: SimpleWalletClient; +}; + +export type CalculateDerivativeMintFeeParams = { + multicall3Client: Multicall3Client; + licenseTemplateClient: PiLicenseTemplateClient; + licensingModuleClient: LicensingModuleClient; + parentIpId: Address; + licenseTermsId: bigint; + receiver: Address; + amount: bigint; +}; diff --git a/packages/core-sdk/src/utils/chain.ts b/packages/core-sdk/src/utils/chain.ts index 63ff55db..3ec1bab3 100644 --- a/packages/core-sdk/src/utils/chain.ts +++ b/packages/core-sdk/src/utils/chain.ts @@ -1,18 +1,18 @@ import { defineChain } from "viem/utils"; -export const homer = defineChain({ +export const aeneid = defineChain({ id: 13_15, - name: "homer", + name: "aeneid", nativeCurrency: { name: "IP", symbol: "IP", decimals: 18 }, rpcUrls: { default: { - http: ["https://devnet.storyrpc.io/"], + http: ["https://aeneid.storyrpc.io/"], }, }, blockExplorers: { default: { name: "Explorer", - url: "https://devnet.storyscan.xyz/", + url: "https://aeneid.storyscan.xyz/", }, }, contracts: { diff --git a/packages/core-sdk/src/utils/contract.ts b/packages/core-sdk/src/utils/contract.ts new file mode 100644 index 00000000..43e0a71f --- /dev/null +++ b/packages/core-sdk/src/utils/contract.ts @@ -0,0 +1,21 @@ +import { WriteContractParameters } from "viem"; + +import { SimulateAndWriteContractParams } from "../types/utils/contract"; + +export async function simulateAndWriteContract({ + rpcClient, + wallet, + waitForTransaction, + data, +}: SimulateAndWriteContractParams) { + const { request } = await rpcClient.simulateContract({ + ...data, + account: wallet.account, + }); + const txHash = await wallet.writeContract(request as WriteContractParameters); + if (waitForTransaction !== false) { + const receipt = await rpcClient.waitForTransactionReceipt({ hash: txHash }); + return { txHash, receipt }; + } + return { txHash }; +} diff --git a/packages/core-sdk/src/utils/erc20.ts b/packages/core-sdk/src/utils/erc20.ts new file mode 100644 index 00000000..7883df18 --- /dev/null +++ b/packages/core-sdk/src/utils/erc20.ts @@ -0,0 +1,16 @@ +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/errors.ts b/packages/core-sdk/src/utils/errors.ts index df23d5cc..cfed82dc 100644 --- a/packages/core-sdk/src/utils/errors.ts +++ b/packages/core-sdk/src/utils/errors.ts @@ -1,6 +1,8 @@ export function handleError(error: unknown, msg: string): never { if (error instanceof Error) { - throw new Error(`${msg}: ${error.message}`); + const newError = new Error(`${msg}: ${error.message}`); + newError.stack = error.stack; + throw newError; } throw new Error(`${msg}: Unknown error type`); } diff --git a/packages/core-sdk/src/utils/sign.ts b/packages/core-sdk/src/utils/sign.ts index 3eb791dc..f2a54121 100644 --- a/packages/core-sdk/src/utils/sign.ts +++ b/packages/core-sdk/src/utils/sign.ts @@ -35,7 +35,9 @@ export const getPermissionSignature = async ( const isBatchPermissionFunction = permissions.length >= 2; const data = encodeFunctionData({ abi: accessControllerAbi, - functionName: isBatchPermissionFunction ? "setBatchPermissions" : "setPermission", + functionName: isBatchPermissionFunction + ? "setBatchTransientPermissions" + : "setTransientPermission", args: isBatchPermissionFunction ? [ permissions.map((item, index) => ({ diff --git a/packages/core-sdk/src/utils/txOptions.ts b/packages/core-sdk/src/utils/txOptions.ts new file mode 100644 index 00000000..7728cdf5 --- /dev/null +++ b/packages/core-sdk/src/utils/txOptions.ts @@ -0,0 +1,16 @@ +import { HandleTxOptionsParams, HandleTxOptionsResponse } from "../types/utils/txOptions"; + +export async function handleTxOptions({ + txOptions, + rpcClient, + txHash, +}: HandleTxOptionsParams): Promise { + if (!txOptions || !txOptions.waitForTransaction) { + return { txHash }; + } + const receipt = await rpcClient.waitForTransactionReceipt({ + ...txOptions, + hash: txHash, + }); + return { txHash, receipt }; +} diff --git a/packages/core-sdk/src/utils/utils.ts b/packages/core-sdk/src/utils/utils.ts index c1815046..28909a75 100644 --- a/packages/core-sdk/src/utils/utils.ts +++ b/packages/core-sdk/src/utils/utils.ts @@ -10,10 +10,11 @@ import { isAddress, checksumAddress, Address, + formatEther, } from "viem"; import { SupportedChainIds } from "../types/config"; -import { homer } from "./chain"; +import { aeneid } from "./chain"; export async function waitTxAndFilterLog< const TAbi extends Abi | readonly unknown[], @@ -80,18 +81,30 @@ export async function waitTx( export function chainStringToViemChain(chainId: SupportedChainIds): Chain { switch (chainId.toString()) { case "1315": - case "homer": - return homer; + case "aeneid": + return aeneid; default: throw new Error(`ChainId ${chainId as string} not supported`); } } export const chain: { [key in SupportedChainIds]: "1315" } = { - homer: "1315", + aeneid: "1315", 1315: "1315", }; +export function validateAddress(address: string): Address { + if (!isAddress(address, { strict: false })) { + throw Error(`Invalid address: ${address}`); + } + return address; +} + +export function validateAddresses(addresses: string[]): Address[] { + return addresses.map((address) => validateAddress(address)); +} + +/** @deprecated use {@link validateAddress} */ export const getAddress = (address: string, name: string, chainId?: number): Address => { if (!isAddress(address, { strict: false })) { throw Error( @@ -100,3 +113,7 @@ export const getAddress = (address: string, name: string, chainId?: number): Add } return checksumAddress(address, chainId); }; + +export function getTokenAmountDisplay(amount: bigint, unit = "IP"): string { + return `${formatEther(amount)}${unit}`; +} diff --git a/packages/core-sdk/src/utils/wipFeeUtils.ts b/packages/core-sdk/src/utils/wipFeeUtils.ts new file mode 100644 index 00000000..3231c200 --- /dev/null +++ b/packages/core-sdk/src/utils/wipFeeUtils.ts @@ -0,0 +1,268 @@ +import { maxUint256, zeroAddress } from "viem"; + +import { erc20TokenAbi, multicall3Abi, SpgnftImplReadOnlyClient } from "../abi/generated"; +import { WIP_TOKEN_ADDRESS } from "../constants/common"; +import { getTokenAmountDisplay } from "./utils"; +import { + WipApprovalCall, + Multicall3ValueCall, + CalculateDerivativeMintFeeParams, + MulticallWithWrapIp, + ContractCallWithWipFees, +} from "../types/utils/wip"; +import { simulateAndWriteContract } from "./contract"; +import { handleTxOptions } from "./txOptions"; +import { HandleTxOptionsResponse } from "../types/utils/txOptions"; + +/** + * check the allowance of all spenders and call approval if any spender + * allowance is lower than the amount they are expected to spend. + * Supports using multicall to return all approve calls in a multicall array. + */ +const approvalAllSpenders = async ({ + spenders, + client, + owner, + useMultiCall, + rpcClient, +}: WipApprovalCall) => { + const approvals = await Promise.all( + spenders.map(async (spender) => { + const spenderAmount = spender.amount || maxUint256; + const { result: allowance } = await client.allowance({ + owner: owner, + spender: spender.address, + }); + if (allowance < spenderAmount) { + return { + spender: spender.address, + amount: maxUint256, // approve max amount to avoid approvals in the future + }; + } + return; + }), + ); + if (useMultiCall) { + const allCalls: Multicall3ValueCall[] = []; + approvals.forEach((approval) => { + if (!approval) { + return; + } + const encodedData = client.approveEncode(approval); + allCalls.push({ + target: encodedData.to, + allowFailure: false, + value: 0n, + callData: encodedData.data, + }); + }); + return allCalls; + } + + // make approval calls sequentially + for (const approval of approvals) { + if (!approval) { + continue; + } + const hash = await client.approve(approval); + await rpcClient.waitForTransactionReceipt({ hash }); + } + return []; +}; + +export const calculateLicenseWipMintFee = async (params: CalculateDerivativeMintFeeParams) => { + const fee = await params.licensingModuleClient.predictMintingLicenseFee({ + licensorIpId: params.parentIpId, + licenseTemplate: params.licenseTemplateClient.address, + licenseTermsId: params.licenseTermsId, + amount: params.amount, + receiver: params.receiver, + royaltyContext: zeroAddress, + }); + if (fee.currencyToken !== WIP_TOKEN_ADDRESS) { + return 0n; + } + return fee.tokenAmount; +}; + +export const calculateSPGWipMintFee = async (spgNftClient: SpgnftImplReadOnlyClient) => { + const token = await spgNftClient.mintFeeToken(); + if (token !== WIP_TOKEN_ADDRESS) { + return 0n; + } + return await spgNftClient.mintFee(); +}; + +const multiCallWrapIp = async ({ + ipAmountToWrap, + wipClient, + multicall3Client, + wipSpenders, + calls, + rpcClient, + wallet, + contractCall, + wipOptions, +}: MulticallWithWrapIp) => { + if (ipAmountToWrap === 0n) { + throw new Error("ipAmountToWrap should be greater than 0"); + } + const multiCalls: Multicall3ValueCall[] = []; + + const useMultiCall = wipOptions?.useMulticallWhenPossible !== false; + + if (useMultiCall) { + const deposit = wipClient.depositEncode(); + multiCalls.push({ + target: deposit.to, + allowFailure: false, + value: ipAmountToWrap, + callData: deposit.data, + }); + } else { + // convert IP to WIP directly from the wallet + await simulateAndWriteContract({ + rpcClient, + wallet: wallet, + data: { + abi: erc20TokenAbi, + address: WIP_TOKEN_ADDRESS, + functionName: "deposit", + value: ipAmountToWrap, + }, + waitForTransaction: true, + }); + } + + const autoApprove = wipOptions?.enableAutoApprove !== false; + if (autoApprove) { + const approvalCalls = await approvalAllSpenders({ + spenders: wipSpenders, + client: wipClient, + owner: useMultiCall ? multicall3Client.address : wallet.account!.address, + rpcClient, + useMultiCall, + }); + if (approvalCalls.length > 0 && useMultiCall) { + multiCalls.push(...approvalCalls); + } + } + + multiCalls.push(...calls); + + if (!useMultiCall) { + const txHash = await contractCall(); + return { txHash }; + } + return simulateAndWriteContract({ + rpcClient, + wallet: wallet, + data: { + abi: multicall3Abi, + address: multicall3Client.address, + functionName: "aggregate3Value", + args: [multiCalls], + value: ipAmountToWrap, + }, + // caller should handle waiting for transaction if needed + waitForTransaction: false, + }); +}; + +/** + * Handle contract calls that require WIP fees by automatically wrapping IP to WIP when needed. + * + * @remarks + * This function will automatically handle the following: + * + * If 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`. + */ +export const contractCallWithWipFees = async ({ + totalFees, + wipOptions, + multicall3Client, + rpcClient, + wipClient, + wallet, + wipSpenders, + contractCall, + sender, + txOptions, + encodedTxs, +}: ContractCallWithWipFees): Promise => { + // if no fees, skip all WIP logic + if (totalFees === 0n) { + const txHash = await contractCall(); + return handleTxOptions({ rpcClient, txOptions, txHash }); + } + + 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) { + if (autoApprove) { + await approvalAllSpenders({ + spenders: wipSpenders, + client: wipClient, + owner: sender, // sender owns the wip + 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. + useMultiCall: false, + }); + } + const txHash = await contractCall(); + return handleTxOptions({ rpcClient, txOptions, txHash }); + } + + 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)}`, + ); + } + + // error if there's enough IP to cover fees and we cannot wrap IP to WIP + if (!autoWrapIp) { + throw new Error( + `Wallet does not have enough WIP to pay for fees. Total fees: ${getTokenAmountDisplay( + totalFees, + )}, balance: ${getTokenAmountDisplay(wipBalance, "WIP")}`, + ); + } + + const { txHash } = await multiCallWrapIp({ + ipAmountToWrap: totalFees, + multicall3Client, + wipClient, + wipOptions, + contractCall, + wipSpenders, + rpcClient, + wallet, + calls, + }); + return handleTxOptions({ rpcClient, txOptions, txHash }); +}; diff --git a/packages/core-sdk/test/integration/dispute.test.ts b/packages/core-sdk/test/integration/dispute.test.ts index 7224918e..f3a5f897 100644 --- a/packages/core-sdk/test/integration/dispute.test.ts +++ b/packages/core-sdk/test/integration/dispute.test.ts @@ -1,7 +1,7 @@ import chai from "chai"; import { StoryClient } from "../../src"; import { RaiseDisputeRequest } from "../../src/index"; -import { mockERC721, getStoryClient, getTokenId, homer } from "./utils/util"; +import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; import chaiAsPromised from "chai-as-promised"; import { Address } from "viem"; import { MockERC20 } from "./utils/mockERC20"; @@ -17,8 +17,8 @@ describe("Dispute Functions", () => { before(async () => { clientA = getStoryClient(); clientB = getStoryClient(); - const mockERC20 = new MockERC20(erc20TokenAddress[homer]); - await mockERC20.approve(arbitrationPolicyUmaAddress[homer]); + const mockERC20 = new MockERC20(erc20TokenAddress[aeneid]); + await mockERC20.approve(arbitrationPolicyUmaAddress[aeneid]); const tokenId = await getTokenId(); ipIdB = ( await clientB.ipAsset.register({ @@ -31,7 +31,7 @@ describe("Dispute Functions", () => { ).ipId!; }); - it("should raise a dispute", async () => { + it.skip("should raise a dispute", async () => { const raiseDisputeRequest: RaiseDisputeRequest = { targetIpId: ipIdB, cid: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", @@ -65,7 +65,7 @@ describe("Dispute Functions", () => { it("should throw error when bond exceeds maximum", async () => { const maxBonds = await clientA.dispute.arbitrationPolicyUmaReadOnlyClient.maxBonds({ - token: erc20TokenAddress[homer], + token: erc20TokenAddress[aeneid], }); const raiseDisputeRequest: RaiseDisputeRequest = { @@ -99,7 +99,7 @@ describe("Dispute Functions", () => { ); }); - it("it should not cancel a dispute (yet)", async () => { + it.skip("it should not cancel a dispute (yet)", async () => { // First raise a dispute const raiseResponse = await clientA.dispute.raiseDispute({ targetIpId: ipIdB, diff --git a/packages/core-sdk/test/integration/group.test.ts b/packages/core-sdk/test/integration/group.test.ts index 889072fc..9215a953 100644 --- a/packages/core-sdk/test/integration/group.test.ts +++ b/packages/core-sdk/test/integration/group.test.ts @@ -1,7 +1,7 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { Address, zeroAddress } from "viem"; -import { homer, getStoryClient, mintBySpg } from "./utils/util"; +import { aeneid, getStoryClient, mintBySpg } from "./utils/util"; import { StoryClient } from "../../src"; import { evenSplitGroupPoolAddress, @@ -19,7 +19,7 @@ describe("Group Functions", () => { let groupId: Address; let ipId: Address; let licenseTermsId: bigint; - const groupPoolAddress = evenSplitGroupPoolAddress[homer]; + const groupPoolAddress = evenSplitGroupPoolAddress[aeneid]; // Setup - create necessary contracts and initial IP before(async () => { @@ -47,7 +47,7 @@ describe("Group Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLrpAddress[homer], + royaltyPolicy: royaltyPolicyLrpAddress[aeneid], defaultMintingFee: 0n, expiration: BigInt(1000), commercialUse: true, @@ -61,7 +61,7 @@ describe("Group Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: BigInt(0), - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -86,7 +86,7 @@ describe("Group Functions", () => { await client.license.setLicensingConfig({ ipId, licenseTermsId, - licenseTemplate: piLicenseTemplateAddress[homer], + licenseTemplate: piLicenseTemplateAddress[aeneid], licensingConfig: { isSet: true, mintingFee: 0n, diff --git a/packages/core-sdk/test/integration/ipAccount.test.ts b/packages/core-sdk/test/integration/ipAccount.test.ts index 7cb82bcc..abd0a799 100644 --- a/packages/core-sdk/test/integration/ipAccount.test.ts +++ b/packages/core-sdk/test/integration/ipAccount.test.ts @@ -1,7 +1,7 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { AccessPermission, StoryClient } from "../../src"; -import { mockERC721, getStoryClient, getTokenId, homer } from "./utils/util"; +import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; import { Hex, encodeFunctionData, getAddress, toFunctionSelector } from "viem"; import { accessControllerAbi, @@ -16,8 +16,8 @@ describe("IPAccount Functions", () => { let client: StoryClient; let ipId: Hex; let data: Hex; - const coreMetadataModule = coreMetadataModuleAddress[homer]; - const permissionAddress = accessControllerAddress[homer]; + const coreMetadataModule = coreMetadataModuleAddress[aeneid]; + const permissionAddress = accessControllerAddress[aeneid]; before(async () => { client = getStoryClient(); @@ -32,7 +32,7 @@ describe("IPAccount Functions", () => { ipId = registerResult.ipId!; data = encodeFunctionData({ abi: accessControllerAbi, - functionName: "setPermission", + functionName: "setTransientPermission", args: [ getAddress(ipId), getAddress(process.env.TEST_WALLET_ADDRESS as Hex), diff --git a/packages/core-sdk/test/integration/ipAsset.test.ts b/packages/core-sdk/test/integration/ipAsset.test.ts index 85d8381d..4b0190c4 100644 --- a/packages/core-sdk/test/integration/ipAsset.test.ts +++ b/packages/core-sdk/test/integration/ipAsset.test.ts @@ -1,29 +1,30 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { StoryClient } from "../../src"; -import { Address, Hex, toHex, zeroAddress } from "viem"; +import { Address, Hex, toHex, zeroAddress, zeroHash } from "viem"; import { mockERC721, getStoryClient, getTokenId, mintBySpg, approveForLicenseToken, - homer, + aeneid, } from "./utils/util"; import { MockERC20 } from "./utils/mockERC20"; import { evenSplitGroupPoolAddress, mockErc20Address, - piLicenseTemplateAddress, royaltyPolicyLapAddress, derivativeWorkflowsAddress, royaltyTokenDistributionWorkflowsAddress, } from "../../src/abi/generated"; +import { MAX_ROYALTY_TOKEN, WIP_TOKEN_ADDRESS } from "../../src/constants/common"; chai.use(chaiAsPromised); const expect = chai.expect; -const pool = evenSplitGroupPoolAddress[homer]; +const pool = evenSplitGroupPoolAddress[aeneid]; +const walletAddress = process.env.TEST_WALLET_ADDRESS! as Address; describe("IP Asset Functions", () => { let client: StoryClient; @@ -126,6 +127,7 @@ describe("IP Asset Functions", () => { describe("SPG NFT Operations", () => { let nftContract: Hex; + let nftContractWithMintingFee: Hex; let licenseTermsId: bigint; before(async () => { @@ -137,7 +139,7 @@ describe("IP Asset Functions", () => { isPublicMinting: true, mintOpen: true, contractURI: "test-uri", - mintFeeRecipient: process.env.TEST_WALLET_ADDRESS! as Address, + mintFeeRecipient: walletAddress, txOptions: { waitForTransaction: true }, }); nftContract = txData.spgNftContract!; @@ -150,7 +152,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 0n, expiration: 0n, commercialUse: true, @@ -164,7 +166,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -187,8 +189,8 @@ describe("IP Asset Functions", () => { // Setup ERC20 const mockERC20 = new MockERC20(); - await mockERC20.approve(derivativeWorkflowsAddress[homer]); - await mockERC20.approve(royaltyTokenDistributionWorkflowsAddress[homer]); + await mockERC20.approve(derivativeWorkflowsAddress[aeneid]); + await mockERC20.approve(royaltyTokenDistributionWorkflowsAddress[aeneid]); await mockERC20.mint(); }); @@ -250,7 +252,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -267,7 +269,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 10000n, expiration: 1000n, commercialUse: true, @@ -281,7 +283,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -317,7 +319,7 @@ describe("IP Asset Functions", () => { txOptions: { waitForTransaction: true }, }); expect(result.txHash).to.be.a("string").and.not.empty; - expect(result.childIpId).to.be.a("string").and.not.empty; + expect(result.ipId).to.be.a("string").and.not.empty; expect(result.tokenId).to.be.a("bigint"); }); @@ -357,7 +359,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -389,7 +391,7 @@ describe("IP Asset Functions", () => { }); await approveForLicenseToken( - derivativeWorkflowsAddress[homer], + derivativeWorkflowsAddress[aeneid], mintLicenseTokensResult.licenseTokenIds![0], ); @@ -422,7 +424,7 @@ describe("IP Asset Functions", () => { }); await approveForLicenseToken( - derivativeWorkflowsAddress[homer], + derivativeWorkflowsAddress[aeneid], mintLicenseTokensResult.licenseTokenIds![0], ); @@ -454,7 +456,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 0n, expiration: 1000n, commercialUse: true, @@ -468,7 +470,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -543,7 +545,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 10000n, expiration: 1000n, commercialUse: true, @@ -557,7 +559,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "test case", }, licensingConfig: { @@ -586,6 +588,260 @@ describe("IP Asset Functions", () => { expect(result.licenseTermsIds).to.be.an("array").and.not.empty; expect(result.tokenId).to.be.a("bigint"); }); + + describe("SPG With Minting Fees", () => { + let parentIpId: Address; + let licenseTermsId: bigint; + let nftContractWithMintingFee: Hex; + + before(async () => { + // ensure we start with no wip since we will be wrapping them + const wipBalance = await client.wipClient.balanceOf(walletAddress); + if (wipBalance > 0n) { + await client.wipClient.withdraw({ + amount: wipBalance, + txOptions: { waitForTransaction: true }, + }); + } + + // create a nft collection that requires minting fee + const rsp = await client.nftClient.createNFTCollection({ + name: "Premium Collection", + symbol: "PC", + isPublicMinting: true, + mintOpen: true, + mintFeeRecipient: walletAddress, + contractURI: "test-uri", + mintFee: 100n, + mintFeeToken: WIP_TOKEN_ADDRESS, + txOptions: { waitForTransaction: true }, + }); + nftContractWithMintingFee = rsp.spgNftContract!; + + // create parent ip with minting fee + const result = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ + spgNftContract: nftContractWithMintingFee, + allowDuplicates: true, + licenseTermsData: [ + { + terms: { + transferable: true, + royaltyPolicy: royaltyPolicyLapAddress[aeneid], + defaultMintingFee: 150n, + expiration: 0n, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: zeroAddress, + commercializerCheckerData: zeroAddress, + commercialRevShare: 10, + commercialRevCeiling: BigInt(0), + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCeiling: BigInt(0), + currency: WIP_TOKEN_ADDRESS, + uri: "test", + }, + licensingConfig: { + isSet: false, + mintingFee: 150n, + licensingHook: zeroAddress, + hookData: zeroAddress, + commercialRevShare: 0, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: zeroAddress, + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + parentIpId = result.ipId!; + licenseTermsId = result.licenseTermsIds![0]; + }); + + it("should auto wrap ip when mint and register derivative", async () => { + const userBalanceBefore = await client.getWalletBalance(); + const rsp = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: nftContractWithMintingFee, // pay 100 here + derivData: { + parentIpIds: [parentIpId], // pay 150 here + licenseTermsIds: [licenseTermsId], + maxMintingFee: 0, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + ipMetadata: { + ipMetadataURI: "test", + ipMetadataHash: zeroHash, + nftMetadataURI: "test", + nftMetadataHash: zeroHash, + }, + allowDuplicates: true, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string").and.not.empty; + expect(rsp.ipId).to.be.a("string").and.not.empty; + + const userBalanceAfter = await client.getWalletBalance(); + const cost = 150n + 100n; + expect(userBalanceAfter < userBalanceBefore - cost).to.be.true; + + // user should not have any WIP tokens since we swap the exact amount + const wipBalance = await client.ipAsset.wipClient.balanceOf({ + owner: walletAddress, + }); + expect(wipBalance.result).to.be.equal(0n); + }); + + it("should auto wrap ip when mint and register derivative with license tokens", async () => { + const { licenseTokenIds } = await client.license.mintLicenseTokens({ + licenseTermsId: licenseTermsId, + licensorIpId: parentIpId, + maxMintingFee: 0n, + maxRevenueShare: 100, + txOptions: { waitForTransaction: true }, + }); + await approveForLicenseToken(derivativeWorkflowsAddress[aeneid], licenseTokenIds![0]); + expect(licenseTokenIds).to.be.an("array").and.not.empty; + const { txHash, ipId } = + await client.ipAsset.mintAndRegisterIpAndMakeDerivativeWithLicenseTokens({ + spgNftContract: nftContractWithMintingFee, + licenseTokenIds: licenseTokenIds!, + maxRts: MAX_ROYALTY_TOKEN, + allowDuplicates: true, + ipMetadata: { + ipMetadataURI: "test", + ipMetadataHash: zeroHash, + nftMetadataURI: "test", + nftMetadataHash: zeroHash, + }, + txOptions: { waitForTransaction: true }, + }); + expect(txHash).to.be.a("string").and.not.empty; + expect(ipId).to.be.a("string").and.not.empty; + const isRegistered = await client.ipAsset.isRegistered(ipId!); + expect(isRegistered).to.be.true; + }); + + it("should auto wrap ip when registering derivative", async () => { + const tokenId = await getTokenId(); + const balanceBefore = await client.getWalletBalance(); + const rsp = await client.ipAsset.registerDerivativeIp({ + nftContract: mockERC721, + tokenId: tokenId!, + derivData: { + parentIpIds: [parentIpId], + licenseTermsIds: [licenseTermsId], + maxMintingFee: 0, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string").and.not.empty; + expect(rsp.ipId).to.be.a("string").and.not.empty; + const balanceAfter = await client.getWalletBalance(); + expect(balanceAfter < balanceBefore - 150n).to.be.true; + }); + + it("errors if minting fees are required but auto wrap is disabled", async () => { + const rsp = client.ipAsset.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens({ + spgNftContract: nftContractWithMintingFee, + derivData: { + parentIpIds: [parentIpId], + licenseTermsIds: [licenseTermsId], + maxMintingFee: 0, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + allowDuplicates: true, + ipMetadata: { + ipMetadataURI: "test", + ipMetadataHash: zeroHash, + nftMetadataURI: "test", + nftMetadataHash: zeroHash, + }, + royaltyShares: [ + { + recipient: walletAddress, + percentage: 100, + }, + ], + wipOptions: { + enableAutoWrapIp: false, + }, + txOptions: { waitForTransaction: true }, + }); + await expect(rsp).to.be.rejectedWith(/^Wallet does not have enough WIP to pay for fees./); + }); + + it("should spend existing wip when register derivative and distribute loyalty tokens", async () => { + const tokenId = await getTokenId(); + await client.wipClient.deposit({ + amount: 150n, + txOptions: { waitForTransaction: true }, + }); + const rsp = + await client.ipAsset.registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokens({ + nftContract: mockERC721, + tokenId: tokenId!, + derivData: { + parentIpIds: [parentIpId], + licenseTermsIds: [licenseTermsId], + maxMintingFee: 0, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + royaltyShares: [ + { + recipient: walletAddress, + percentage: 100, + }, + ], + txOptions: { waitForTransaction: true }, + }); + expect( + rsp.registerDerivativeIpAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash, + ).to.be.a("string").and.not.empty; + expect(rsp.ipRoyaltyVault).to.be.a("string").and.not.empty; + expect(rsp.distributeRoyaltyTokensTxHash).to.be.a("string").and.not.empty; + expect(rsp.ipId).to.be.a("string").and.not.empty; + const wipAfter = await client.wipClient.balanceOf(walletAddress); + expect(wipAfter).to.be.equal(0n); + }); + + it("should auto wrap ip when mint and register derivative and distribute loyalty tokens", async () => { + const rsp = + await client.ipAsset.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens({ + spgNftContract: nftContractWithMintingFee, + derivData: { + parentIpIds: [parentIpId], + licenseTermsIds: [licenseTermsId], + maxMintingFee: 0, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + allowDuplicates: true, + ipMetadata: { + ipMetadataURI: "test", + ipMetadataHash: zeroHash, + nftMetadataURI: "test", + nftMetadataHash: zeroHash, + }, + royaltyShares: [ + { + recipient: walletAddress, + percentage: 100, + }, + ], + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string").and.not.empty; + expect(rsp.ipId).to.be.a("string").and.not.empty; + }); + }); }); describe("Batch Operations", () => { @@ -663,7 +919,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 8n, expiration: 0n, commercialUse: true, @@ -677,7 +933,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -700,7 +956,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 8n, expiration: 0n, commercialUse: true, @@ -714,7 +970,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -790,24 +1046,25 @@ describe("IP Asset Functions", () => { nftContract: mockERC721, tokenId: tokenId2!, }, - { - nftContract, - tokenId: spgTokenId1!, - ipMetadata: { - ipMetadataURI: "test-uri2", - ipMetadataHash: toHex("test-metadata-hash2", { size: 32 }), - nftMetadataHash: toHex("test-nft-metadata-hash2", { size: 32 }), - }, - }, - { - nftContract, - tokenId: spgTokenId2!, - ipMetadata: { - ipMetadataURI: "test-uri", - ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), - nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), - }, - }, + // todo: need to disable for now, some issues with signature validation when using multicall + // { + // nftContract, + // tokenId: spgTokenId1!, + // ipMetadata: { + // ipMetadataURI: "test-uri2", + // ipMetadataHash: toHex("test-metadata-hash2", { size: 32 }), + // nftMetadataHash: toHex("test-nft-metadata-hash2", { size: 32 }), + // }, + // }, + // { + // nftContract, + // tokenId: spgTokenId2!, + // ipMetadata: { + // ipMetadataURI: "test-uri", + // ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), + // nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), + // }, + // }, ], txOptions: { waitForTransaction: true }, }); @@ -859,7 +1116,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "", }, licensingConfig: { @@ -889,7 +1146,7 @@ describe("IP Asset Functions", () => { { terms: { transferable: true, - royaltyPolicy: royaltyPolicyLapAddress[homer], + royaltyPolicy: royaltyPolicyLapAddress[aeneid], defaultMintingFee: 10000n, expiration: 1000n, commercialUse: true, @@ -903,7 +1160,7 @@ describe("IP Asset Functions", () => { derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], uri: "test case", }, licensingConfig: { diff --git a/packages/core-sdk/test/integration/license.test.ts b/packages/core-sdk/test/integration/license.test.ts index b284e11d..7ba804b6 100644 --- a/packages/core-sdk/test/integration/license.test.ts +++ b/packages/core-sdk/test/integration/license.test.ts @@ -2,15 +2,14 @@ import chai from "chai"; import { StoryClient } from "../../src"; import { Hex, zeroAddress } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { mockERC721, getStoryClient, getTokenId, homer } from "./utils/util"; +import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; import { MockERC20 } from "./utils/mockERC20"; import { licensingModuleAddress, mockErc20Address, piLicenseTemplateAddress, - royaltyPolicyLapAddress, - royaltyPolicyLapConfig, } from "../../src/abi/generated"; +import { WIP_TOKEN_ADDRESS } from "../../src/constants/common"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -25,7 +24,7 @@ describe("License Functions", () => { it("should register license ", async () => { const result = await client.license.registerPILTerms({ defaultMintingFee: 0, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], transferable: false, royaltyPolicy: zeroAddress, commercialUse: false, @@ -58,7 +57,7 @@ describe("License Functions", () => { it("should register license with commercial use", async () => { const result = await client.license.registerCommercialUsePIL({ defaultMintingFee: "1", - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], txOptions: { waitForTransaction: true, }, @@ -70,7 +69,7 @@ describe("License Functions", () => { const result = await client.license.registerCommercialRemixPIL({ defaultMintingFee: "1", commercialRevShare: 100, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], txOptions: { waitForTransaction: true, }, @@ -82,6 +81,7 @@ describe("License Functions", () => { describe("attach License Terms and mint license tokens", async () => { let ipId: Hex; let licenseId: bigint; + let paidLicenseId: bigint; // license with 0.01IP minting fee let tokenId; before(async () => { tokenId = await getTokenId(); @@ -93,17 +93,25 @@ describe("License Functions", () => { }, }); const mockERC20 = new MockERC20(); - await mockERC20.approve(licensingModuleAddress[homer]); + await mockERC20.approve(licensingModuleAddress[aeneid]); ipId = registerResult.ipId!; const registerLicenseResult = await client.license.registerCommercialRemixPIL({ defaultMintingFee: 0, commercialRevShare: 100, - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], txOptions: { waitForTransaction: true, }, }); licenseId = registerLicenseResult.licenseTermsId!; + + const paidLicenseResult = await client.license.registerCommercialRemixPIL({ + defaultMintingFee: 100n, + commercialRevShare: 10, + currency: WIP_TOKEN_ADDRESS, + txOptions: { waitForTransaction: true }, + }); + paidLicenseId = paidLicenseResult.licenseTermsId!; }); it("should attach License Terms", async () => { @@ -117,7 +125,19 @@ describe("License Functions", () => { expect(result.txHash).to.be.a("string").and.not.empty; }); + it("should be able to attach another license terms", async () => { + const result = await client.license.attachLicenseTerms({ + ipId: ipId, + licenseTermsId: paidLicenseId, + txOptions: { + waitForTransaction: true, + }, + }); + expect(result.txHash).to.be.a("string").and.not.empty; + }); + it("should mint license tokens", async () => { + const balanceBefore = await client.getWalletBalance(); const result = await client.license.mintLicenseTokens({ licenseTermsId: licenseId, licensorIpId: ipId, @@ -131,6 +151,20 @@ describe("License Functions", () => { expect(result.licenseTokenIds).to.be.a("array").and.not.empty; }); + it("should mint license tokens with fee and pay with IP", async () => { + const balanceBefore = await client.getWalletBalance(); + const result = await client.license.mintLicenseTokens({ + licenseTermsId: paidLicenseId, + licensorIpId: ipId, + maxMintingFee: 0n, + maxRevenueShare: 50, + txOptions: { waitForTransaction: true }, + }); + expect(result.txHash).to.be.a("string").and.not.empty; + const balanceAfter = await client.getWalletBalance(); + expect(balanceAfter < balanceBefore - 100n).to.be.true; + }); + it("should get license terms", async () => { const result = await client.license.getLicenseTerms(licenseId); expect(result).not.empty; @@ -150,7 +184,7 @@ describe("License Functions", () => { const result = await client.license.setLicensingConfig({ ipId: ipId, licenseTermsId: licenseId, - licenseTemplate: piLicenseTemplateAddress[homer], + licenseTemplate: piLicenseTemplateAddress[aeneid], licensingConfig: { mintingFee: 0, isSet: true, diff --git a/packages/core-sdk/test/integration/permission.test.ts b/packages/core-sdk/test/integration/permission.test.ts index 73bf9abe..88b14660 100644 --- a/packages/core-sdk/test/integration/permission.test.ts +++ b/packages/core-sdk/test/integration/permission.test.ts @@ -1,6 +1,6 @@ import chai from "chai"; import { StoryClient } from "../../src"; -import { mockERC721, getStoryClient, getTokenId, homer } from "./utils/util"; +import { mockERC721, getStoryClient, getTokenId, aeneid } from "./utils/util"; import { Address } from "viem"; import { AccessPermission } from "../../src/types/resources/permission"; import chaiAsPromised from "chai-as-promised"; @@ -12,7 +12,7 @@ const expect = chai.expect; describe("Permission Functions", () => { let client: StoryClient; let ipId: Address; - const coreMetadataModule = coreMetadataModuleAddress[homer]; + const coreMetadataModule = coreMetadataModuleAddress[aeneid]; before(async () => { client = getStoryClient(); diff --git a/packages/core-sdk/test/integration/royalty.test.ts b/packages/core-sdk/test/integration/royalty.test.ts index 5e550d84..3dd8081d 100644 --- a/packages/core-sdk/test/integration/royalty.test.ts +++ b/packages/core-sdk/test/integration/royalty.test.ts @@ -1,10 +1,11 @@ import chai from "chai"; import { StoryClient } from "../../src"; -import { Address, Hex, encodeFunctionData } from "viem"; +import { Address, Hex, encodeFunctionData, zeroAddress } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { mockERC721, getTokenId, getStoryClient, homer } from "./utils/util"; +import { mockERC721, getTokenId, getStoryClient, aeneid } from "./utils/util"; import { MockERC20 } from "./utils/mockERC20"; -import { mockErc20Address } from "../../src/abi/generated"; +import { mockErc20Address, royaltyPolicyLapAddress } from "../../src/abi/generated"; +import { MAX_ROYALTY_TOKEN, WIP_TOKEN_ADDRESS } from "../../src/constants/common"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -34,7 +35,7 @@ describe("Royalty Functions", () => { const getCommercialPolicyId = async (): Promise => { const response = await client.license.registerCommercialRemixPIL({ defaultMintingFee: "100000", - currency: mockErc20Address[homer], + currency: mockErc20Address[aeneid], commercialRevShare: 10, txOptions: { waitForTransaction: true }, }); @@ -107,7 +108,7 @@ describe("Royalty Functions", () => { const response = await client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[homer], + token: mockErc20Address[aeneid], amount: 10 * 10 ** 2, txOptions: { waitForTransaction: true }, }); @@ -115,11 +116,25 @@ describe("Royalty Functions", () => { expect(response.txHash).to.be.a("string").and.not.empty; }); + it("should auto convert IP to WIP when paying WIP on behalf", async () => { + const balanceBefore = await client.getWalletBalance(); + const response = await client.royalty.payRoyaltyOnBehalf({ + receiverIpId: parentIpId, + payerIpId: childIpId, + token: WIP_TOKEN_ADDRESS, + amount: 100n, + txOptions: { waitForTransaction: true }, + }); + expect(response.txHash).to.be.a("string"); + const balanceAfter = await client.getWalletBalance(); + expect(balanceAfter < balanceBefore - 100n).to.be.true; + }); + it("should return encoded transaction data for payRoyaltyOnBehalf", async () => { const response = await client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[homer], + token: mockErc20Address[aeneid], amount: 10 * 10 ** 2, txOptions: { encodedTxDataOnly: true }, }); @@ -134,7 +149,7 @@ describe("Royalty Functions", () => { client.royalty.payRoyaltyOnBehalf({ receiverIpId: unregisteredIpId, payerIpId: childIpId, - token: mockErc20Address[homer], + token: mockErc20Address[aeneid], amount: 10 * 10 ** 2, txOptions: { waitForTransaction: true }, }), @@ -147,7 +162,7 @@ describe("Royalty Functions", () => { const response = await client.royalty.claimableRevenue({ royaltyVaultIpId: parentIpId, claimer: process.env.TEST_WALLET_ADDRESS as Address, - token: mockErc20Address[homer], + token: mockErc20Address[aeneid], }); expect(response).to.be.a("bigint"); @@ -173,7 +188,7 @@ describe("Royalty Functions", () => { client.royalty.payRoyaltyOnBehalf({ receiverIpId: parentIpId, payerIpId: childIpId, - token: mockErc20Address[homer], + token: mockErc20Address[aeneid], amount: -1, txOptions: { waitForTransaction: true }, }), @@ -190,4 +205,125 @@ describe("Royalty Functions", () => { expect(response).to.equal(0n); }); }); + + describe("ClaimAllRevenue With WIP", () => { + let ipA: Address; + let ipB: Address; + let ipC: Address; + let ipD: Address; + let spgNftContract: Address; + let licenseTermsId: bigint; + + before(async () => { + // set up + // minting Fee: 100, 10% LAP rev share, A expect to get 120 WIP + // A -> B -> C -> D + 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 retA = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ + spgNftContract, + allowDuplicates: true, + 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, + }, + }, + ], + txOptions: { waitForTransaction: true }, + }); + ipA = retA.ipId!; + licenseTermsId = retA.licenseTermsIds![0]; + + const retB = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract, + allowDuplicates: true, + derivData: { + parentIpIds: [ipA!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 0n, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + ipB = retB.ipId!; + + const retC = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract, + allowDuplicates: true, + derivData: { + parentIpIds: [ipB!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 0n, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + ipC = retC.ipId!; + + const retD = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ + spgNftContract, + allowDuplicates: true, + derivData: { + parentIpIds: [ipC!], + licenseTermsIds: [licenseTermsId!], + maxMintingFee: 0n, + maxRts: MAX_ROYALTY_TOKEN, + maxRevenueShare: 100, + }, + txOptions: { waitForTransaction: true }, + }); + ipD = retD.ipId!; + }); + + it("should claim all revenue and convert WIP back to IP", async () => { + const ret = await client.royalty.claimAllRevenue({ + ancestorIpId: ipA, + claimer: ipA, + childIpIds: [ipB, ipC], + royaltyPolicies: [royaltyPolicyLapAddress[aeneid], royaltyPolicyLapAddress[aeneid]], + currencyTokens: [WIP_TOKEN_ADDRESS, WIP_TOKEN_ADDRESS], + }); + expect(ret.txHashes).to.be.an("array").and.not.empty; + expect(ret.claimedTokens![0].amount).to.equal(120n); + }); + }); }); diff --git a/packages/core-sdk/test/integration/utils/mockERC20.ts b/packages/core-sdk/test/integration/utils/mockERC20.ts index 059d01d2..a1046992 100644 --- a/packages/core-sdk/test/integration/utils/mockERC20.ts +++ b/packages/core-sdk/test/integration/utils/mockERC20.ts @@ -9,16 +9,16 @@ import { } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { chainStringToViemChain, waitTx } from "../../../src/utils/utils"; -import { RPC, homer } from "./util"; +import { RPC, aeneid } from "./util"; import { mockErc20Address } from "../../../src/abi/generated"; export class MockERC20 { private publicClient: PublicClient; private walletClient: WalletClient; - public address: Address = mockErc20Address[homer]; + public address: Address = mockErc20Address[aeneid]; constructor(address?: Address) { const baseConfig = { - chain: chainStringToViemChain("homer"), + chain: chainStringToViemChain("aeneid"), transport: http(RPC), } as const; this.publicClient = createPublicClient(baseConfig); @@ -26,7 +26,7 @@ export class MockERC20 { ...baseConfig, account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), }); - this.address = address || mockErc20Address[homer]; + this.address = address || mockErc20Address[aeneid]; } public async approve(contract: Address): Promise { diff --git a/packages/core-sdk/test/integration/utils/util.ts b/packages/core-sdk/test/integration/utils/util.ts index 9944d113..257ba6d1 100644 --- a/packages/core-sdk/test/integration/utils/util.ts +++ b/packages/core-sdk/test/integration/utils/util.ts @@ -6,8 +6,9 @@ import { createWalletClient, Hex, Address, - zeroAddress, zeroHash, + TransactionReceipt, + parseEther, } from "viem"; import { StoryClient, StoryConfig } from "../../../src"; import { @@ -15,15 +16,16 @@ import { licenseTokenAddress, spgnftBeaconAddress, } from "../../../src/abi/generated"; -export const RPC = "https://devnet.storyrpc.io"; -export const homer = 1315; +export const RPC = "https://aeneid.storyrpc.io"; +export const aeneid = 1315; export const mockERC721 = "0xa1119092ea911202E0a65B743a13AE28C5CF2f21"; -export const licenseToken = licenseTokenAddress[homer]; -export const spgNftBeacon = spgnftBeaconAddress[homer]; +export const licenseToken = licenseTokenAddress[aeneid]; +export const spgNftBeacon = spgnftBeaconAddress[aeneid]; +export const TEST_WALLET_ADDRESS = process.env.TEST_WALLET_ADDRESS! as Address; const baseConfig = { - chain: chainStringToViemChain("homer"), + chain: chainStringToViemChain("aeneid"), transport: http(RPC), } as const; export const publicClient = createPublicClient(baseConfig); @@ -130,7 +132,7 @@ export const approveForLicenseToken = async (address: Address, tokenId: bigint) }; export const getStoryClient = (): StoryClient => { const config: StoryConfig = { - chainId: "homer", + chainId: "aeneid", transport: http(RPC), account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Address), }; diff --git a/packages/core-sdk/test/integration/wip.test.ts b/packages/core-sdk/test/integration/wip.test.ts new file mode 100644 index 00000000..f4f78cf9 --- /dev/null +++ b/packages/core-sdk/test/integration/wip.test.ts @@ -0,0 +1,53 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { parseEther } from "viem"; +import { StoryClient } from "../../src"; +import { getStoryClient, TEST_WALLET_ADDRESS } from "./utils/util"; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe("WIP Functions", () => { + let client: StoryClient; + + before(async () => { + client = getStoryClient(); + }); + + describe("deposit", () => { + const ipAmtStr = "0.01"; + const ipAmt = parseEther(ipAmtStr); + + it(`should deposit ${ipAmtStr} WIP`, async () => { + const balanceBefore = await client.getWalletBalance(); + const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); + const rsp = await client.wipClient.deposit({ + amount: ipAmt, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + const balanceAfter = await client.getWalletBalance(); + const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); + expect(wipAfter).to.equal(wipBefore + ipAmt); + const gasCost = rsp.receipt!.gasUsed * rsp.receipt!.effectiveGasPrice; + expect(balanceAfter).to.equal(balanceBefore - ipAmt - gasCost); + }); + }); + + describe("withdraw", () => { + it("should withdrawal WIP", async () => { + const balanceBefore = await client.getWalletBalance(); + const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); + const rsp = await client.wipClient.withdraw({ + amount: wipBefore, + txOptions: { waitForTransaction: true }, + }); + expect(rsp.txHash).to.be.a("string"); + const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); + expect(wipAfter).to.equal(0n); + const balanceAfter = await client.getWalletBalance(); + const gasCost = rsp.receipt!.gasUsed * rsp.receipt!.effectiveGasPrice; + expect(balanceAfter).to.equal(balanceBefore + wipBefore - gasCost); + }); + }); +}); diff --git a/packages/core-sdk/test/unit/client.test.ts b/packages/core-sdk/test/unit/client.test.ts index 3f429eed..270f51f3 100644 --- a/packages/core-sdk/test/unit/client.test.ts +++ b/packages/core-sdk/test/unit/client.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { createWalletClient, http, Transport } from "viem"; -import { StoryClient, StoryConfig, homer } from "../../src/index"; +import { StoryClient, StoryConfig, aeneid } from "../../src/index"; const rpc = "http://127.0.0.1:8545"; describe("Test StoryClient", () => { @@ -36,7 +36,7 @@ describe("Test StoryClient", () => { transport: http(rpc), wallet: createWalletClient({ account: privateKeyToAccount(generatePrivateKey()), - chain: homer, + chain: aeneid, transport: http(rpc), }), }); @@ -49,7 +49,7 @@ describe("Test StoryClient", () => { transport: http(rpc), wallet: createWalletClient({ account: privateKeyToAccount(generatePrivateKey()), - chain: homer, + chain: aeneid, transport: http(rpc), }), }); @@ -69,7 +69,7 @@ describe("Test StoryClient", () => { const account = privateKeyToAccount(generatePrivateKey()); const transport = http(rpc); const config: StoryConfig = { - chainId: "homer", + chainId: "aeneid", transport, account, }; diff --git a/packages/core-sdk/test/unit/resources/dispute.test.ts b/packages/core-sdk/test/unit/resources/dispute.test.ts index a7ca7fbc..10f81011 100644 --- a/packages/core-sdk/test/unit/resources/dispute.test.ts +++ b/packages/core-sdk/test/unit/resources/dispute.test.ts @@ -16,7 +16,7 @@ describe("Test DisputeClient", () => { beforeEach(() => { rpcMock = createMock(); walletMock = createMock(); - disputeClient = new DisputeClient(rpcMock, walletMock, "homer"); + disputeClient = new DisputeClient(rpcMock, walletMock, "aeneid"); }); afterEach(() => { diff --git a/packages/core-sdk/test/unit/resources/ipAsset.test.ts b/packages/core-sdk/test/unit/resources/ipAsset.test.ts index 3f3c7e6e..20620a1a 100644 --- a/packages/core-sdk/test/unit/resources/ipAsset.test.ts +++ b/packages/core-sdk/test/unit/resources/ipAsset.test.ts @@ -13,7 +13,6 @@ import { Address, } from "viem"; import chaiAsPromised from "chai-as-promised"; -import { MockERC20 } from "../../integration/utils/mockERC20"; import { LicenseRegistryReadOnlyClient } from "../../../src/abi/generated"; import { MAX_ROYALTY_TOKEN, royaltySharesTotalSupply } from "../../../src/constants/common"; import { LicensingConfig } from "../../../src/types/common"; @@ -22,6 +21,8 @@ const { RoyaltyModuleReadOnlyClient, IpRoyaltyVaultImplReadOnlyClient, IpAccountImplClient, + SpgnftImplReadOnlyClient, + LicensingModuleClient, } = require("../../../src/abi/generated"); const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; chai.use(chaiAsPromised); @@ -73,8 +74,8 @@ describe("Test IpAssetClient", () => { rpcMock = createMock(); walletMock = createMock(); const accountMock = createMock(); - ipAssetClient = new IPAssetClient(rpcMock, walletMock, "1315"); walletMock.account = accountMock; + ipAssetClient = new IPAssetClient(rpcMock, walletMock, "1315"); sinon.stub(LicenseRegistryReadOnlyClient.prototype, "getDefaultLicenseTerms").resolves({ licenseTemplate: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", licenseTermsId: 5n, @@ -106,6 +107,11 @@ describe("Test IpAssetClient", () => { "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"; (ipAssetClient.derivativeWorkflowsClient as any).address = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"; + sinon.stub(SpgnftImplReadOnlyClient.prototype, "mintFeeToken").resolves(zeroAddress); + sinon.stub(LicensingModuleClient.prototype, "predictMintingLicenseFee").resolves({ + currencyToken: zeroAddress, + tokenAmount: 0n, + }); }); afterEach(() => { @@ -259,7 +265,7 @@ describe("Test IpAssetClient", () => { it("should throw account error when register given wallet have no signTypedData ", async () => { const walletMock = createMock(); walletMock.account = createMock(); - ipAssetClient = new IPAssetClient(rpcMock, walletMock, "homer"); + ipAssetClient = new IPAssetClient(rpcMock, walletMock, "aeneid"); (ipAssetClient.registrationWorkflowsClient as any).address = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"; (ipAssetClient.coreMetadataModuleClient as any).address = @@ -1447,7 +1453,7 @@ describe("Test IpAssetClient", () => { }); expect(res.txHash).equal(txHash); - expect(res.childIpId).equal("0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"); + expect(res.ipId).equal("0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"); }); it("should return encoded tx data when call mintAndRegisterIpAndMakeDerivative given correct args and encodedTxDataOnly of true", async () => { @@ -2332,12 +2338,12 @@ describe("Test IpAssetClient", () => { { nftContract: spgNftContract, tokenId: "2", - ipMetadata: { - ipMetadataURI: "", - ipMetadataHash: toHex(0, { size: 32 }), - nftMetadataHash: toHex("nftMetadata", { size: 32 }), - nftMetadataURI: "", - }, + // ipMetadata: { + // ipMetadataURI: "", + // ipMetadataHash: toHex(0, { size: 32 }), + // nftMetadataHash: toHex("nftMetadata", { size: 32 }), + // nftMetadataURI: "", + // }, }, ], txOptions: { @@ -3177,12 +3183,35 @@ describe("Test IpAssetClient", () => { } }); it("should return txHash when mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens given correct args", async () => { + const ipId = "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"; sinon .stub( ipAssetClient.royaltyTokenDistributionWorkflowsClient, "mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens", ) .resolves(txHash); + sinon.stub(ipAssetClient.ipAssetRegistryClient, "parseTxIpRegisteredEvent").returns([ + { + ipId, + chainId: 0n, + tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + tokenId: 0n, + name: "", + uri: "", + registrationDate: 0n, + }, + ]); + sinon + .stub(ipAssetClient.licenseTemplateClient, "getLicenseTermsId") + .resolves({ selectedLicenseTermsId: 5n }); + sinon + .stub(ipAssetClient.royaltyModuleEventClient, "parseTxIpRoyaltyVaultDeployedEvent") + .returns([ + { + ipId, + ipRoyaltyVault: zeroAddress, + }, + ]); const result = await ipAssetClient.mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens({ spgNftContract, @@ -3484,6 +3513,7 @@ describe("Test IpAssetClient", () => { }); expect(result).to.deep.equal({ txHash: txHash, + receipt: {}, ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", tokenId: 0n, }); diff --git a/packages/core-sdk/test/unit/resources/license.test.ts b/packages/core-sdk/test/unit/resources/license.test.ts index 8f5ff972..5fa8f822 100644 --- a/packages/core-sdk/test/unit/resources/license.test.ts +++ b/packages/core-sdk/test/unit/resources/license.test.ts @@ -1,12 +1,12 @@ import chai from "chai"; -import { createMock } from "../testUtils"; +import { createMock, generateRandomAddress, generateRandomHash } from "../testUtils"; 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 { LicenseTerms } from "../../../src/types/resources/license"; -import { MockERC20 } from "../../integration/utils/mockERC20"; +import { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; const { RoyaltyModuleReadOnlyClient } = require("../../../src/abi/generated"); chai.use(chaiAsPromised); @@ -17,6 +17,7 @@ describe("Test LicenseClient", () => { let licenseClient: LicenseClient; let rpcMock: PublicClient; let walletMock: WalletClient; + let predictMintingLicenseFeeStub: sinon.SinonStub; beforeEach(() => { rpcMock = createMock(); @@ -25,6 +26,14 @@ describe("Test LicenseClient", () => { accountMock.address = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; walletMock.account = accountMock; licenseClient = new LicenseClient(rpcMock, walletMock, "1315"); + (licenseClient.licenseTemplateClient as any).address = generateRandomAddress(); + (licenseClient.licensingModuleClient as any).address = generateRandomAddress(); + predictMintingLicenseFeeStub = sinon + .stub(licenseClient.licensingModuleClient, "predictMintingLicenseFee") + .resolves({ + currencyToken: WIP_TOKEN_ADDRESS, + tokenAmount: 0n, + }); }); afterEach(() => { @@ -818,6 +827,82 @@ describe("Test LicenseClient", () => { data: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", }); }); + + 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(() => { + predictMintingLicenseFeeStub.resolves({ + currencyToken: WIP_TOKEN_ADDRESS, + tokenAmount: 100n, + }); + approveStub = sinon.stub(licenseClient.wipClient, "approve").resolves(txHash); + sinon.stub(licenseClient.wipClient, "allowance").resolves({ + result: 50n, + }); + wipBalanceOfStub = sinon.stub(licenseClient.wipClient, "balanceOf").resolves({ + result: 0n, + }); + balanceStub = sinon.stub().resolves(200n); + rpcMock.getBalance = balanceStub; + simulateContractStub = sinon.stub().resolves(generateRandomHash()); + rpcMock.simulateContract = simulateContractStub; + walletMock.writeContract = sinon.stub().resolves(generateRandomHash()); + mintLicenseTokensStub = sinon + .stub(licenseClient.licensingModuleClient, "mintLicenseTokens") + .resolves(txHash); + sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); + sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); + sinon + .stub(licenseClient.licenseRegistryReadOnlyClient, "hasIpAttachedLicenseTerms") + .resolves(true); + }); + + it("should auto convert IP to WIP", async () => { + const result = await licenseClient.mintLicenseTokens({ + licensorIpId: zeroAddress, + licenseTermsId: "1", + maxMintingFee: 1, + maxRevenueShare: 1, + txOptions: { waitForTransaction: false }, + wipOptions: { useMulticallWhenPossible: false }, + }); + 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, + ); + expect(simulateContractStub.callCount).to.equal(1); + expect(simulateContractStub.firstCall.args[0].functionName).to.equal("deposit"); + }); + + it("should support multicall when converting IP to WIP", async () => { + const mockLicenseTokenIds = [{ startLicenseTokenId: 1n }]; + sinon + .stub(licenseClient.licensingModuleClient, "parseTxLicenseTokensMintedEvent") + .returns(mockLicenseTokenIds as any); + const { txHash, receipt, licenseTokenIds } = await licenseClient.mintLicenseTokens({ + licensorIpId: zeroAddress, + licenseTermsId: "1", + maxMintingFee: 1, + maxRevenueShare: 1, + txOptions: { waitForTransaction: true }, + }); + expect(licenseTokenIds![0]).to.equal(mockLicenseTokenIds[0].startLicenseTokenId); + expect(txHash).not.to.be.undefined; + expect(receipt).not.to.be.undefined; + expect(mintLicenseTokensStub.notCalled).to.be.true; + expect(simulateContractStub.calledOnce).to.be.true; + const calls = simulateContractStub.firstCall.args[0].args[0]; + expect(calls.length).to.equal(3); + }); + }); }); describe("Test licenseClient.getLicenseTerms", async () => { @@ -902,7 +987,7 @@ describe("Test LicenseClient", () => { it("should return currency token and token amount when call predictMintingLicenseFee given licenseTemplate and receiver", async () => { sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); - sinon.stub(licenseClient.licensingModuleClient, "predictMintingLicenseFee").resolves({ + predictMintingLicenseFeeStub.resolves({ currencyToken: zeroAddress, tokenAmount: 1n, }); @@ -922,7 +1007,7 @@ describe("Test LicenseClient", () => { it("should return currency token and token amount when call predictMintingLicenseFee given correct args ", async () => { sinon.stub(licenseClient.ipAssetRegistryClient, "isRegistered").resolves(true); sinon.stub(licenseClient.piLicenseTemplateReadOnlyClient, "exists").resolves(true); - sinon.stub(licenseClient.licensingModuleClient, "predictMintingLicenseFee").resolves({ + predictMintingLicenseFeeStub.resolves({ currencyToken: zeroAddress, tokenAmount: 1n, }); diff --git a/packages/core-sdk/test/unit/resources/permission.test.ts b/packages/core-sdk/test/unit/resources/permission.test.ts index 37a63da8..ffd9f8b2 100644 --- a/packages/core-sdk/test/unit/resources/permission.test.ts +++ b/packages/core-sdk/test/unit/resources/permission.test.ts @@ -20,7 +20,7 @@ describe("Test Permission", () => { walletMock.signTypedData = sinon .stub() .resolves("0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"); - permissionClient = new PermissionClient(rpcMock, walletMock, "homer"); + permissionClient = new PermissionClient(rpcMock, walletMock, "aeneid"); sinon .stub(IpAccountImplClient.prototype, "state") .resolves({ result: "0x2e778894d11b5308e4153f094e190496c1e0609652c19f8b87e5176484b9a5e" }); diff --git a/packages/core-sdk/test/unit/resources/royalty.test.ts b/packages/core-sdk/test/unit/resources/royalty.test.ts index 3d4bb7bb..3f65b8c0 100644 --- a/packages/core-sdk/test/unit/resources/royalty.test.ts +++ b/packages/core-sdk/test/unit/resources/royalty.test.ts @@ -4,6 +4,7 @@ import * as sinon from "sinon"; import { PublicClient, WalletClient, Account } from "viem"; import chaiAsPromised from "chai-as-promised"; import { RoyaltyClient } from "../../../src/resources/royalty"; +import { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; const { IpRoyaltyVaultImplReadOnlyClient } = require("../../../src/abi/generated"); chai.use(chaiAsPromised); const expect = chai.expect; @@ -82,6 +83,27 @@ describe("Test RoyaltyClient", () => { 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; + walletMock.writeContract = sinon.stub().resolves(txHash); + const result = await royaltyClient.payRoyaltyOnBehalf({ + receiverIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + payerIpId: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", + token: WIP_TOKEN_ADDRESS, + amount: 100n, + txOptions: { waitForTransaction: true }, + }); + expect(result.txHash).to.be.a("string").and.not.empty; + expect(simulateContractStub.calledOnce).to.be.true; + const calls = simulateContractStub.firstCall.args[0].args[0]; + 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); diff --git a/packages/core-sdk/test/unit/testUtils.ts b/packages/core-sdk/test/unit/testUtils.ts index 1159d084..58c8a976 100644 --- a/packages/core-sdk/test/unit/testUtils.ts +++ b/packages/core-sdk/test/unit/testUtils.ts @@ -1,4 +1,7 @@ +import { randomBytes } from "crypto"; import sinon from "sinon"; +import { Address, Hex, keccak256, toBytes } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; export function createMock(obj = {}): T { const mockObj: any = obj; @@ -8,3 +11,14 @@ export function createMock(obj = {}): T { mockObj.getBlock = sinon.stub().resolves({ timestamp: 1629820800n }); return mockObj; } + +export function generateRandomHash(): Hex { + return keccak256(randomBytes(32)); +} + +export function generateRandomAddress(): Address { + const privateKey = generatePrivateKey(); + const account = privateKeyToAccount(privateKey); + const address = account.address; + return address; +} diff --git a/packages/core-sdk/test/unit/utils/sign.test.ts b/packages/core-sdk/test/unit/utils/sign.test.ts index 88369d81..9165f584 100644 --- a/packages/core-sdk/test/unit/utils/sign.test.ts +++ b/packages/core-sdk/test/unit/utils/sign.test.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { getDeadline, getPermissionSignature } from "../../../src/utils/sign"; import { Hex, WalletClient, createWalletClient, http, zeroAddress } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { homer } from "../../integration/utils/util"; +import { aeneid } from "../../integration/utils/util"; import { chainStringToViemChain } from "../../../src/utils/utils"; describe("Sign", () => { @@ -15,7 +15,7 @@ describe("Sign", () => { deadline: 1000n, permissions: [{ ipId: zeroAddress, signer: zeroAddress, to: zeroAddress, permission: 0 }], wallet: {} as WalletClient, - chainId: BigInt(homer), + chainId: BigInt(aeneid), }); } catch (e) { expect((e as Error).message).to.equal( @@ -32,7 +32,7 @@ describe("Sign", () => { deadline: 1000n, permissions: [{ ipId: zeroAddress, signer: zeroAddress, to: zeroAddress, permission: 0 }], wallet: { signTypedData: () => Promise.resolve("") } as unknown as WalletClient, - chainId: BigInt(homer), + chainId: BigInt(aeneid), }); } catch (e) { expect((e as Error).message).to.equal( @@ -43,7 +43,7 @@ describe("Sign", () => { it("should return signature when call getPermissionSignature given account support signTypedData", async () => { const walletClient = createWalletClient({ - chain: chainStringToViemChain("homer"), + chain: chainStringToViemChain("aeneid"), transport: http(), account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), }); @@ -61,7 +61,7 @@ describe("Sign", () => { }, ], wallet: walletClient, - chainId: BigInt(homer), + chainId: BigInt(aeneid), }); expect(result.signature).is.a("string").and.not.empty; expect(result.nonce).is.a("string").and.not.empty; @@ -69,7 +69,7 @@ describe("Sign", () => { it("should return signature when call getPermissionSignature given account support signTypedData and multiple permissions", async () => { const walletClient = createWalletClient({ - chain: chainStringToViemChain("homer"), + chain: chainStringToViemChain("aeneid"), transport: http(), account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), }); @@ -88,7 +88,7 @@ describe("Sign", () => { }, ], wallet: walletClient, - chainId: BigInt(homer), + chainId: BigInt(aeneid), }); expect(result.signature).is.a("string").and.not.empty; expect(result.nonce).is.a("string").and.not.empty; diff --git a/packages/core-sdk/test/unit/utils/utils.test.ts b/packages/core-sdk/test/unit/utils/utils.test.ts index fef0b992..3f72f5aa 100644 --- a/packages/core-sdk/test/unit/utils/utils.test.ts +++ b/packages/core-sdk/test/unit/utils/utils.test.ts @@ -10,7 +10,7 @@ import { } from "../../../src/utils/utils"; import { createMock } from "../testUtils"; import { licensingModuleAbi } from "../../../src/abi/generated"; -import { homer } from "../../../src"; +import { aeneid } from "../../../src"; describe("Test waitTxAndFilterLog", () => { const txHash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997"; @@ -152,13 +152,13 @@ describe("Test chainStringToViemChain", () => { } }); - it("should return homer testnet if id is 1315", () => { + it("should return aeneid testnet if id is 1315", () => { const chain = chainStringToViemChain("1315"); - expect(chain).to.equal(homer); + expect(chain).to.equal(aeneid); }); - it("should return homer testnet if id is iliad", () => { - const chain = chainStringToViemChain("homer"); - expect(chain).to.equal(homer); + it("should return aeneid testnet if id is iliad", () => { + const chain = chainStringToViemChain("aeneid"); + expect(chain).to.equal(aeneid); }); }); diff --git a/packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts b/packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts new file mode 100644 index 00000000..a0d624e1 --- /dev/null +++ b/packages/core-sdk/test/unit/utils/wipFeeUtils.test.ts @@ -0,0 +1,416 @@ +import chai from "chai"; +import * as sinon from "sinon"; +import chaiAsPromised from "chai-as-promised"; +import { Address, LocalAccount, PublicClient, WalletClient, maxUint256, parseEther } from "viem"; +import { + Multicall3Client, + Erc20TokenClient, + royaltyModuleAddress, + derivativeWorkflowsAddress, +} 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 { WIP_TOKEN_ADDRESS } from "../../../src/constants/common"; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe("WIP Fee Utilities", () => { + let wipClient: Erc20TokenClient; + let rpcMock: PublicClient; + let walletMock: WalletClient; + let multicall3Client: Multicall3Client; + let contractCallMock: sinon.SinonStub; + let rpcWaitForTxMock: sinon.SinonStub; + let walletBalanceMock: sinon.SinonStub; + + beforeEach(() => { + rpcMock = createMock(); + walletBalanceMock = sinon.stub().resolves(0); + rpcMock.getBalance = walletBalanceMock; + walletMock = createMock(); + const accountMock = createMock(); + walletMock.account = accountMock; + walletMock.writeContract = sinon.stub().resolves(generateRandomHash()); + wipClient = createMock(); + multicall3Client = createMock(); + rpcWaitForTxMock = rpcMock.waitForTransactionReceipt as sinon.SinonStub; + }); + + afterEach(() => { + sinon.restore(); + }); + + function getDefaultParams(overrides: Partial): ContractCallWithWipFees { + const hash = generateRandomHash(); + contractCallMock = sinon.stub().resolves(hash); + return { + rpcClient: rpcMock, + wallet: walletMock, + multicall3Client: multicall3Client, + wipClient: wipClient, + totalFees: 0n, + wipSpenders: [], + contractCall: contractCallMock, + encodedTxs: [ + { + to: generateRandomAddress(), + data: "0x", + }, + ], + sender: TEST_WALLET_ADDRESS, + ...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 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 contractCallWithWipFees(params); + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.calledOnce).to.be.true; + expect(txHash).not.to.be.empty; + }); + }); + + describe("Enough WIP", () => { + beforeEach(() => { + wipClient.balanceOf = sinon.stub().resolves({ + result: 200n, + }); + }); + + it("should not call approval if disabled via enableAutoApprove", async () => { + const params = getDefaultParams({ + totalFees: 100n, + wipOptions: { enableAutoApprove: false }, + }); + const { txHash, receipt } = await contractCallWithWipFees(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 enough allowance", async () => { + const params = getDefaultParams({ + totalFees: 100n, + wipSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 50n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 50n, + }, + ], + txOptions: { waitForTransaction: false }, + }); + const allowanceMock = sinon.stub().resolves({ + result: 50n, + }); + wipClient.allowance = allowanceMock; + + const { txHash, receipt } = await contractCallWithWipFees(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, + }), + ).to.be.true; + expect( + allowanceMock.secondCall.calledWith({ + owner: TEST_WALLET_ADDRESS, + spender: params.wipSpenders[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", async () => { + const params = getDefaultParams({ + totalFees: 100n, + wipSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 10n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 90n, + }, + ], + txOptions: { waitForTransaction: true }, + }); + const allowanceMock = sinon.stub().resolves({ + result: 15n, + }); + wipClient.allowance = allowanceMock; + const { txHash, receipt } = await contractCallWithWipFees(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(rpcWaitForTxMock.callCount).to.equal(2); // 1 approval + 1 contract call + expect(txHash).not.to.be.empty; + }); + }); + + describe("Enough IP, not enough WIP", () => { + let simulateContractMock: sinon.SinonStub; + let params: ContractCallWithWipFees; + + beforeEach(() => { + wipClient.balanceOf = sinon.stub().resolves({ + result: 1n, + }); + walletBalanceMock.resolves(1_000); + simulateContractMock = sinon.stub().resolves({ request: {} }); + rpcMock.simulateContract = simulateContractMock; + rpcMock; + params = getDefaultParams({ + totalFees: 100n, + wipSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 20n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 80n, + }, + ], + }); + const allowanceMock = sinon.stub().resolves({ + result: 50n, + }); + wipClient.allowance = allowanceMock; + }); + + it("should error if enableAutoWrapIp is false", async () => { + await expect( + contractCallWithWipFees({ + ...params, + 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({ + ...params, + wipOptions: { useMulticallWhenPossible: false }, + }); + expect(receipt).to.be.undefined; + expect(simulateContractMock.calledOnce).to.be.true; + expect(simulateContractMock.firstCall.args[0]).to.include({ + functionName: "deposit", + value: 100n, + address: WIP_TOKEN_ADDRESS, + }); + expect(approveMock.calledOnce).to.be.true; + expect( + approveMock.firstCall.calledWith({ + spender: derivativeWorkflowsAddress[aeneid], + amount: maxUint256, + }), + ); + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.callCount).to.equal(2); // 1 deposit + 1 approval, no contract call + expect(txHash).not.to.be.empty; + }); + + it("should support wait for tx", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + wipOptions: { useMulticallWhenPossible: false }, + txOptions: { waitForTransaction: true }, + }); + expect(receipt).not.to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(approveMock.calledOnce).to.be.true; + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.callCount).to.equal(3); + }); + + it("should not call approval if enableAutoApprove is false", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + wipOptions: { enableAutoApprove: false, useMulticallWhenPossible: false }, + }); + expect(receipt).to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(approveMock.notCalled).to.be.true; + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.callCount).to.equal(1); + }); + + it("should not call approval if spender has enough allowance", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + wipSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 20n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 10n, + }, + ], + wipOptions: { useMulticallWhenPossible: false }, + }); + expect(receipt).to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(approveMock.notCalled).to.be.true; + expect(contractCallMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.callCount).to.equal(1); + }); + }); + + describe("multicall", () => { + let depositEncodeMock: sinon.SinonStub; + let approveEncodeMock: sinon.SinonStub; + + beforeEach(() => { + depositEncodeMock = sinon.stub().returns({ + to: generateRandomAddress(), + data: "", + }); + wipClient.depositEncode = depositEncodeMock; + approveEncodeMock = sinon.stub().returns({ + to: generateRandomAddress(), + data: "", + }); + wipClient.approveEncode = approveEncodeMock; + }); + + it("should deposit, approve, and call contract in one multicall", async () => { + const { txHash, receipt } = await contractCallWithWipFees(params); + expect(receipt).to.be.undefined; + expect(txHash).not.to.be.empty; + expect(depositEncodeMock.calledOnce).to.be.true; + expect(approveEncodeMock.calledOnce).to.be.true; + expect(simulateContractMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + expect(simulateContractMock.firstCall.args[0]).to.include({ + functionName: "aggregate3Value", + value: 100n, + }); + const calls = simulateContractMock.firstCall.args[0].args[0]; + expect(calls).to.have.length(3); // 1 deposit, 1 approve, 1 call + expect(calls.map((c: { target: Address }) => c.target)).to.deep.eq([ + depositEncodeMock.returnValues[0].to, + approveEncodeMock.returnValues[0].to, + params.encodedTxs[0].to, + ]); + }); + + it("should support wait for tx", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + txOptions: { waitForTransaction: true }, + }); + expect(receipt).not.to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(rpcWaitForTxMock.calledOnce).to.be.true; + const calls = simulateContractMock.firstCall.args[0].args[0]; + expect(calls).to.have.length(3); + }); + + it("should not include approvals if enableAutoApprove is false", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + wipOptions: { enableAutoApprove: false }, + }); + expect(receipt).to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(approveEncodeMock.notCalled).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + const calls = simulateContractMock.firstCall.args[0].args[0]; + expect(calls).to.have.length(2); // 1 deposit, 1 call, no approves + }); + + it("should only include approves if enough allowances", async () => { + const { txHash, receipt } = await contractCallWithWipFees({ + ...params, + wipSpenders: [ + { + address: royaltyModuleAddress[aeneid], + amount: 20n, + }, + { + address: derivativeWorkflowsAddress[aeneid], + amount: 10n, + }, + ], + }); + expect(receipt).to.be.undefined; + expect(txHash).not.to.be.empty; + expect(simulateContractMock.calledOnce).to.be.true; + expect(approveEncodeMock.notCalled).to.be.true; + expect(rpcWaitForTxMock.notCalled).to.be.true; + const calls = simulateContractMock.firstCall.args[0].args[0]; + expect(calls).to.have.length(2); // 1 deposit, 1 call + }); + }); + }); + + describe("Not enough IP or WIP", () => { + const totalFees = parseEther("1"); + + beforeEach(() => { + wipClient.balanceOf = sinon.stub().resolves({ + result: parseEther("0.1"), + }); + walletBalanceMock.resolves(parseEther("0.1")); + }); + + it("should throw error indicating not enough funds to complete", async () => { + const params = getDefaultParams({ totalFees }); + await expect(contractCallWithWipFees(params)).to.be.rejectedWith( + "Wallet does not have enough IP to wrap to WIP and pay for fees. Total fees: 1IP, balance: 0.1IP", + ); + }); + }); + }); +}); diff --git a/packages/wagmi-generator/wagmi.config.ts b/packages/wagmi-generator/wagmi.config.ts index 8f034890..c4d51628 100644 --- a/packages/wagmi-generator/wagmi.config.ts +++ b/packages/wagmi-generator/wagmi.config.ts @@ -4,7 +4,7 @@ import type { Evaluate } from "@wagmi/cli/src/types"; import type { ContractConfig } from "@wagmi/cli/src/config"; import { resolveProxyContracts } from "./resolveProxyContracts"; import { optimizedBlockExplorer } from "./optimizedBlockExplorer"; -const homerChainId = 1315; +const aeneidChainId = 1315; import "dotenv/config"; export default defineConfig(async () => { @@ -12,169 +12,169 @@ export default defineConfig(async () => { { name: "AccessController", address: { - [homerChainId]: "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", + [aeneidChainId]: "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", }, }, { name: "DisputeModule", address: { - [homerChainId]: "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", + [aeneidChainId]: "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", }, }, { name: "IPAccountImpl", address: { - [homerChainId]: "0x7343646585443F1c3F64E4F08b708788527e1C77", + [aeneidChainId]: "0x7343646585443F1c3F64E4F08b708788527e1C77", }, }, { name: "IPAssetRegistry", address: { - [homerChainId]: "0x77319B4031e6eF1250907aa00018B8B1c67a244b", + [aeneidChainId]: "0x77319B4031e6eF1250907aa00018B8B1c67a244b", }, }, { name: "IpRoyaltyVaultImpl", address: { - [homerChainId]: "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298", + [aeneidChainId]: "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298", }, }, { name: "LicenseRegistry", address: { - [homerChainId]: "0x529a750E02d8E2f15649c13D69a465286a780e24", + [aeneidChainId]: "0x529a750E02d8E2f15649c13D69a465286a780e24", }, }, { name: "LicenseToken", address: { - [homerChainId]: "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", + [aeneidChainId]: "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", }, }, { name: "LicensingModule", address: { - [homerChainId]: "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", + [aeneidChainId]: "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", }, }, { name: "PILicenseTemplate", address: { - [homerChainId]: "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", + [aeneidChainId]: "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", }, }, { name: "ModuleRegistry", address: { - [homerChainId]: "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", + [aeneidChainId]: "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", }, }, { name: "RoyaltyModule", address: { - [homerChainId]: "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", + [aeneidChainId]: "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", }, }, { name: "RoyaltyPolicyLAP", address: { - [homerChainId]: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", + [aeneidChainId]: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", }, }, { name: "ArbitrationPolicyUMA", address: { - [homerChainId]: "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", + [aeneidChainId]: "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", }, }, { name: "RoyaltyPolicyLRP", address: { - [homerChainId]: "0x9156e603C949481883B1d3355c6f1132D191fC41", + [aeneidChainId]: "0x9156e603C949481883B1d3355c6f1132D191fC41", }, }, { name: "SPGNFTBeacon", address: { - [homerChainId]: "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218", + [aeneidChainId]: "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218", }, }, { name: "SPGNFTImpl", address: { - [homerChainId]: "0x6Cfa03Bc64B1a76206d0Ea10baDed31D520449F5", + [aeneidChainId]: "0x6Cfa03Bc64B1a76206d0Ea10baDed31D520449F5", }, }, { name: "CoreMetadataModule", address: { - [homerChainId]: "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", + [aeneidChainId]: "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", }, }, { name: "DerivativeWorkflows", address: { - [homerChainId]: "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", + [aeneidChainId]: "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", }, }, { name: "GroupingWorkflows", address: { - [homerChainId]: "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", + [aeneidChainId]: "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", }, }, { name: "RegistrationWorkflows", address: { - [homerChainId]: "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", + [aeneidChainId]: "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", }, }, { name: "RoyaltyWorkflows", address: { - [homerChainId]: "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", + [aeneidChainId]: "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", }, }, { name: "LicenseAttachmentWorkflows", address: { - [homerChainId]: "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", + [aeneidChainId]: "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", }, }, { name: "RoyaltyTokenDistributionWorkflows", address: { - [homerChainId]: "0xa38f42B8d33809917f23997B8423054aAB97322C", + [aeneidChainId]: "0xa38f42B8d33809917f23997B8423054aAB97322C", }, }, { name: "GroupingModule", address: { - [homerChainId]: "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", + [aeneidChainId]: "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", }, }, { name: "EvenSplitGroupPool", address: { - [homerChainId]: "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89", + [aeneidChainId]: "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89", }, }, { name: "MockERC20", address: { - [homerChainId]: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", + [aeneidChainId]: "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", }, }, { - name: "ERC20Token", + name: "WrappedIP", address: { - [homerChainId]: "0x1514000000000000000000000000000000000000", + [aeneidChainId]: "0x1514000000000000000000000000000000000000", }, }, { name: "Multicall3", address: { - [homerChainId]: "0xca11bde05977b3631167028862be2a173976ca11", + [aeneidChainId]: "0xca11bde05977b3631167028862be2a173976ca11", }, }, ]; @@ -183,12 +183,12 @@ export default defineConfig(async () => { contracts: [], plugins: [ optimizedBlockExplorer({ - baseUrl: "https://devnet.storyscan.xyz/api", - name: "homer", + baseUrl: "https://aeneid.storyscan.xyz/api", + name: "aeneid", getAddress: await resolveProxyContracts({ - baseUrl: "https://devnet.storyrpc.io", + baseUrl: "https://aeneid.storyrpc.io", contracts: contracts, - chainId: homerChainId, + chainId: aeneidChainId, }), contracts: contracts, }), @@ -210,7 +210,13 @@ export default defineConfig(async () => { "resolveDispute", "isWhitelistedDisputeTag", ], - IPAccountImpl: ["execute", "executeWithSig", "state", "token"], + IPAccountImpl: [ + "execute", + "executeWithSig", + "state", + "token", + "owner", + ], IPAssetRegistry: [ "IPRegistered", "ipId", @@ -280,12 +286,7 @@ export default defineConfig(async () => { "mintAndRegisterIpAndAttachPILTerms", "multicall", ], - RoyaltyWorkflows: [ - "transferToVaultAndSnapshotAndClaimByTokenBatch", - "transferToVaultAndSnapshotAndClaimBySnapshotBatch", - "snapshotAndClaimByTokenBatch", - "snapshotAndClaimBySnapshotBatch", - ], + RoyaltyWorkflows: ["claimAllRevenue"], Multicall3: ["aggregate3"], RoyaltyTokenDistributionWorkflows: [ "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens",