Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support distributing royalty token on registration #328

Merged
merged 16 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokens…
… method
  • Loading branch information
bonnie57 committed Nov 29, 2024
commit 1f9c04b0ef6b930637e93ff27a92d7e176dc2227
2 changes: 2 additions & 0 deletions packages/core-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type {
RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest,
RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse,
RoyaltyShare,
RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest,
RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse,
} from "./types/resources/ipAsset";

export type {
Expand Down
153 changes: 153 additions & 0 deletions packages/core-sdk/src/resources/ipAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import {
RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse,
BatchMintAndRegisterIpAssetWithPilTermsResult,
IpIdAndTokenId,
RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest,
RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse,
} from "../types/resources/ipAsset";
import {
AccessControllerClient,
Expand Down Expand Up @@ -1549,6 +1551,8 @@ export class IPAssetClient {
* @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s.
* @param request.txOptions [Optional] This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property, without encodedTxData option.
* @returns A Promise that resolves to a transaction hashes, IP ID, License terms ID, and IP royalty vault.
* @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate)
* @emits IpRoyaltyVaultDeployed (ipId, ipRoyaltyVault)
*/
public async registerIPAndAttachLicenseTermsAndDistributeRoyaltyTokens(
request: RegisterIPAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest,
Expand All @@ -1565,6 +1569,10 @@ export class IPAssetClient {
getAddress(request.nftContract, "request.nftContract"),
request.tokenId,
);
const isRegistered = await this.isRegistered(ipIdAddress);
if (isRegistered) {
throw new Error(`The NFT with id ${request.tokenId} is already registered as IP.`);
}
const { signature: sigMetadataSignature, nonce: sigMetadataState } =
await getPermissionSignature({
ipId: ipIdAddress,
Expand Down Expand Up @@ -1644,6 +1652,12 @@ export class IPAssetClient {
totalAmount: totalAmount,
txOptions: request.txOptions,
});
if (request.txOptions?.waitForTransaction) {
await this.rpcClient.waitForTransactionReceipt({
...request.txOptions,
hash: distributeRoyaltyTokensTxHash,
});
}
return {
registerIpAndAttachPilTermsAndDeployRoyaltyVaultTxHash,
distributeRoyaltyTokensTxHash,
Expand All @@ -1658,6 +1672,145 @@ export class IPAssetClient {
);
}
}
/**
* Register the given NFT as a derivative IP and attach license terms and distribute royalty tokens.
* @param request - The request object that contains all data needed to register derivative IP.
* @param request.nftContract The address of the NFT collection.
* @param request.tokenId The ID of the NFT.
* @param request.derivData The derivative data to be used for registerDerivative.
* @param request.derivData.parentIpIds The IDs of the parent IPs to link the registered derivative IP.
* @param request.derivData.licenseTemplate [Optional] The address of the license template to be used for the linking.
* @param request.derivData.licenseTermsIds The IDs of the license terms to be used for the linking.
* @param request.ipMetadata - [Optional] The desired metadata for the newly minted NFT and newly registered IP.
* @param request.ipMetadata.ipMetadataURI [Optional] The URI of the metadata for the IP.
* @param request.ipMetadata.ipMetadataHash [Optional] The hash of the metadata for the IP.
* @param request.ipMetadata.nftMetadataURI [Optional] The URI of the metadata for the NFT.
* @param request.ipMetadata.nftMetadataHash [Optional] The hash of the metadata for the IP NFT.
* @param request.deadline [Optional] The deadline for the signature in seconds, default is 1000s.
* @param {Array} request.royaltyShares Authors of the IP and their shares of the royalty tokens.
* @param request.royaltyShares.author The address of the author.
* @param request.royaltyShares.percentage The percentage of the royalty share, 10 represents 10%.
* @param request.txOptions - [Optional] transaction. This extends `WaitForTransactionReceiptParameters` from the Viem library, excluding the `hash` property.
* @returns A Promise that resolves to a transaction hashes, IP ID and IP royalty vault.
* @emits IPRegistered (ipId, chainId, tokenContract, tokenId, name, uri, registrationDate)
* @emits IpRoyaltyVaultDeployed (ipId, ipRoyaltyVault)
*/
//TODO: ERC20: transfer amount exceeds balance
public async registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokens(
request: RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest,
): Promise<RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse> {
try {
const { royaltyShares, totalAmount } = this.getRoyaltyShares(request.royaltyShares);
const blockTimestamp = (await this.rpcClient.getBlock()).timestamp;
const calculatedDeadline = getDeadline(blockTimestamp, request.deadline);
const ipIdAddress = await this.getIpIdAddress(request.nftContract, request.tokenId);
const isRegistered = await this.isRegistered(ipIdAddress);
if (isRegistered) {
throw new Error(`The NFT with id ${request.tokenId} is already registered as IP.`);
}
const { signature: sigMetadataSignature, nonce: sigMetadataState } =
await getPermissionSignature({
ipId: ipIdAddress,
deadline: calculatedDeadline,
state: toHex(0, { size: 32 }),
wallet: this.wallet as WalletClient,
chainId: chain[this.chainId],
permissions: [
{
ipId: ipIdAddress,
signer: getAddress(
this.royaltyTokenDistributionWorkflowsClient.address,
"royaltyTokenDistributionWorkflowsClient",
),
to: getAddress(this.coreMetadataModuleClient.address, "coreMetadataModuleAddress"),
permission: AccessPermission.ALLOW,
func: "function setAll(address,string,bytes32,bytes32)",
},
],
});
const { signature: sigAttachSignature, nonce: sigAttachState } = await getPermissionSignature(
{
ipId: ipIdAddress,
deadline: calculatedDeadline,
state: sigMetadataState,
wallet: this.wallet as WalletClient,
chainId: chain[this.chainId],
permissions: [
{
ipId: ipIdAddress,
signer: this.royaltyTokenDistributionWorkflowsClient.address,
to: getAddress(this.licensingModuleClient.address, "licensingModuleAddress"),
permission: AccessPermission.ALLOW,
func: "function registerDerivative(address,address[],uint256[],address,bytes)",
},
],
},
);
const txHash =
await this.royaltyTokenDistributionWorkflowsClient.registerIpAndMakeDerivativeAndDeployRoyaltyVault(
{
nftContract: getAddress(request.nftContract, "request.nftContract"),
tokenId: BigInt(request.tokenId),
ipMetadata: {
ipMetadataURI: request.ipMetadata?.ipMetadataURI || "",
ipMetadataHash: request.ipMetadata?.ipMetadataHash || zeroHash,
nftMetadataURI: request.ipMetadata?.nftMetadataURI || "",
nftMetadataHash: request.ipMetadata?.nftMetadataHash || zeroHash,
},
derivData: {
...request.derivData,
licenseTemplate:
request.derivData.licenseTemplate || this.licenseTemplateClient.address,
royaltyContext: zeroAddress,
},
sigMetadata: {
signer: getAddress(this.wallet.account!.address, "wallet.account.address"),
deadline: calculatedDeadline,
signature: sigMetadataSignature,
},
sigRegister: {
signer: this.wallet.account!.address,
deadline: calculatedDeadline,
signature: sigAttachSignature,
},
},
);
const txReceipt = await this.rpcClient.waitForTransactionReceipt({
...request.txOptions,
hash: txHash,
});
const { ipId, tokenId } = this.getIpIdAndTokenIdsFromEvent(txReceipt)[0];
const { ipRoyaltyVault } =
this.royaltyModuleEventClient.parseTxIpRoyaltyVaultDeployedEvent(txReceipt)[0];
const distributeRoyaltyTokensTxHash = await this.distributeRoyaltyTokens({
ipId,
deadline: calculatedDeadline,
state: sigAttachState,
ipRoyaltyVault,
royaltyShares: royaltyShares,
totalAmount: totalAmount,
txOptions: request.txOptions,
});
if (request.txOptions?.waitForTransaction) {
await this.rpcClient.waitForTransactionReceipt({
...request.txOptions,
hash: distributeRoyaltyTokensTxHash,
});
}
return {
registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: txHash,
distributeRoyaltyTokensTxHash,
ipId,
tokenId,
ipRoyaltyVault,
};
} catch (error) {
handleError(
error,
"Failed to register derivative IP and attach license terms and distribute royalty tokens",
);
}
}
private getRoyaltyShares(royaltyShares: RoyaltyShare[]) {
const total = 100000000;
let actualTotal = 0;
Expand Down
27 changes: 27 additions & 0 deletions packages/core-sdk/src/types/resources/ipAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,30 @@ export type RoyaltyShare = {
export type IpIdAndTokenId<T extends string | undefined> = T extends undefined
? { ipId: Address; tokenId: bigint }
: { ipId: Address; tokenId: bigint } & { [T: string]: Address };

export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensRequest = {
nftContract: Address;
tokenId: bigint | string | number;
deadline?: string | number | bigint;
derivData: {
parentIpIds: Address[];
licenseTemplate?: Address;
licenseTermsIds: bigint[];
};
royaltyShares: RoyaltyShare[];
ipMetadata?: {
ipMetadataURI?: string;
ipMetadataHash?: Hex;
nftMetadataURI?: string;
nftMetadataHash?: Hex;
};
txOptions?: Omit<TxOptions, "encodedTxDataOnly">;
};

export type RegisterDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensResponse = {
registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash: Address;
distributeRoyaltyTokensTxHash: Address;
ipId: Address;
tokenId: bigint;
ipRoyaltyVault: Address;
};
52 changes: 39 additions & 13 deletions packages/core-sdk/test/integration/ipAsset.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import chai from "chai";
import { StoryClient, PIL_TYPE } from "../../src";
import { StoryClient } from "../../src";
import { Address, Hex, toHex, zeroAddress } from "viem";
import chaiAsPromised from "chai-as-promised";
import {
Expand Down Expand Up @@ -149,10 +149,10 @@ describe("IP Asset Functions ", () => {
terms: [
{
transferable: true,
royaltyPolicy: zeroAddress,
royaltyPolicy: royaltyPolicyLapAddress[odyssey],
defaultMintingFee: BigInt(1),
expiration: BigInt(0),
commercialUse: false,
commercialUse: true,
commercialAttribution: false,
commercializerChecker: zeroAddress,
commercializerCheckerData: zeroAddress,
Expand All @@ -176,8 +176,9 @@ describe("IP Asset Functions ", () => {
const mockERC20 = new MockERC20();
await mockERC20.approve(derivativeWorkflowsAddress[odyssey]);
await mockERC20.approve(royaltyTokenDistributionWorkflowsAddress[odyssey]);
await mockERC20.mint();
});
it("should not throw error when register a IP Asset given metadata", async () => {
it.skip("should not throw error when register a IP Asset given metadata", async () => {
const tokenId = await mintBySpg(nftContract, "test-metadata");
const response = await client.ipAsset.register({
nftContract,
Expand All @@ -194,7 +195,7 @@ describe("IP Asset Functions ", () => {
});
expect(response.ipId).to.be.a("string").and.not.empty;
});
it("should not throw error when register derivative ip", async () => {
it.skip("should not throw error when register derivative ip", async () => {
const tokenChildId = await mintBySpg(nftContract, "test-metadata");
const result = await client.ipAsset.registerDerivativeIp({
nftContract: nftContract,
Expand All @@ -212,7 +213,7 @@ describe("IP Asset Functions ", () => {
expect(result.ipId).to.be.a("string").and.not.empty;
});

it("should not throw error when register ip and attach pil terms", async () => {
it.skip("should not throw error when register ip and attach pil terms", async () => {
const tokenId = await mintBySpg(nftContract, "test-metadata");
const deadline = 1000n;
const result = await client.ipAsset.registerIpAndAttachPilTerms({
Expand Down Expand Up @@ -268,7 +269,7 @@ describe("IP Asset Functions ", () => {
expect(result.licenseTermsIds).to.be.an("array").and.not.empty;
});

it("should not throw error when mint and register ip and make derivative", async () => {
it.skip("should not throw error when mint and register ip and make derivative", async () => {
const result = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({
spgNftContract: nftContract,
derivData: {
Expand All @@ -284,7 +285,7 @@ describe("IP Asset Functions ", () => {
expect(result.tokenId).to.be.a("bigint");
});

it("should not throw error when mint and register ip", async () => {
it.skip("should not throw error when mint and register ip", async () => {
const result = await client.ipAsset.mintAndRegisterIp({
spgNftContract: nftContract,
ipMetadata: {
Expand All @@ -299,7 +300,7 @@ describe("IP Asset Functions ", () => {
expect(result.txHash).to.be.a("string").and.not.empty;
expect(result.ipId).to.be.a("string").and.not.empty;
});
it("should not throw error when call register pil terms and attach", async () => {
it.skip("should not throw error when call register pil terms and attach", async () => {
const tokenId = await getTokenId();
const ipId = (
await client.ipAsset.register({
Expand Down Expand Up @@ -361,7 +362,7 @@ describe("IP Asset Functions ", () => {
expect(result.licenseTermsIds).to.be.an("array").and.not.empty;
});

it("should not throw error when call mint and register ip and make derivative with license tokens", async () => {
it.skip("should not throw error when call mint and register ip and make derivative with license tokens", async () => {
const mintLicenseTokensResult = await client.license.mintLicenseTokens({
licenseTermsId: noCommercialLicenseTermsId,
licensorIpId: parentIpId,
Expand Down Expand Up @@ -390,7 +391,7 @@ describe("IP Asset Functions ", () => {
expect(result.tokenId).to.be.a("bigint");
});

it("should not throw error when call register ip and make derivative with license tokens", async () => {
it.skip("should not throw error when call register ip and make derivative with license tokens", async () => {
const tokenId = await mintBySpg(nftContract, "test-metadata");
const mintLicenseTokensResult = await client.license.mintLicenseTokens({
licenseTermsId: noCommercialLicenseTermsId,
Expand Down Expand Up @@ -454,7 +455,7 @@ describe("IP Asset Functions ", () => {
royaltyShares: [
{
author: process.env.TEST_WALLET_ADDRESS! as Address,
percentage: 50,
percentage: 1,
},
],
txOptions: {
Expand All @@ -468,9 +469,34 @@ describe("IP Asset Functions ", () => {
expect(result.ipId).to.be.a("string").and.not.empty;
expect(result.licenseTermsId).to.be.a("bigint");
});

it("should not throw error when call register derivative and attach license terms and distribute royalty tokens", async () => {
const tokenId = await getTokenId();
const result =
await client.ipAsset.registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokens({
nftContract: mockERC721,
tokenId: tokenId!,
derivData: {
parentIpIds: [parentIpId!],
licenseTermsIds: [licenseTermsId],
},
royaltyShares: [
{
author: process.env.TEST_WALLET_ADDRESS! as Address,
percentage: 10, //100%
},
],
});
expect(
result.registerDerivativeAndAttachLicenseTermsAndDistributeRoyaltyTokensTxHash,
).to.be.a("string");
expect(result.distributeRoyaltyTokensTxHash).to.be.a("string");
expect(result.ipId).to.be.a("string");
expect(result.tokenId).to.be.a("bigint");
});
});

describe("Multicall", () => {
describe.skip("Multicall", () => {
let nftContract: Hex;
beforeEach(async () => {
const txData = await client.nftClient.createNFTCollection({
Expand Down
Loading