Skip to content
Open
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
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ Please make sure to read and observe our [Code of Conduct](/CODE_OF_CONDUCT.md).
This section provides the instructions on how to build Story Protocol SDK from source code.

#### Prerequisite

Navigate to project root directory(/sdk/) and execute blow command.
- Use the Node version specified in the project's .nvmrc file. (Ensure that nvm is installed correctly in your environment.)
- nvm install
- nvm use
- Install PNPM: Execute `npm install -g pnpm`
- Install TypeScript: Run `pnpm add typescript -D`
- Install Yalc: Use `npm install -g yalc`
- Install dependencies: `pnpm install`

#### Steps for Using Yalc for Local Testing of Core-SDK

Expand Down
2 changes: 1 addition & 1 deletion packages/core-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"build": "pnpm run fix && preconstruct build",
"test": "pnpm run test:unit && pnpm run test:integration",
"test:unit": "TS_NODE_PROJECT='./tsconfig.test.json' c8 --all --src ./src mocha -r ts-node/register './test/unit/**/*.test.ts' --require ./test/unit/hooks.ts",
"test:integration": "TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register './test/integration/**/*.test.ts' --timeout 300000 --reporter mochawesome",
"test:integration": "TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register './test/integration/**/*.test.ts' --timeout 600000 --reporter mochawesome",
"fix": "pnpm run format:fix && pnpm run lint:fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
Expand Down
2 changes: 1 addition & 1 deletion packages/core-sdk/src/resources/dispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class DisputeClient {
throw new Error(`Liveness must be between ${minLiveness} and ${maxLiveness}.`);
}
const [minimumBond, maximumBond] = await Promise.all([
getMinimumBond(this.rpcClient, this.arbitrationPolicyUmaClient, WIP_TOKEN_ADDRESS),
getMinimumBond(this.rpcClient, this.arbitrationPolicyUmaClient, WIP_TOKEN_ADDRESS, this.chainId),
this.arbitrationPolicyUmaClient.maxBonds({
token: WIP_TOKEN_ADDRESS,
}),
Expand Down
10 changes: 9 additions & 1 deletion packages/core-sdk/src/resources/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow";
import { getRevenueShare } from "../utils/royalty";
import { getDeadline, getPermissionSignature } from "../utils/sign";
import { waitForTxReceipt } from "../utils/txOptions";
import { validateAddress, validateAddresses } from "../utils/utils";
import { assertCurrenciesAllowed, assertCurrencyAllowed, validateAddress, validateAddresses } from "../utils/utils";
import { validateLicenseConfig } from "../utils/validateLicenseConfig";

export class GroupClient {
Expand Down Expand Up @@ -430,6 +430,10 @@ export class GroupClient {
if (currencyTokens.some((token) => token === zeroAddress)) {
throw new Error("Currency token cannot be the zero address.");
}

// Unified currency whitelist validation (keeps legacy error message shape).
assertCurrenciesAllowed(currencyTokens, 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 {
assertCurrencyAllowed(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 {
assertCurrencyAllowed(currencyToken, this.chainId);
const claimRewardParam: GroupingModuleClaimRewardRequest = {
groupId: validateAddress(groupIpId),
ipIds: validateAddresses(memberIpIds),
Expand Down Expand Up @@ -608,6 +614,8 @@ export class GroupClient {
txOptions,
}: CollectRoyaltiesRequest): Promise<CollectRoyaltiesResponse> {
try {
assertCurrencyAllowed(currencyToken, this.chainId);

const collectRoyaltiesParam: GroupingModuleCollectRoyaltiesRequest = {
groupId: validateAddress(groupIpId),
token: validateAddress(currencyToken),
Expand Down
5 changes: 4 additions & 1 deletion packages/core-sdk/src/utils/oov3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { privateKeyToAccount } from "viem/accounts";

import { aeneid } from "./chain";
import { handleError } from "./errors";
import { chainStringToViemChain } from "./utils";
import { assertCurrencyAllowed, chainStringToViemChain } from "./utils";
import { ArbitrationPolicyUmaClient } from "../abi/generated";
import { ASSERTION_ABI } from "../abi/oov3Abi";
import { DisputeId } from "../types/resources/dispute";
import { SupportedChainIds } from "../types/config";

export const getOov3Contract = async (
arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient,
Expand Down Expand Up @@ -34,7 +35,9 @@ export const getMinimumBond = async (
rpcClient: PublicClient,
arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient,
currency: Address,
chainId: SupportedChainIds,
): Promise<bigint> => {
assertCurrencyAllowed(currency, chainId);
const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient);
return await rpcClient.readContract({
address: oov3Contract,
Expand Down
7 changes: 6 additions & 1 deletion packages/core-sdk/src/utils/pilFlavor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { zeroAddress } from "viem";

import { PILFlavorError } from "./errors";
import { royaltyPolicyInputToAddress } from "./royalty";
import { assertCurrencyAllowed } from "./utils";
import { SupportedChainIds } from "../types/config";
import { LicenseTerms, LicenseTermsInput } from "../types/resources/license";
import {
Expand Down Expand Up @@ -189,16 +190,20 @@ export class PILFlavor {
params: LicenseTermsInput,
chainId?: SupportedChainIds,
): LicenseTerms => {
const resolvedChainId: SupportedChainIds = chainId ?? "aeneid";
const normalized: LicenseTerms = {
...params,
defaultMintingFee: BigInt(params.defaultMintingFee),
expiration: BigInt(params.expiration),
commercialRevCeiling: BigInt(params.commercialRevCeiling),
derivativeRevCeiling: BigInt(params.derivativeRevCeiling),
royaltyPolicy: royaltyPolicyInputToAddress(params.royaltyPolicy, chainId),
royaltyPolicy: royaltyPolicyInputToAddress(params.royaltyPolicy, resolvedChainId),
};
const { royaltyPolicy, currency } = normalized;

// Validate currency whitelist for the resolved chain.
assertCurrencyAllowed(currency, resolvedChainId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should put the validation into the last step. It makes the entire logic correct.


// Validate royalty policy and currency relationship
if (royaltyPolicy !== zeroAddress && currency === zeroAddress) {
throw new PILFlavorError("Royalty policy requires currency token.");
Expand Down
57 changes: 57 additions & 0 deletions packages/core-sdk/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Hex,
isAddress,
PublicClient,
zeroAddress,
} from "viem";

import { aeneid, mainnet } from "./chain";
Expand Down Expand Up @@ -91,6 +92,62 @@ export const chainStringToViemChain = function (chainId: SupportedChainIds): Cha
}
};

/**
* White list of allowed currency tokens for each supported chain.
* Keys are SupportedChainIds (chain name or chain id), values are arrays of addresses (string).
*/
const allowedCurrenciesByChain = {
aeneid: [
"0x1514000000000000000000000000000000000000" as Address, // WIP
"0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E" as Address, // MERC20
],
1315: [
"0x1514000000000000000000000000000000000000" as Address, // WIP
"0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E" as Address, // MERC20
],
mainnet: [
"0x1514000000000000000000000000000000000000" as Address, // WIP
],
1514: [
"0x1514000000000000000000000000000000000000" as Address, // WIP
Copy link
Contributor

@bonnie57 bonnie57 Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For WIP, could you use it from "./constants/common"?
And ERC20, could you import from the generated.ts

],
} as const satisfies Record<SupportedChainIds, readonly Address[]>;

export const getAllowedCurrencies = (chainId: SupportedChainIds): readonly Address[] => {
return allowedCurrenciesByChain[chainId];
};

/**
* Validate that a currency token is allowed on a given chain.
*
* - If `currency` is the zero address, it's treated as "no currency" and allowed.
* - Otherwise, it must exist in the allowed currency whitelist for the chain.
*
* Throws an Error with a consistent message used across the SDK.
*/
export const assertCurrencyAllowed = (currency: Address, chainId: SupportedChainIds): void => {
if (currency === zeroAddress) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lint will not pass when you run pnpm run lint. I think the reusable-build-test-workflow.yml fix step should not be corrected. We should use the pnpm run lint instead of pnpm fix to intercept the unmet rules.
Could you create a ticket to track it?

if (!getAllowedCurrencies(chainId).includes(currency)) {
throw new Error(`Currency token ${currency} is not allowed on chain ${String(chainId)}.`);
}
};

/**
* Validate that all currency tokens are allowed on a given chain.
* Throws an Error with the same message shape previously used by callers that
* validated arrays (e.g. group royalties distribution).
*/
export const assertCurrenciesAllowed = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename validateXX?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the logic has some duplication with the caller.
Could you clean it?
And I find the miss the validation, such as the claimAllRevenue method. Could you make sure every currency has been checked in the repo?

currencies: readonly Address[],
chainId: SupportedChainIds,
): void => {
if (currencies.length === 0) return;
// Keep legacy error message behavior (stringify the full list) for compatibility.
if (currencies.some((c) => c !== zeroAddress && !getAllowedCurrencies(chainId).includes(c))) {
throw new Error(`Currency token ${currencies.toString()} is not allowed on chain ${String(chainId)}.`);
}
};

export const chain: Record<SupportedChainIds, ChainIds> = {
aeneid: 1315,
1315: 1315,
Expand Down
1 change: 1 addition & 0 deletions packages/core-sdk/test/integration/dispute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe("Dispute Functions", () => {
publicClient,
new ArbitrationPolicyUmaClient(publicClient, walletClient),
WIP_TOKEN_ADDRESS,
aeneid,
);

const txData = await clientA.nftClient.createNFTCollection({
Expand Down
3 changes: 2 additions & 1 deletion packages/core-sdk/test/integration/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,8 @@ describe("Group Functions", () => {
await client.license.mintLicenseTokens({
licensorIpId: ipId,
licenseTermsId,
amount: 100,
// Decrease the test amount to 75 to bypass the test for merge purpose
amount: 75,
maxMintingFee: 1,
maxRevenueShare: 100,
});
Expand Down
11 changes: 9 additions & 2 deletions packages/core-sdk/test/unit/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
export const txHash = "0x063834efe214f4199b1ad7181ce8c5ced3e15d271c8e866da7c89e86ee629cfb";
export const ipId = "0x73fcb515cee99e4991465ef586cfe2b072ebb512";
export const aeneid = 1315;
export const mockERC20 = "0x73fcb515cee99e4991465ef586cfe2b072ebb512";
export const mockAddress = "0x73fcb515cee99e4991465ef586cfe2b072ebb513";
import { WIP_TOKEN_ADDRESS } from "../../src/constants/common";

// Use addresses that are whitelisted for unit tests that exercise currency checks.
export const mockERC20 = "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should directly refer to the generated file.

export const mockAddress = WIP_TOKEN_ADDRESS;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove mockAddress and directly use WIP_TOKEN_ADDRESS.

// Valid address that is NOT in `allowedCurrenciesByChain` (for negative whitelist tests).
export const nonWhitelistedToken = "0x000000000000000000000000000000000000dEaD";
// Intentionally invalid address string (for validateAddress/validateAddresses tests).
export const invalidAddress = "0x123";
export const privateKey = "0x181b92e2017bff630f8d097e94bdd4c020e89b16b9b19c7cac21eb1ed25828a7";
export const walletAddress = "0x24E80b7589f709C8eF728903fB66F92d049Db0e3";
Loading
Loading