Skip to content
31 changes: 22 additions & 9 deletions packages/core-sdk/src/resources/ipAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,23 +681,36 @@ export class IPAssetClient {
licenseTermsIds: [],
}));

// Due to emit event log by sequence, we need to get license terms id from request.args
// Get license terms id and max license tokens mapping relationship by the sequence of the request.args
for (let j = 0; j < request.args.length; j++) {
const licenseTerms: LicenseTerms[] = [];
const termsIndexes: number[] = [];
const licenseTermsData = request.args[j].licenseTermsData;
const licenseTermsIds = new Array(licenseTermsData.length).fill(null);
for (let i = 0; i < licenseTermsData.length; i++) {
const licenseTerm = PILFlavor.validateLicenseTerms(
licenseTermsData[i].terms,
this.chainId,
);
licenseTerms.push(licenseTerm);
const licenseTermsDataInput = licenseTermsData[i];
if (licenseTermsDataInput.terms) {
const validatedTerms = PILFlavor.validateLicenseTerms(
licenseTermsDataInput.terms,
this.chainId,
);
validatedTerms.commercialRevShare = getRevenueShare(validatedTerms.commercialRevShare);
licenseTerms.push(validatedTerms);
termsIndexes.push(i);
} else if (licenseTermsDataInput.licenseTermsId !== undefined) {
licenseTermsIds[i] = BigInt(licenseTermsDataInput.licenseTermsId);
}
}
const licenseTermsIds = await this.getLicenseTermsId(licenseTerms);
results[j].licenseTermsIds = licenseTermsIds;
const resolvedIds = await this.getLicenseTermsId(licenseTerms);
termsIndexes.forEach((value, index) => {
licenseTermsIds[value] = resolvedIds[index];
});
const filteredLicenseTermsIds = licenseTermsIds.filter((id): id is bigint => id !== null);
results[j].licenseTermsIds = filteredLicenseTermsIds;
const maxLicenseTokensTxHashes = await setMaxLicenseTokens({
maxLicenseTokensData: licenseTermsData,
licensorIpId: results[j].ipId,
licenseTermsIds,
licenseTermsIds: filteredLicenseTermsIds,
totalLicenseTokenLimitHookClient: this.totalLicenseTokenLimitHookClient,
templateAddress: this.licenseTemplateAddress,
});
Expand Down
9 changes: 5 additions & 4 deletions packages/core-sdk/src/types/resources/ipAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ export type RegisterDerivativeRequest = WithErc20AndWipOptions &
childIpId: Address;
};

