Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions packages/core-sdk/src/resources/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
RemoveIpsFromGroupRequest,
} from "../types/resources/group";
import { AccessPermission } from "../types/resources/permission";
import { validateCurrencyToken } from "../utils/currencyValidation";
import { handleError } from "../utils/errors";
import { getFunctionSignature } from "../utils/getFunctionSignature";
import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow";
Expand Down Expand Up @@ -430,6 +431,9 @@ export class GroupClient {
if (currencyTokens.some((token) => token === zeroAddress)) {
throw new Error("Currency token cannot be the zero address.");
}
currencyTokens.forEach((token) =>
validateCurrencyToken(validateAddress(token), this.chainId),
);
const collectAndClaimParams = {
groupIpId: validateAddress(groupIpId),
currencyTokens: validateAddresses(currencyTokens),
Expand Down Expand Up @@ -527,6 +531,7 @@ export class GroupClient {
memberIpIds,
}: GetClaimableRewardRequest): Promise<bigint[]> {
try {
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
const claimableReward = await this.groupingModuleClient.getClaimableReward({
groupId: validateAddress(groupIpId),
ipIds: validateAddresses(memberIpIds),
Expand Down Expand Up @@ -576,6 +581,7 @@ export class GroupClient {
txOptions,
}: ClaimRewardRequest): Promise<ClaimRewardResponse> {
try {
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
const claimRewardParam: GroupingModuleClaimRewardRequest = {
groupId: validateAddress(groupIpId),
ipIds: validateAddresses(memberIpIds),
Expand Down Expand Up @@ -608,6 +614,7 @@ export class GroupClient {
txOptions,
}: CollectRoyaltiesRequest): Promise<CollectRoyaltiesResponse> {
try {
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
const collectRoyaltiesParam: GroupingModuleCollectRoyaltiesRequest = {
groupId: validateAddress(groupIpId),
token: validateAddress(currencyToken),
Expand Down
68 changes: 68 additions & 0 deletions packages/core-sdk/src/utils/currencyValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Address, getAddress, zeroAddress } from "viem";

import { erc20Address, wrappedIpAddress } from "../abi/generated";
import { ChainIds, SupportedChainIds } from "../types/config";

const MAINNET_CHAIN_ID = 1514;
const AENEID_CHAIN_ID = 1315;

/** Per-chain whitelist: chainId -> set of allowed currency token addresses (checksummed). */
const CURRENCY_TOKEN_WHITELIST: Record<ChainIds, ReadonlySet<string>> = {
[MAINNET_CHAIN_ID]: new Set([
getAddress(wrappedIpAddress[MAINNET_CHAIN_ID]),
]),
[AENEID_CHAIN_ID]: new Set([
getAddress(wrappedIpAddress[AENEID_CHAIN_ID]),
getAddress(erc20Address[AENEID_CHAIN_ID]),
]),
} as const;

/** Human-readable description and error hint per chain. */
const CHAIN_CURRENCY_HINT: Record<ChainIds, string> = {
[MAINNET_CHAIN_ID]: "On Mainnet, only WIP is allowed as currency token.",
[AENEID_CHAIN_ID]: "On Aeneid Testnet, only WIP or MERC20 is allowed as currency token.",
};

const toChainId = (chainId: SupportedChainIds): ChainIds => {
if (chainId === "mainnet" || chainId === 1514) {
return 1514;
}

if (chainId === "aeneid" || chainId === 1315) {
return 1315;
}

return chainId as ChainIds;
};

/**
* Validates that the currency token is allowed for the given chain.
* - Aeneid Testnet (1315): allows WIP or MERC20
* - Mainnet (1514): allows WIP only
*
* @param token - The currency token address to validate
* @param chainId - The chain ID (1315 or "aeneid" for Aeneid, 1514 or "mainnet" for Mainnet)
* @throws Error if token is zero address or not allowed for the chain
*/
export const validateCurrencyToken = (
token: Address,
chainId: SupportedChainIds | ChainIds,
): void => {
if (token === null || token === undefined || token === zeroAddress) {
throw new Error("Currency token cannot be zero address.");
}

const resolvedChainId = toChainId(chainId as SupportedChainIds);
const allowedTokens = CURRENCY_TOKEN_WHITELIST[resolvedChainId];

if (allowedTokens === null || allowedTokens === undefined) {
throw new Error(`Unsupported chain ID: ${chainId}.`);
}

const normalizedToken = getAddress(token);
if (!allowedTokens.has(normalizedToken)) {
throw new Error(
`${CHAIN_CURRENCY_HINT[resolvedChainId]} The provided token is not allowed.`,
);
}
};
13 changes: 12 additions & 1 deletion packages/core-sdk/src/utils/pilFlavor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { zeroAddress } from "viem";

import { validateCurrencyToken } from "./currencyValidation";
import { PILFlavorError } from "./errors";
import { royaltyPolicyInputToAddress } from "./royalty";
import { SupportedChainIds } from "../types/config";
import { ChainIds, SupportedChainIds } from "../types/config";
import { LicenseTerms, LicenseTermsInput } from "../types/resources/license";
import {
CommercialRemixRequest,
Expand Down Expand Up @@ -204,6 +205,16 @@ export class PILFlavor {
throw new PILFlavorError("Royalty policy requires currency token.");
}

// Validate currency token is allowed for the chain when currency is set (Aeneid: WIP or MERC20; Mainnet: WIP only). Zero address is allowed (e.g. no royalty).
if (
currency !== null &&
currency !== undefined &&
currency !== zeroAddress &&
chainId !== undefined
) {
validateCurrencyToken(currency, chainId as ChainIds);
}

// Validate defaultMintingFee
if (normalized.defaultMintingFee < 0n) {
throw new PILFlavorError("DefaultMintingFee should be greater than or equal to 0.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ValidateDerivativeDataConfig,
} from "../../types/utils/registerHelper";
import { TokenSpender } from "../../types/utils/token";
import { validateCurrencyToken } from "../currencyValidation";
import { PILFlavor } from "../pilFlavor";
import { getRevenueShare } from "../royalty";
import { getDeadline } from "../sign";
Expand Down Expand Up @@ -68,6 +69,7 @@ export const validateLicenseTermsData = async (
}

if (validateAddress(licenseTerm.currency) !== zeroAddress) {
validateCurrencyToken(licenseTerm.currency, chainId);
const isWhitelistedRoyaltyToken =
await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken({
token: licenseTerm.currency,
Expand Down
34 changes: 19 additions & 15 deletions packages/core-sdk/test/unit/resources/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { stub } from "sinon";
import { Address, PublicClient, WalletClient, zeroAddress, zeroHash } from "viem";

import { GroupClient } from "../../../src";
import { IpAccountImplClient } from "../../../src/abi/generated";
import { IpAccountImplClient, wrappedIpAddress } from "../../../src/abi/generated";
import { LicenseDataInput } from "../../../src/types/resources/group";
import { mockAddress, txHash, walletAddress } from "../mockData";
import { aeneid, mockAddress, txHash, walletAddress } from "../mockData";
import { createMockPublicClient, createMockWalletClient } from "../testUtils";

use(chaiAsPromised);

// Aeneid (1315) allows WIP or MERC20; group tests use chainId 1315
const validCurrencyToken = wrappedIpAddress[aeneid];

const mockLicenseData: LicenseDataInput = {
licenseTermsId: 100,
licensingConfig: {
Expand Down Expand Up @@ -529,7 +533,7 @@ describe("Test IpAssetClient", () => {

const result = groupClient.collectAndDistributeGroupRoyalties({
groupIpId: mockAddress,
currencyTokens: [mockAddress],
currencyTokens: [validCurrencyToken],
memberIpIds: [mockAddress],
});
await expect(result).to.be.rejectedWith(
Expand All @@ -545,7 +549,7 @@ describe("Test IpAssetClient", () => {
.resolves(false);
const result = groupClient.collectAndDistributeGroupRoyalties({
groupIpId: mockAddress,
currencyTokens: [mockAddress],
currencyTokens: [validCurrencyToken],
memberIpIds: [mockAddress],
});
await expect(result).to.be.rejectedWith(
Expand All @@ -557,7 +561,7 @@ describe("Test IpAssetClient", () => {
stub(groupClient.ipAssetRegistryClient, "isRegistered").resolves(true);
const result = groupClient.collectAndDistributeGroupRoyalties({
groupIpId: mockAddress,
currencyTokens: [mockAddress],
currencyTokens: [validCurrencyToken],
memberIpIds: [],
});
await expect(result).to.be.rejectedWith(
Expand Down Expand Up @@ -616,7 +620,7 @@ describe("Test IpAssetClient", () => {

const result = await groupClient.collectAndDistributeGroupRoyalties({
groupIpId: mockAddress,
currencyTokens: [mockAddress],
currencyTokens: [validCurrencyToken],
memberIpIds: [mockAddress],
});
expect(result.txHash).equal(txHash);
Expand Down Expand Up @@ -648,7 +652,7 @@ describe("Test IpAssetClient", () => {
stub(groupClient.groupingWorkflowsClient, "collectRoyaltiesAndClaimReward").resolves(txHash);
const result = await groupClient.collectAndDistributeGroupRoyalties({
groupIpId: mockAddress,
currencyTokens: [mockAddress],
currencyTokens: [validCurrencyToken],
memberIpIds: [mockAddress],
});
expect(result.txHash).equal(txHash);
Expand Down Expand Up @@ -725,7 +729,7 @@ describe("Test IpAssetClient", () => {
stub(groupClient.groupingModuleClient, "getClaimableReward").rejects(new Error("rpc error"));
const result = groupClient.getClaimableReward({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
memberIpIds: [mockAddress],
});
await expect(result).to.be.rejectedWith("Failed to get claimable reward: rpc error");
Expand All @@ -735,7 +739,7 @@ describe("Test IpAssetClient", () => {
stub(groupClient.groupingModuleClient, "getClaimableReward").resolves([10n]);
const result = await groupClient.getClaimableReward({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
memberIpIds: [mockAddress],
});
expect(result).to.deep.equal([10n]);
Expand Down Expand Up @@ -767,7 +771,7 @@ describe("Test IpAssetClient", () => {
stub(groupClient.groupingModuleClient, "claimReward").rejects(new Error("rpc error"));
const result = groupClient.claimReward({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
memberIpIds: [mockAddress],
});
await expect(result).to.be.rejectedWith("Failed to claim reward: rpc error");
Expand All @@ -785,7 +789,7 @@ describe("Test IpAssetClient", () => {
]);
const result = await groupClient.claimReward({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
memberIpIds: [mockAddress],
});
expect(result.txHash).equal(txHash);
Expand All @@ -803,7 +807,7 @@ describe("Test IpAssetClient", () => {
]);
const result = await groupClient.claimReward({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
memberIpIds: [mockAddress],
});
expect(result.txHash).equal(txHash);
Expand All @@ -824,7 +828,7 @@ describe("Test IpAssetClient", () => {

const result = groupClient.collectRoyalties({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
});
await expect(result).to.be.rejectedWith("Failed to collect royalties: rpc error");
});
Expand All @@ -844,7 +848,7 @@ describe("Test IpAssetClient", () => {
]);
const result = await groupClient.collectRoyalties({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
});
expect(result.txHash).equal(txHash);
expect(result.collectedRoyalties).to.equal(100n);
Expand All @@ -866,7 +870,7 @@ describe("Test IpAssetClient", () => {

const result = await groupClient.collectRoyalties({
groupIpId: mockAddress,
currencyToken: mockAddress,
currencyToken: validCurrencyToken,
});
expect(result.txHash).equal(txHash);
expect(result.collectedRoyalties).to.equal(100n);
Expand Down
18 changes: 9 additions & 9 deletions packages/core-sdk/test/unit/resources/ipAsset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const licenseTerms: LicenseTerms = {
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: BigInt(0),
currency: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
currency: WIP_TOKEN_ADDRESS,
uri: "",
};

Expand Down Expand Up @@ -1059,7 +1059,7 @@ describe("Test IpAssetClient", () => {
{
terms: PILFlavor.commercialRemix({
defaultMintingFee: 0n,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
commercialRevShare: 90,
royaltyPolicy: NativeRoyaltyPolicy.LAP,
override: {
Expand All @@ -1078,7 +1078,7 @@ describe("Test IpAssetClient", () => {
commercialUse: true,
commercializerChecker: zeroAddress,
commercializerCheckerData: zeroAddress,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
defaultMintingFee: 0n,
derivativeRevCeiling: 0n,
derivativesAllowed: true,
Expand Down Expand Up @@ -1414,7 +1414,7 @@ describe("Test IpAssetClient", () => {
{
terms: PILFlavor.commercialUse({
defaultMintingFee: 100n,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
royaltyPolicy: NativeRoyaltyPolicy.LRP,
}),
},
Expand All @@ -1429,7 +1429,7 @@ describe("Test IpAssetClient", () => {
commercialUse: true,
commercializerChecker: zeroAddress,
commercializerCheckerData: zeroAddress,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
defaultMintingFee: 100n,
derivativeRevCeiling: 0n,
derivativesAllowed: false,
Expand Down Expand Up @@ -3011,7 +3011,7 @@ describe("Test IpAssetClient", () => {
licenseTermsData: [
{
terms: PILFlavor.creativeCommonsAttribution({
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
royaltyPolicy: mockAddress,
}),
},
Expand Down Expand Up @@ -3040,7 +3040,7 @@ describe("Test IpAssetClient", () => {
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0n,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json",
defaultMintingFee: 0n,
expiration: 0n,
Expand Down Expand Up @@ -3703,7 +3703,7 @@ describe("Test IpAssetClient", () => {
licenseTermsData: [
{
terms: PILFlavor.creativeCommonsAttribution({
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
royaltyPolicy: mockAddress,
}),
},
Expand All @@ -3728,7 +3728,7 @@ describe("Test IpAssetClient", () => {
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: 0n,
currency: mockAddress,
currency: WIP_TOKEN_ADDRESS,
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json",
defaultMintingFee: 0n,
expiration: 0n,
Expand Down
Loading