export type LicenseTermsData = Omit<
LicenseTermsDataInput<LicenseTerms, LicensingConfig>,
"licensingConfig"
> & {
/**
* Processed license terms data with fully resolved license terms.
*/
export type LicenseTermsData = {
terms: LicenseTerms;
licensingConfig: LicensingConfig;
};

Expand Down
51 changes: 47 additions & 4 deletions packages/core-sdk/src/types/resources/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,53 @@ export type RegisterPilTermsAndAttachResponse = {
maxLicenseTokensTxHashes?: Hash[];
};

export type LicenseTermsDataInput<T = LicenseTermsInput, C = LicensingConfigInput> = {
/** Programmable IP License */
terms: T;
licensingConfig?: C;
/**
* The data of the license and its configuration to be attached to the IP.
*
* You must provide either `licenseTermsId` or `terms`:
* - `licenseTermsId`: Use an existing pre-registered license terms
* - `terms`: Register new license terms and attach
*
* If both are provided, `terms` takes priority and new terms will be registered.
*/
export type LicenseTermsDataInput = {
/**
* Full license terms object to register and attach.
* Use this to create and attach new license terms in a single workflow.
*
* For convenient and robust creation of license terms, utilize the {@link PILFlavor} factory helpers.
* These pre-validated, best-practice recipes ensure you generate valid, well-structured license terms
* for the most common commercial remixing, commercial use, non-commercial social remixing, and creative commons attribution scenarios.
*
* Most workflows should leverage these strongly-typed helpers for
* optimal compatibility, validation, and future-proofing of your on-chain ecosystem integrations.
*
* Supported templates include but are not limited to:
* - {@link PILFlavor.commercialRemix} for commercial remixing license terms
* - {@link PILFlavor.commercialUse} for commercial use license terms
* - {@link PILFlavor.nonComSocialRemixing} for non-commercial social remixing license terms
* - {@link PILFlavor.creativeCommonsAttribution} for creative commons attribution(CC-BY) license terms
*
* @example
* ```typescript
* // Example: Creating standardized commercial remix license terms
* const licenseTerms = PILFlavor.commercialRemix({
* commercialRevShare: 50, // 50%
* currency: "0x...",
* royaltyPolicy: "0x...",
* });
* ```
*
* If both `terms` and `licenseTermsId` are specified, `terms` takes precedence and new license terms will be registered on-chain.
*/
terms?: LicenseTermsInput;
/**
* The ID of pre-registered license terms to attach.
* Use this when the license terms already exist on-chain.
*/
licenseTermsId?: LicenseTermsIdInput;
/** The licensing configuration for the license. */
licensingConfig?: LicensingConfigInput;
/**
* The max number of license tokens that can be minted from this license term.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IpAssetRegistryClient,
LicenseRegistryReadOnlyClient,
piLicenseTemplateAddress,
PiLicenseTemplateReadOnlyClient,
RoyaltyModuleReadOnlyClient,
SpgnftImplReadOnlyClient,
totalLicenseTokenLimitHookAddress,
Expand Down Expand Up @@ -50,28 +51,47 @@ export const validateLicenseTermsData = async (
const processedLicenseTermsData: LicenseTermsData[] = [];
const maxLicenseTokens: bigint[] = [];
for (let i = 0; i < licenseTermsData.length; i++) {
const licenseTerm = PILFlavor.validateLicenseTerms(licenseTermsData[i].terms, chainId);
licenseTerm.commercialRevShare = getRevenueShare(licenseTerm.commercialRevShare);
const royaltyModuleReadOnlyClient = new RoyaltyModuleReadOnlyClient(rpcClient);
if (validateAddress(licenseTerm.royaltyPolicy) !== zeroAddress) {
const isWhitelistedArbitrationPolicy =
await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyPolicy({
royaltyPolicy: licenseTerm.royaltyPolicy,
});
if (!isWhitelistedArbitrationPolicy) {
throw new Error(`The royalty policy ${licenseTerm.royaltyPolicy} is not whitelisted.`);
let licenseTerm: LicenseTerms;
const licenseTermsDataInput = licenseTermsData[i];
if (licenseTermsDataInput.terms) {
licenseTerm = PILFlavor.validateLicenseTerms(licenseTermsDataInput.terms, chainId);
licenseTerm.commercialRevShare = getRevenueShare(licenseTerm.commercialRevShare);
const royaltyModuleReadOnlyClient = new RoyaltyModuleReadOnlyClient(rpcClient);
if (validateAddress(licenseTerm.royaltyPolicy) !== zeroAddress) {
const isWhitelistedArbitrationPolicy =
await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyPolicy({
royaltyPolicy: licenseTerm.royaltyPolicy,
});
if (!isWhitelistedArbitrationPolicy) {
throw new Error(`The royalty policy ${licenseTerm.royaltyPolicy} is not whitelisted.`);
}
}
}

if (validateAddress(licenseTerm.currency) !== zeroAddress) {
const isWhitelistedRoyaltyToken = await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken(
{
token: licenseTerm.currency,
},
);
if (!isWhitelistedRoyaltyToken) {
throw new Error(`The currency token ${licenseTerm.currency} is not whitelisted.`);
if (validateAddress(licenseTerm.currency) !== zeroAddress) {
const isWhitelistedRoyaltyToken =
await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken({
token: licenseTerm.currency,
});
if (!isWhitelistedRoyaltyToken) {
throw new Error(`The currency token ${licenseTerm.currency} is not whitelisted.`);
}
}
} else if (licenseTermsDataInput.licenseTermsId !== undefined) {
const piLicenseTemplateReadOnlyClient = new PiLicenseTemplateReadOnlyClient(rpcClient);
const isExist = await piLicenseTemplateReadOnlyClient.exists({
licenseTermsId: BigInt(licenseTermsDataInput.licenseTermsId),
});
if (!isExist) {
throw new Error(
`The license terms id ${licenseTermsDataInput.licenseTermsId} is not exist.`,
);
}
const response = await piLicenseTemplateReadOnlyClient.getLicenseTerms({
selectedLicenseTermsId: BigInt(licenseTermsDataInput.licenseTermsId),
});
licenseTerm = response.terms;
} else {
throw new Error("Either terms or licenseTermsId must be provided.");
}

const licensingConfig = validateLicenseConfig(licenseTermsData[i].licensingConfig);
Expand Down
15 changes: 9 additions & 6 deletions packages/core-sdk/test/integration/ipAsset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4031,7 +4031,7 @@ describe("IP Asset Functions", () => {
licenseTermsData: [
{
terms: PILFlavor.commercialRemix({
defaultMintingFee: 100n,
defaultMintingFee: 10n,
commercialRevShare: 10,
currency: WIP_TOKEN_ADDRESS,
}),
Expand Down Expand Up @@ -4066,6 +4066,10 @@ describe("IP Asset Functions", () => {
commercialRevShare: 10,
currency: WIP_TOKEN_ADDRESS,
}),
licenseTermsId: licenseTermsIdFor10ERC20,
},
{
licenseTermsId: licenseTermsIdFor10ERC20,
},
],
royaltyShares: [
Expand All @@ -4084,11 +4088,7 @@ describe("IP Asset Functions", () => {
nft: { type: "mint", spgNftContract: spgContractWith10ERC20 },
licenseTermsData: [
{
terms: PILFlavor.commercialRemix({
defaultMintingFee: 100n,
commercialRevShare: 10,
currency: erc20Address[aeneid],
}),
licenseTermsId: licenseTermsIdFor10ERC20,
},
],
});
Expand Down Expand Up @@ -4185,6 +4185,9 @@ describe("IP Asset Functions", () => {
currency: WIP_TOKEN_ADDRESS,
}),
},
{
licenseTermsId: licenseTermsIdFor100WIP,
},
],
royaltyShares: [
{
Expand Down
102 changes: 101 additions & 1 deletion packages/core-sdk/test/unit/resources/ipAsset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
LicenseRegistryReadOnlyClient,
LicensingModuleClient,
PiLicenseTemplateClient,
PiLicenseTemplateReadOnlyClient,
RoyaltyModuleEventClient,
RoyaltyModuleReadOnlyClient,
royaltyPolicyLapAddress,
Expand Down Expand Up @@ -2126,7 +2127,6 @@ describe("Test IpAssetClient", () => {
ipMetadataURI: "",
ipMetadataHash: toHex(0, { size: 32 }),
},

licenseTermsData: [
{
terms: licenseTerms,
Expand Down Expand Up @@ -2180,6 +2180,106 @@ describe("Test IpAssetClient", () => {
},
]);
});

it("should return txHash and ipId when batchMintAndRegisterIpAssetWithPilTerms given correct args with license terms id", async () => {
stub(ipAssetClient.licenseAttachmentWorkflowsClient, "multicall").resolves(txHash);
stub(ipAssetClient.ipAssetRegistryClient, "parseTxIpRegisteredEvent").returns([
{
ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
chainId: 0n,
tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
tokenId: 1n,
name: "",
uri: "",
registrationDate: 0n,
},
{
ipId: ipId,
chainId: 0n,
tokenContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
tokenId: 2n,
name: "",
uri: "",
registrationDate: 0n,
},
]);
stub(ipAssetClient.licenseTemplateClient, "getLicenseTermsId")
.onFirstCall()
.resolves({
selectedLicenseTermsId: 5n,
})
.onSecondCall()
.resolves({
selectedLicenseTermsId: 6n,
})
.onThirdCall()
.resolves({
selectedLicenseTermsId: 7n,
});
stub(PiLicenseTemplateReadOnlyClient.prototype, "getLicenseTerms").resolves({
terms: licenseTerms,
});
const result = await ipAssetClient.batchMintAndRegisterIpAssetWithPilTerms({
args: [
{
spgNftContract,
ipMetadata: {
ipMetadataURI: "",
ipMetadataHash: toHex(0, { size: 32 }),
},
licenseTermsData: [
{
terms: licenseTerms,
licensingConfig,
maxLicenseTokens: 100,
},
{
terms: licenseTerms,
licensingConfig,
},
{
licenseTermsId: 6n,
maxLicenseTokens: 100,
},
],
allowDuplicates: false,
},
{
spgNftContract,
licenseTermsData: [
{
licenseTermsId: 7n,
},
{
licenseTermsId: 8n,
maxLicenseTokens: 100,
},
{
terms: PILFlavor.nonCommercialSocialRemixing(),
maxLicenseTokens: 100,
},
],
},
],
});
expect(result.txHash).to.equal(txHash);
expect(result.results).to.deep.equal([
{
ipId: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
licenseTermsIds: [5n, 6n, 6n],
tokenId: 1n,
spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
maxLicenseTokensTxHashes: [txHash, txHash],
},
{
ipId: ipId,
licenseTermsIds: [7n, 8n, 7n],
tokenId: 2n,
spgNftContract: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
maxLicenseTokensTxHashes: [txHash, txHash],
},
]);
});
});

describe("Test ipAssetClient.batchMintAndRegisterIpAndMakeDerivative", () => {
Expand Down
Loading