├── .nvmrc ├── pnpm-workspace.yaml ├── .dockerignore ├── packages ├── core-sdk │ ├── .prettierignore │ ├── .env.example │ ├── tsdoc.json │ ├── test │ │ ├── unit │ │ │ ├── hooks.ts │ │ │ ├── mockData.ts │ │ │ ├── utils │ │ │ │ ├── errors.test.ts │ │ │ │ ├── ipfs.test.ts │ │ │ │ ├── setMaxLicenseTokens.test.ts │ │ │ │ ├── validateLicenseConfig.test.ts │ │ │ │ ├── oov3.test.ts │ │ │ │ ├── royalty.test.ts │ │ │ │ ├── sign.test.ts │ │ │ │ └── getFunctionSignature.test.ts │ │ │ ├── testUtils.ts │ │ │ ├── client.test.ts │ │ │ └── resources │ │ │ │ ├── nftClient.test.ts │ │ │ │ └── wip.test.ts │ │ └── integration │ │ │ ├── utils │ │ │ ├── generateHex.ts │ │ │ ├── BIP32.ts │ │ │ └── util.ts │ │ │ ├── wip.test.ts │ │ │ └── permission.test.ts │ ├── tsconfig.test.json │ ├── eslint.config.mjs │ ├── src │ │ ├── types │ │ │ ├── utils │ │ │ │ ├── contract.ts │ │ │ │ ├── pilFlavor.ts │ │ │ │ └── token.ts │ │ │ ├── resources │ │ │ │ ├── tagging.ts │ │ │ │ ├── wip.ts │ │ │ │ ├── nftClient.ts │ │ │ │ ├── ipAccount.ts │ │ │ │ ├── permission.ts │ │ │ │ ├── royalty.ts │ │ │ │ └── dispute.ts │ │ │ ├── config.ts │ │ │ ├── options.ts │ │ │ └── common.ts │ │ ├── constants │ │ │ └── common.ts │ │ ├── utils │ │ │ ├── errors.ts │ │ │ ├── contract.ts │ │ │ ├── getIpMetadataForWorkflow.ts │ │ │ ├── txOptions.ts │ │ │ ├── ipfs.ts │ │ │ ├── chain.ts │ │ │ ├── royalty.ts │ │ │ ├── setMaxLicenseTokens.ts │ │ │ ├── validateLicenseConfig.ts │ │ │ ├── getFunctionSignature.ts │ │ │ ├── token.ts │ │ │ ├── utils.ts │ │ │ ├── calculateMintFee.ts │ │ │ ├── oov3.ts │ │ │ ├── sign.ts │ │ │ └── registrationUtils │ │ │ │ └── registerHelper.ts │ │ ├── index.ts │ │ ├── abi │ │ │ └── oov3Abi.ts │ │ ├── resources │ │ │ ├── wip.ts │ │ │ ├── nftClient.ts │ │ │ └── ipAccount.ts │ │ └── client.ts │ ├── tsconfig.json │ ├── .vscode │ │ └── launch.json │ ├── package.json │ └── README.md ├── eslint-config-story │ ├── README.md │ ├── package.json │ └── index.js ├── tsconfig │ ├── package.json │ └── base.json ├── prettier-config │ ├── index.json │ └── package.json └── wagmi-generator │ ├── package.json │ ├── tsconfig.json │ ├── optimizedBlockExplorer.ts │ └── resolveProxyContracts.ts ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yaml │ ├── feature_request.md │ ├── tracking_issue.md │ ├── task.md │ └── bug_report.md ├── workflows │ ├── pr-internal.yml │ ├── notify-slack-on-pr-merge.yml │ ├── pr-external.yaml │ ├── scorecards.yml │ ├── create-release.yml │ ├── sync-labels.yml │ └── publish-package.yaml └── pull_request_template.md ├── .pre-commit-config.yaml ├── Dockerfile ├── .changeset └── config.json ├── turbo.json ├── .gitignore ├── configs └── configuration.json ├── LICENSE.md ├── package.json ├── README.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.0.0 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .git 4 | 5 | -------------------------------------------------------------------------------- /packages/core-sdk/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | mochawesome-report -------------------------------------------------------------------------------- /packages/eslint-config-story/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @LeoHChen @DonFungible @edisonz0718 @jacob-tucker @AndyBoWu @allenchuang @bpolania @bonnie57 @DracoLi @limengformal @lucas2brh 2 | -------------------------------------------------------------------------------- /packages/core-sdk/.env.example: -------------------------------------------------------------------------------- 1 | TEST_WALLET_ADDRESS = 0x0000000000000000000000000000000000000000 2 | WALLET_PRIVATE_KEY = 0x0000000000000000000000000000000000000000 -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@story-protocol/tsconfig", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "./base.json" 6 | } 7 | -------------------------------------------------------------------------------- /packages/core-sdk/tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "extends": ["typedoc/tsdoc.json"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/prettier-config/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "singleQuote": false, 4 | "trailingComma": "all", 5 | "printWidth": 100 6 | } -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.28.0 # latest version 4 | hooks: 5 | - id: gitleaks 6 | args: ['--verbose', '--redact'] 7 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/hooks.ts: -------------------------------------------------------------------------------- 1 | // Restores the default sandbox after every test 2 | import * as sinon from "sinon"; 3 | 4 | export const mochaHooks = { 5 | afterEach(): void { 6 | sinon.restore(); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.15.0-slim 2 | 3 | # install pnpm 4 | RUN npm i -g pnpm@8.8.0 5 | 6 | # prepare packages 7 | COPY . /repo 8 | WORKDIR /repo 9 | RUN pnpm install 10 | 11 | # set entrypoint 12 | ENTRYPOINT ["pnpm"] 13 | 14 | -------------------------------------------------------------------------------- /packages/core-sdk/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@story-protocol/tsconfig/base.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "module": "CommonJS", 6 | "lib": ["dom"] 7 | }, 8 | "include": ["./test"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core-sdk/test/integration/utils/generateHex.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | import { Hex } from "viem"; 4 | 5 | export const generateHex = (length: number = 32): Hex => { 6 | const randomBytes = crypto.randomBytes(length); 7 | return `0x${randomBytes.toString("hex")}`; 8 | }; 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch" 9 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Story Protocol Official Discord 4 | url: https://discord.gg/storyprotocol 5 | about: If you're a user, this is the fastest way to get help. Do not give your wallet private key or mnemonic words to anyone. 6 | -------------------------------------------------------------------------------- /packages/prettier-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@story-protocol/prettier-config", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "./index.json", 6 | "devDependencies": { 7 | "prettier": "^3.0.0" 8 | }, 9 | "peerDependencies": { 10 | "prettier": "^3.0.0" 11 | } 12 | } -------------------------------------------------------------------------------- /packages/core-sdk/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from "@story-protocol/eslint-config"; 2 | 3 | /** @type {import("eslint").Linter.Config[]} */ 4 | export default [ 5 | ...config, 6 | //TODO: need to fix `e` which is not being used in generated files 7 | { 8 | ignores: ["./src/abi/generated.ts", "./mochawesome-report", "./dist"], 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/utils/contract.ts: -------------------------------------------------------------------------------- 1 | import { PublicClient, SimulateContractParameters } from "viem"; 2 | 3 | import { SimpleWalletClient } from "../../abi/generated"; 4 | 5 | export type SimulateAndWriteContractParams = { 6 | rpcClient: PublicClient; 7 | wallet: SimpleWalletClient; 8 | data: Exclude; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@story-protocol/tsconfig/base.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "module": "ESNext", 6 | "outDir": "dist", 7 | "types": ["mocha"], 8 | "lib": ["dom", "es2015"], 9 | "allowJs": true 10 | }, 11 | "include": ["eslint.config.mjs", "src/**/*", "test/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/core-sdk/src/constants/common.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "viem"; 2 | 3 | import { wrappedIpAddress } from "../abi/generated"; 4 | import { mainnet } from "../utils/chain"; 5 | 6 | export const defaultFunctionSelector: Hex = "0x00000000"; 7 | export const royaltySharesTotalSupply: number = 100_000_000; 8 | export const MAX_ROYALTY_TOKEN = 100_000_000; 9 | export const WIP_TOKEN_ADDRESS = wrappedIpAddress[mainnet.id]; 10 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | }, 8 | "fix": { 9 | "cache": false 10 | }, 11 | "lint": {}, 12 | "lint:fix": {}, 13 | "test": {}, 14 | "test:integration": {}, 15 | "dev": { 16 | "cache": false, 17 | "persistent": true 18 | } 19 | }, 20 | "globalEnv": ["TEST_WALLET_ADDRESS", "WALLET_PRIVATE_KEY"] 21 | } -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export const handleError = (error: unknown, msg: string): never => { 2 | if (error instanceof Error) { 3 | const newError = new Error(`${msg}: ${error.message}`); 4 | newError.stack = error.stack; 5 | throw newError; 6 | } 7 | throw new Error(`${msg}: Unknown error type`); 8 | }; 9 | 10 | export class PILFlavorError extends Error { 11 | constructor(message: string) { 12 | super(message); 13 | this.name = "PILFlavorError"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core-sdk/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Test File", 8 | "outputCapture": "std", 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/mocha.js", 10 | "args": ["-r", "ts-node/register", "${relativeFile}"], 11 | "console": "integratedTerminal", 12 | "env": { 13 | "TS_NODE_PROJECT": "./tsconfig.test.json" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/wagmi-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@story-protocol/wagmi-generator", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "generate": "wagmi generate" 6 | }, 7 | "devDependencies": { 8 | "@types/node": "^20.8.2", 9 | "@wagmi/cli": "^2.1.15", 10 | "@wagmi/connectors": "^4.1.14", 11 | "@wagmi/core": "^2.6.5", 12 | "abitype": "^1.0.2", 13 | "change-case": "^5.4.3", 14 | "dotenv": "^16.3.1", 15 | "typescript": "^5.4.2", 16 | "viem": "^2.8.6", 17 | "wagmi": "^2.5.7" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/mockData.ts: -------------------------------------------------------------------------------- 1 | export const txHash = "0x063834efe214f4199b1ad7181ce8c5ced3e15d271c8e866da7c89e86ee629cfb"; 2 | export const ipId = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; 3 | export const aeneid = 1315; 4 | export const mockERC20 = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; 5 | export const mockAddress = "0x73fcb515cee99e4991465ef586cfe2b072ebb513"; 6 | export const privateKey = "0x181b92e2017bff630f8d097e94bdd4c020e89b16b9b19c7cac21eb1ed25828a7"; 7 | export const walletAddress = "0x24E80b7589f709C8eF728903fB66F92d049Db0e3"; 8 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/tagging.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash } from "viem"; 2 | 3 | import { TxOptions } from "../options"; 4 | 5 | export type Tag = { 6 | id: string; 7 | ipId: Address; 8 | tag: string; 9 | }; 10 | 11 | export type SetTagRequest = { 12 | tag: string; 13 | ipId: Address; 14 | txOptions?: TxOptions; 15 | }; 16 | 17 | export type SetTagResponse = { 18 | txHash: Hash; 19 | }; 20 | 21 | export type RemoveTagRequest = { 22 | tag: string; 23 | ipId: Address; 24 | txOptions?: TxOptions; 25 | }; 26 | 27 | export type RemoveTagResponse = { 28 | txHash: Hash; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/wagmi-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "allowSyntheticDefaultImports": false, 7 | "noEmitHelpers": true, 8 | "pretty": true, 9 | "sourceMap": true, 10 | "strictNullChecks": false, 11 | "baseUrl": ".", 12 | "moduleResolution": "node", 13 | "sourceRoot": ".", 14 | "lib": [ 15 | "es5", 16 | "es6", 17 | "dom" 18 | ], 19 | "typeRoots": [ 20 | "./node_modules/@types" 21 | ] 22 | }, 23 | "exclude": [ 24 | "./node_modules", 25 | ] 26 | } -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "inlineSources": false, 12 | "isolatedModules": true, 13 | "moduleResolution": "node", 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "lib": ["ESNext"] 19 | }, 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .idea 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | build 16 | 17 | # typescript 18 | dist 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # turbo 37 | .turbo 38 | 39 | # vercel 40 | .vercel 41 | 42 | # cursor 43 | .cursor 44 | 45 | # mocha test report 46 | /packages/core-sdk/mochawesome-report/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { handleError } from "../../../src/utils/errors"; 4 | 5 | describe("Test handleError", () => { 6 | it("should throw unknown error message when passing in a non-Error error", () => { 7 | try { 8 | handleError({}, "abc"); 9 | } catch (error) { 10 | expect((error as Error).message).to.equal("abc: Unknown error type"); 11 | } 12 | }); 13 | 14 | it("should throw normal error message when passing in aError", () => { 15 | try { 16 | handleError(new Error("cde"), "abc"); 17 | } catch (error) { 18 | expect((error as Error).message).to.equal("abc: cde"); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/contract.ts: -------------------------------------------------------------------------------- 1 | import { WriteContractParameters } from "viem"; 2 | 3 | import { TransactionResponse } from "../types/options"; 4 | import { SimulateAndWriteContractParams } from "../types/utils/contract"; 5 | 6 | export const simulateAndWriteContract = async ({ 7 | rpcClient, 8 | wallet, 9 | data, 10 | }: SimulateAndWriteContractParams): Promise => { 11 | const { request } = await rpcClient.simulateContract({ 12 | ...data, 13 | account: wallet.account, 14 | }); 15 | const txHash = await wallet.writeContract(request as WriteContractParameters); 16 | const receipt = await rpcClient.waitForTransactionReceipt({ hash: txHash }); 17 | return { txHash, receipt }; 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/tracking_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tracking issue 3 | about: Tracking issues are task lists used to better organize regular work items. 4 | title: 'Tracking issue for *ADD_PROJECT* - *ADD_COMPONENT*' 5 | labels: 'epic' 6 | assignees: '' 7 | 8 | --- 9 | 10 | This issue is for grouping *ADD_COMPONENT* related tasks that are necessary for *ADD_PROJECT*. 11 | 12 | ### Other tracking issues for the same project: 13 | 14 | 15 | 16 | - #XXXX 17 | - #XXXX 18 | - #XXXX 19 | 20 | 21 | ```[tasklist] 22 | ### Task list 23 | - [ ] XXXX 24 | - [ ] XXXX 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/wip.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "viem"; 2 | 3 | import { TokenAmountInput } from "../common"; 4 | import { WithTxOptions } from "../options"; 5 | 6 | export type ApproveRequest = WithTxOptions & { 7 | /** The address that will use the WIP tokens */ 8 | spender: Address; 9 | /** The amount of WIP tokens to approve. */ 10 | amount: TokenAmountInput; 11 | }; 12 | 13 | export type DepositRequest = WithTxOptions & { 14 | amount: TokenAmountInput; 15 | }; 16 | 17 | export type WithdrawRequest = WithTxOptions & { 18 | amount: TokenAmountInput; 19 | }; 20 | 21 | export type TransferRequest = WithTxOptions & { 22 | to: Address; 23 | amount: TokenAmountInput; 24 | }; 25 | 26 | export type TransferFromRequest = TransferRequest & { 27 | from: Address; 28 | }; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Create a regular work item to be picked up by a contributor. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description and context 11 | 12 | 13 | 14 | 15 | 16 | ## Suggested solution 17 | 18 | 19 | ## Definition of done 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-internal.yml: -------------------------------------------------------------------------------- 1 | name: Workflow for Internal PRs with Unit & Integration Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Timestamp: 13 | if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} 14 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main 15 | 16 | tests: 17 | needs: [Timestamp] 18 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-build-test-workflow.yml@main 19 | with: 20 | sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} 21 | ENVIRONMENT: "odyssey" 22 | secrets: 23 | WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }} 24 | TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - Browser or Nodejs 28 | - Browser [e.g. chrome, safari] 29 | - Browser Version [e.g. 22] 30 | - Node Version 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /configs/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["enhancement"] 6 | }, 7 | { 8 | "title": "## 🐛 Fixes", 9 | "labels": ["bug"] 10 | }, 11 | { 12 | "key": "tests", 13 | "title": "## 🧪 Tests", 14 | "labels": ["test"] 15 | }, 16 | { 17 | "key": "Others", 18 | "title": "## 💬 Others", 19 | "labels": ["documentation", "other"] 20 | }, 21 | { 22 | "key": "Dependencies", 23 | "title": "## 📦 Dependencies", 24 | "labels": ["dependency"] 25 | } 26 | ], 27 | "ignore_labels": ["ignore"], 28 | "pr_template": "- #{{TITLE}}\n - PR: ##{{NUMBER}}", 29 | "sort": { 30 | "order": "ASC", 31 | "on_property": "mergedAt" 32 | }, 33 | "template": "#{{CHANGELOG}}\n## Contributors:\n- #{{CONTRIBUTORS}}" 34 | } 35 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/getIpMetadataForWorkflow.ts: -------------------------------------------------------------------------------- 1 | import { Hex, zeroHash } from "viem"; 2 | 3 | export type IpMetadataForWorkflow = { 4 | /** The URI of the metadata for the IP. */ 5 | ipMetadataURI: string; 6 | /** The hash of the metadata for the IP. */ 7 | ipMetadataHash: Hex; 8 | /** The URI of the metadata for the NFT. */ 9 | nftMetadataURI: string; 10 | /** The hash of the metadata for the IP NFT. */ 11 | nftMetadataHash: Hex; 12 | }; 13 | 14 | export const getIpMetadataForWorkflow = ( 15 | ipMetadata?: Partial, 16 | ): IpMetadataForWorkflow => { 17 | return { 18 | ipMetadataURI: ipMetadata?.ipMetadataURI || "", 19 | ipMetadataHash: ipMetadata?.ipMetadataHash || zeroHash, 20 | nftMetadataURI: ipMetadata?.nftMetadataURI || "", 21 | nftMetadataHash: ipMetadata?.nftMetadataHash || zeroHash, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/eslint-config-story/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@story-protocol/eslint-config", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "private": true, 6 | "devDependencies": { 7 | "@eslint/js": "^9.26.0", 8 | "@story-protocol/prettier-config": "workspace:*", 9 | "@stylistic/eslint-plugin-ts": "^4.2.0", 10 | "@typescript-eslint/eslint-plugin": "^8.32.1", 11 | "@typescript-eslint/parser": "^8.32.1", 12 | "eslint": "^9.26.0", 13 | "eslint-config-prettier": "^8.10.0", 14 | "eslint-config-turbo": "^1.13.4", 15 | "eslint-import-resolver-typescript": "^4.3.4", 16 | "eslint-plugin-import": "^2.31.0", 17 | "eslint-plugin-tsdoc": "^0.4.0", 18 | "globals": "^16.1.0", 19 | "prettier": "^3.5.3", 20 | "typescript-eslint": "^8.32.0" 21 | }, 22 | "peerDependencies": { 23 | "@eslint/js": "^9.26.0" 24 | }, 25 | "exports": { 26 | "default": "./index.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/notify-slack-on-pr-merge.yml: -------------------------------------------------------------------------------- 1 | name: Slack Notification on PR Merge 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | notify: 9 | if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == github.event.repository.default_branch 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Send Slack Notification 14 | run: | 15 | curl -X POST \ 16 | -H 'Content-type: application/json' \ 17 | --data '{ 18 | "channel": "qa-messenger", 19 | "text": "---\n**PR Merged** \n*Repo:* '"${{ github.repository }}"'\n*Title:* '"${{ github.event.pull_request.title }}"'\n*Tag:* <'"${{ secrets.SLACK_BORIS_MEMBER_ID }}"'> <'"${{ secrets.SLACK_BO_MEMBER_ID }}"'>\n*Link:* <'"${{ github.event.pull_request.html_url }}"'|View PR>" 20 | }' \ 21 | ${{ secrets.SLACK_QA_MESSENGER_WEBHOOK_URL }} 22 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/txOptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TransactionResponse, 3 | WaitForTransactionReceiptRequest, 4 | WaitForTransactionReceiptsRequest, 5 | } from "../types/options"; 6 | 7 | export const waitForTxReceipt = async ({ 8 | txOptions, 9 | rpcClient, 10 | txHash, 11 | }: WaitForTransactionReceiptRequest): Promise => { 12 | const receipt = await rpcClient.waitForTransactionReceipt({ 13 | ...txOptions, 14 | hash: txHash, 15 | }); 16 | return { txHash, receipt }; 17 | }; 18 | 19 | export const waitForTxReceipts = async ({ 20 | txOptions, 21 | rpcClient, 22 | txHashes, 23 | }: WaitForTransactionReceiptsRequest): Promise => { 24 | const receipts = await Promise.all( 25 | txHashes.map((hash) => rpcClient.waitForTransactionReceipt({ ...txOptions, hash })), 26 | ); 27 | return receipts.map((receipt) => ({ txHash: receipt.transactionHash, receipt })); 28 | }; 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Example: 4 | This pr adds user login function, includes: 5 | 6 | - 1. add user login page. 7 | - 2. ... 8 | 9 | ## Test Plan 10 | 12 | Example: 13 | - 1. Use different test accounts for login tests, including correct user names and passwords, and incorrect user names and passwords. 14 | - 2. ... 15 | 16 | ## Related Issue 17 | 18 | 19 | Example: Issue #123 20 | 21 | ## Notes 22 | 23 | 24 | - Example: Links and navigation need to be added to the front-end interface -------------------------------------------------------------------------------- /.github/workflows/pr-external.yaml: -------------------------------------------------------------------------------- 1 | name: Workflow for External PRs with Unit & Integration Tests 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | Timestamp_PR_CREATED: 11 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main 12 | 13 | authorize: 14 | if: github.event.pull_request.head.repo.full_name != github.repository 15 | needs: [Timestamp_PR_CREATED] 16 | environment: "external" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: true 20 | 21 | Timestamp_PR_APPROVED: 22 | needs: [authorize] 23 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main 24 | 25 | tests: 26 | needs: [authorize, Timestamp_PR_APPROVED] 27 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-build-test-workflow.yml@main 28 | with: 29 | sha: ${{ github.event.pull_request.head.sha }} 30 | ENVIRONMENT: "odyssey" 31 | secrets: 32 | WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }} 33 | TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} 34 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/ipfs.ts: -------------------------------------------------------------------------------- 1 | import bs58 from "bs58"; 2 | import { base58btc } from "multiformats/bases/base58"; 3 | import { CID } from "multiformats/cid"; 4 | import { Hex } from "viem"; 5 | 6 | const v0Prefix = "1220"; 7 | export const convertCIDtoHashIPFS = (cid: string): Hex => { 8 | const isV0 = cid.startsWith("Qm"); 9 | const parsedCID = CID.parse(cid); 10 | const base58CID = isV0 ? parsedCID.toString() : parsedCID.toV0().toString(); 11 | const bytes = bs58.decode(base58CID); 12 | const base16CID = Array.from(bytes) 13 | .map((byte) => byte.toString(16).padStart(2, "0")) 14 | .join(""); 15 | return ("0x" + base16CID.slice(v0Prefix.length)) as Hex; 16 | }; 17 | 18 | export const convertHashIPFStoCID = (hash: Hex, version: "v0" | "v1" = "v0"): string => { 19 | const base16CID = v0Prefix + hash.slice(2); 20 | const bytes = new Uint8Array(base16CID.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))); 21 | const base58CID = bs58.encode(Buffer.from(bytes)); 22 | if (version === "v0") { 23 | return base58CID; 24 | } else { 25 | return CID.parse(base58CID, base58btc).toV1().toString(); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Story Protocol Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "viem/utils"; 2 | 3 | export const aeneid = defineChain({ 4 | id: 13_15, 5 | name: "aeneid", 6 | nativeCurrency: { name: "IP", symbol: "IP", decimals: 18 }, 7 | rpcUrls: { 8 | default: { 9 | http: ["https://aeneid.storyrpc.io/"], 10 | }, 11 | }, 12 | blockExplorers: { 13 | default: { 14 | name: "Explorer", 15 | url: "https://aeneid.storyscan.xyz/", 16 | }, 17 | }, 18 | contracts: { 19 | multicall3: { 20 | address: "0xca11bde05977b3631167028862be2a173976ca11", 21 | blockCreated: 1792, 22 | }, 23 | }, 24 | testnet: true, 25 | }); 26 | 27 | export const mainnet = defineChain({ 28 | id: 15_14, 29 | name: "mainnet", 30 | nativeCurrency: { name: "IP", symbol: "IP", decimals: 18 }, 31 | rpcUrls: { 32 | default: { 33 | http: ["https://mainnet.storyrpc.io/"], 34 | }, 35 | }, 36 | blockExplorers: { 37 | default: { 38 | name: "Explorer", 39 | url: "https://dev-mainnet.storyscan.xyz/", 40 | }, 41 | }, 42 | contracts: { 43 | multicall3: { 44 | address: "0xca11bde05977b3631167028862be2a173976ca11", 45 | blockCreated: 340998, 46 | }, 47 | }, 48 | testnet: false, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/wagmi-generator/optimizedBlockExplorer.ts: -------------------------------------------------------------------------------- 1 | import {BlockExplorerConfig} from "@wagmi/cli/src/plugins/blockExplorer"; 2 | import {blockExplorer} from "@wagmi/cli/plugins"; 3 | import type {ContractConfig} from "@wagmi/cli/src/config"; 4 | 5 | export function optimizedBlockExplorer(config: BlockExplorerConfig) { 6 | const getAddress = config.getAddress 7 | 8 | let lastAddress: { address: NonNullable } = null 9 | config.getAddress = (config: { address: NonNullable }) => { 10 | lastAddress = config 11 | return getAddress(config) 12 | } 13 | 14 | const result = blockExplorer(config) 15 | const contracts = result.contracts 16 | 17 | result.contracts = () => { 18 | const result = contracts() 19 | if (result instanceof Promise) { 20 | result.catch(error => { 21 | console.log("") 22 | console.error("!!! RESOLVING CONTRACTS ERROR !!!") 23 | console.log(`${error}`) 24 | console.log("----------") 25 | console.log(`Address: ${JSON.stringify(lastAddress)}`) 26 | if (error.details) { 27 | console.log(`Details: `, error.details) 28 | } 29 | }) 30 | } 31 | return result 32 | } 33 | return result 34 | } -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/royalty.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "viem"; 2 | 3 | import { chain, validateAddress } from "./utils"; 4 | import { royaltyPolicyLapAddress, royaltyPolicyLrpAddress } from "../abi/generated"; 5 | import { RevShareInput, RevShareType } from "../types/common"; 6 | import { SupportedChainIds } from "../types/config"; 7 | import { NativeRoyaltyPolicy, RoyaltyPolicyInput } from "../types/resources/royalty"; 8 | 9 | export const royaltyPolicyInputToAddress = ( 10 | input?: RoyaltyPolicyInput, 11 | chainId?: SupportedChainIds, 12 | ): Address => { 13 | const transferredChainId = chain[chainId || "aeneid"]; 14 | let address: Address; 15 | switch (input) { 16 | case undefined: 17 | case NativeRoyaltyPolicy.LAP: 18 | address = royaltyPolicyLapAddress[transferredChainId]; 19 | break; 20 | case NativeRoyaltyPolicy.LRP: 21 | address = royaltyPolicyLrpAddress[transferredChainId]; 22 | break; 23 | default: 24 | address = validateAddress(input); 25 | } 26 | return address; 27 | }; 28 | 29 | export const getRevenueShare = ( 30 | revShareNumber: RevShareInput, 31 | type: RevShareType = RevShareType.COMMERCIAL_REVENUE_SHARE, 32 | ): number => { 33 | if (revShareNumber < 0 || revShareNumber > 100) { 34 | throw new Error(`${type} must be between 0 and 100.`); 35 | } 36 | // use Math.trunc to avoid precision issues 37 | return Math.trunc(revShareNumber * 10 ** 6); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/config.ts: -------------------------------------------------------------------------------- 1 | import { Account, Address, Transport } from "viem"; 2 | 3 | import { SimpleWalletClient } from "../abi/generated"; 4 | 5 | /** 6 | * Supported chains. For convenience, both name or chain ID are supported. 7 | * 8 | * @public 9 | */ 10 | export type SupportedChainIds = "aeneid" | "mainnet" | ChainIds; 11 | 12 | /** 13 | * Configuration for the SDK Client. 14 | * 15 | * @public 16 | */ 17 | export type UseAccountStoryConfig = { 18 | readonly account: Account | Address; 19 | /** 20 | * The chain ID to use, the default is `aeneid`. 21 | * 22 | * @default 13_15 23 | */ 24 | readonly chainId?: SupportedChainIds; 25 | readonly transport: Transport; 26 | }; 27 | 28 | export type UseWalletStoryConfig = { 29 | /** 30 | * The chain ID to use, the default is `aeneid`. 31 | * 32 | * @default 13_15 33 | */ 34 | readonly chainId?: SupportedChainIds; 35 | readonly transport: Transport; 36 | readonly wallet: SimpleWalletClient; 37 | }; 38 | 39 | export type StoryConfig = { 40 | readonly transport: Transport; 41 | /** 42 | * The chain ID to use, the default is `aeneid`. 43 | * 44 | * @default 13_15 45 | */ 46 | readonly chainId?: SupportedChainIds; 47 | readonly wallet?: SimpleWalletClient; 48 | readonly account?: Account | Address; 49 | }; 50 | 51 | export type ContractAddress = { [key in SupportedChainIds]: Record }; 52 | 53 | export type ChainIds = 1315 | 1514; 54 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/setMaxLicenseTokens.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash } from "viem"; 2 | 3 | import { TotalLicenseTokenLimitHookClient } from "../abi/generated"; 4 | import { LicenseTermsDataInput } from "../types/resources/license"; 5 | 6 | export type SetMaxLicenseTokens = { 7 | maxLicenseTokensData: (LicenseTermsDataInput | { maxLicenseTokens: bigint })[]; 8 | licensorIpId: Address; 9 | licenseTermsIds: bigint[]; 10 | totalLicenseTokenLimitHookClient: TotalLicenseTokenLimitHookClient; 11 | templateAddress: Address; 12 | }; 13 | 14 | export const setMaxLicenseTokens = async ({ 15 | maxLicenseTokensData, 16 | licensorIpId, 17 | licenseTermsIds, 18 | totalLicenseTokenLimitHookClient, 19 | templateAddress, 20 | }: SetMaxLicenseTokens): Promise => { 21 | const licenseTermsMaxLimitTxHashes: Hash[] = []; 22 | for (let i = 0; i < maxLicenseTokensData.length; i++) { 23 | const maxLicenseTokens = maxLicenseTokensData[i].maxLicenseTokens; 24 | if (maxLicenseTokens === undefined || maxLicenseTokens < 0n) { 25 | continue; 26 | } 27 | const txHash = await totalLicenseTokenLimitHookClient.setTotalLicenseTokenLimit({ 28 | licensorIpId, 29 | licenseTemplate: templateAddress, 30 | licenseTermsId: licenseTermsIds[i], 31 | limit: BigInt(maxLicenseTokens), 32 | }); 33 | if (txHash) { 34 | licenseTermsMaxLimitTxHashes.push(txHash); 35 | } 36 | } 37 | return licenseTermsMaxLimitTxHashes; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/ipfs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { convertCIDtoHashIPFS, convertHashIPFStoCID } from "../../../src/utils/ipfs"; 4 | 5 | describe("IPFS", () => { 6 | it("should return hash when call convertCIDtoHashIPFS given CID with v0", () => { 7 | const result = convertCIDtoHashIPFS("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"); 8 | expect(result).to.equal("0xc3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a"); 9 | }); 10 | 11 | it("should return hash when call convertCIDtoHashIPFS given CID with v1", () => { 12 | const result = convertCIDtoHashIPFS( 13 | "bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", 14 | ); 15 | expect(result).to.equal("0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 16 | }); 17 | 18 | it("should return CID when call convertHashIPFStoCID given hash with v0", () => { 19 | const result = convertHashIPFStoCID( 20 | "0xc3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a", 21 | "v0", 22 | ); 23 | expect(result).to.equal("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"); 24 | }); 25 | 26 | it("should return CID when call convertHashIPFStoCID given hash with v1", () => { 27 | const result = convertHashIPFStoCID( 28 | "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 29 | "v1", 30 | ); 31 | expect(result).to.equal("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/validateLicenseConfig.ts: -------------------------------------------------------------------------------- 1 | import { zeroAddress, zeroHash } from "viem"; 2 | 3 | import { getRevenueShare } from "./royalty"; 4 | import { validateAddress } from "./utils"; 5 | import { LicensingConfig, LicensingConfigInput, RevShareType } from "../types/common"; 6 | 7 | export const validateLicenseConfig = (licensingConfig?: LicensingConfigInput): LicensingConfig => { 8 | if (!licensingConfig) { 9 | return { 10 | isSet: false, 11 | mintingFee: 0n, 12 | licensingHook: zeroAddress, 13 | hookData: zeroHash, 14 | commercialRevShare: 0, 15 | disabled: false, 16 | expectMinimumGroupRewardShare: 0, 17 | expectGroupRewardPool: zeroAddress, 18 | }; 19 | } 20 | const licenseConfig = { 21 | expectMinimumGroupRewardShare: getRevenueShare( 22 | licensingConfig.expectMinimumGroupRewardShare, 23 | RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE, 24 | ), 25 | commercialRevShare: getRevenueShare(licensingConfig.commercialRevShare), 26 | mintingFee: BigInt(licensingConfig.mintingFee), 27 | expectGroupRewardPool: validateAddress(licensingConfig.expectGroupRewardPool), 28 | licensingHook: validateAddress(licensingConfig.licensingHook), 29 | hookData: licensingConfig.hookData, 30 | isSet: licensingConfig.isSet, 31 | disabled: licensingConfig.disabled, 32 | }; 33 | 34 | if (licenseConfig.mintingFee < 0) { 35 | throw new Error(`The mintingFee must be greater than 0.`); 36 | } 37 | return licenseConfig; 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | workflow_dispatch: 4 | 5 | # Declare default permissions as read only. 6 | permissions: read-all 7 | 8 | jobs: 9 | analysis: 10 | name: Scorecards analysis 11 | runs-on: ubuntu-latest 12 | permissions: 13 | # Needed to upload the results to code-scanning dashboard. 14 | security-events: write 15 | id-token: write 16 | 17 | steps: 18 | - name: "Checkout code" 19 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 20 | with: 21 | persist-credentials: false 22 | 23 | # This is a pre-submit / pre-release. 24 | - name: "Run analysis" 25 | uses: ossf/scorecard-action@main 26 | with: 27 | results_file: results.sarif 28 | results_format: sarif 29 | publish_results: true 30 | 31 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 32 | # format to the repository Actions tab. 33 | - name: "Upload artifact" 34 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 35 | with: 36 | name: SARIF file 37 | path: results.sarif 38 | retention-days: 5 39 | 40 | # Upload the results to GitHub's code scanning dashboard. 41 | - name: "Upload to code-scanning" 42 | uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 43 | with: 44 | sarif_file: results.sarif 45 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/nftClient.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash } from "viem"; 2 | 3 | import { EncodedTxData } from "../../abi/generated"; 4 | import { FeeInput, TokenIdInput } from "../common"; 5 | import { TxOptions } from "../options"; 6 | 7 | export type CreateNFTCollectionRequest = { 8 | name: string; 9 | symbol: string; 10 | /** If true, anyone can mint from the collection. If false, only the addresses with the minter role can mint. */ 11 | isPublicMinting: boolean; 12 | /** Whether the collection is open for minting on creation. */ 13 | mintOpen: boolean; 14 | /** The address to receive mint fees. */ 15 | mintFeeRecipient: Address; 16 | /** The contract URI for the collection. Follows ERC-7572 standard. See https://eips.ethereum.org/EIPS/eip-7572. */ 17 | contractURI: string; 18 | /** The base URI for the collection. If baseURI is not empty, tokenURI will be either baseURI + token ID (if nftMetadataURI is empty) or baseURI + nftMetadataURI. */ 19 | baseURI?: string; 20 | maxSupply?: number; 21 | /** The cost to mint a token. */ 22 | mintFee?: FeeInput; 23 | /** The token to mint. */ 24 | mintFeeToken?: Address; 25 | /** The owner of the collection. */ 26 | owner?: Address; 27 | txOptions?: TxOptions; 28 | }; 29 | 30 | export type CreateNFTCollectionResponse = { 31 | txHash?: Hash; 32 | encodedTxData?: EncodedTxData; 33 | spgNftContract?: Address; 34 | }; 35 | 36 | export type SetTokenURIRequest = { 37 | tokenId: TokenIdInput; 38 | /** The new metadata URI to associate with the token. */ 39 | tokenURI: string; 40 | spgNftContract: Address; 41 | txOptions?: Omit; 42 | }; 43 | 44 | export type GetTokenURIRequest = { 45 | tokenId: TokenIdInput; 46 | spgNftContract: Address; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/core-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export { StoryClient } from "./client"; 2 | export { WIP_TOKEN_ADDRESS } from "./constants/common"; 3 | export { aeneid, mainnet } from "./utils/chain"; 4 | export { IPAssetClient } from "./resources/ipAsset"; 5 | export { PermissionClient } from "./resources/permission"; 6 | export { LicenseClient } from "./resources/license"; 7 | export { DisputeClient } from "./resources/dispute"; 8 | export { NftClient } from "./resources/nftClient"; 9 | export { IPAccountClient } from "./resources/ipAccount"; 10 | export { RoyaltyClient } from "./resources/royalty"; 11 | export { GroupClient } from "./resources/group"; 12 | export { WipClient } from "./resources/wip"; 13 | export { PILFlavor } from "./utils/pilFlavor"; 14 | 15 | export * from "./types/config"; 16 | export * from "./types/common"; 17 | export * from "./types/options"; 18 | export * from "./types/utils/pilFlavor"; 19 | 20 | export * from "./types/resources/ipAsset"; 21 | export * from "./types/resources/ipMetadata"; 22 | export * from "./types/resources/license"; 23 | export * from "./types/resources/royalty"; 24 | export * from "./types/resources/permission"; 25 | export * from "./types/resources/dispute"; 26 | export * from "./types/resources/ipAccount"; 27 | export * from "./types/resources/nftClient"; 28 | export * from "./types/resources/group"; 29 | export * from "./types/resources/wip"; 30 | 31 | export type { 32 | EncodedTxData, 33 | IpAccountImplStateResponse, 34 | LicensingModulePredictMintingLicenseFeeResponse, 35 | PiLicenseTemplateGetLicenseTermsResponse, 36 | } from "./abi/generated"; 37 | 38 | export { royaltyPolicyLapAddress, royaltyPolicyLrpAddress } from "./abi/generated"; 39 | 40 | export { getPermissionSignature, getSignature } from "./utils/sign"; 41 | 42 | export { convertCIDtoHashIPFS, convertHashIPFStoCID } from "./utils/ipfs"; 43 | 44 | export { settleAssertion } from "./utils/oov3"; 45 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/getFunctionSignature.ts: -------------------------------------------------------------------------------- 1 | import { Abi, AbiFunction, AbiParameter } from "viem"; 2 | 3 | /** 4 | * Gets the function signature from an ABI for a given method name 5 | * @param abi - The contract ABI 6 | * @param methodName - The name of the method to get the signature for 7 | * @param overloadIndex - Optional index for overloaded functions (0-based) 8 | * @returns The function signature in standard format (e.g. "methodName(uint256,address)") 9 | * @throws Error if method not found or if overloadIndex is required but not provided 10 | */ 11 | export const getFunctionSignature = ( 12 | abi: Abi, 13 | methodName: string, 14 | overloadIndex?: number, 15 | ): string => { 16 | const functions = abi.filter( 17 | (x): x is AbiFunction => x.type === "function" && x.name === methodName, 18 | ); 19 | 20 | if (functions.length === 0) { 21 | throw new Error(`Method ${methodName} not found in ABI.`); 22 | } 23 | 24 | if (functions.length > 1 && overloadIndex === undefined) { 25 | throw new Error( 26 | `Method ${methodName} has ${functions.length} overloads. Please specify overloadIndex (0-${ 27 | functions.length - 1 28 | }).`, 29 | ); 30 | } 31 | 32 | const func = functions[overloadIndex || 0]; 33 | 34 | const getTypeString = ( 35 | input: AbiParameter & { components?: readonly AbiParameter[] }, 36 | ): string => { 37 | if (input.type.startsWith("tuple")) { 38 | const components = input.components 39 | ?.map((comp: AbiParameter) => 40 | getTypeString(comp as AbiParameter & { components?: AbiParameter[] }), 41 | ) 42 | .join(","); 43 | return `(${components})`; 44 | } 45 | return input.type; 46 | }; 47 | 48 | const inputs = func.inputs 49 | .map((input) => getTypeString(input as AbiParameter & { components?: readonly AbiParameter[] })) 50 | .join(","); 51 | return `${methodName}(${inputs})`; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/setMaxLicenseTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { stub } from "sinon"; 3 | 4 | import { TotalLicenseTokenLimitHookClient } from "../../../src/abi/generated"; 5 | import { setMaxLicenseTokens, SetMaxLicenseTokens } from "../../../src/utils/setMaxLicenseTokens"; 6 | import { mockAddress, txHash } from "../mockData"; 7 | import { createMockPublicClient, createMockWalletClient } from "../testUtils"; 8 | 9 | describe("Test setMaxLicenseTokens", () => { 10 | const mockTotalLicenseTokenLimitHookClient = new TotalLicenseTokenLimitHookClient( 11 | createMockPublicClient(), 12 | createMockWalletClient(), 13 | ); 14 | 15 | beforeEach(() => { 16 | stub(mockTotalLicenseTokenLimitHookClient, "setTotalLicenseTokenLimit").resolves(txHash); 17 | }); 18 | 19 | it("should return empty array when maxLicenseTokensData is empty", async () => { 20 | const params: SetMaxLicenseTokens = { 21 | maxLicenseTokensData: [], 22 | licensorIpId: mockAddress, 23 | licenseTermsIds: [1n, 2n, 3n], 24 | totalLicenseTokenLimitHookClient: mockTotalLicenseTokenLimitHookClient, 25 | templateAddress: mockAddress, 26 | }; 27 | 28 | const result = await setMaxLicenseTokens(params); 29 | 30 | expect(result).to.deep.equal([]); 31 | }); 32 | 33 | it("should skip items with negative maxLicenseTokens", async () => { 34 | const params: SetMaxLicenseTokens = { 35 | maxLicenseTokensData: [ 36 | { maxLicenseTokens: 100n }, 37 | { maxLicenseTokens: -1n }, 38 | { maxLicenseTokens: 200n }, 39 | ], 40 | licensorIpId: mockAddress, 41 | licenseTermsIds: [1n, 2n, 3n], 42 | totalLicenseTokenLimitHookClient: mockTotalLicenseTokenLimitHookClient, 43 | templateAddress: mockAddress, 44 | }; 45 | 46 | const result = await setMaxLicenseTokens(params); 47 | 48 | expect(result).to.have.length(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Reusable workflow to create release in GitHub 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | tag_version: 7 | type: string 8 | required: true 9 | description: "Tag version to be published" 10 | last_tag: 11 | type: string 12 | description: "last tag name" 13 | required: true 14 | 15 | permissions: 16 | contents: write 17 | actions: write 18 | 19 | jobs: 20 | release: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 25 | 26 | - name: Tag and Push 27 | id: tag_and_push 28 | run: | 29 | TAG_VERSION=${{ inputs.tag_version}} 30 | git config --global user.name 'GitHub Actions' 31 | git config --global user.email 'actions@github.com' 32 | git tag -a $TAG_VERSION -m "Release $TAG_VERSION" 33 | git push origin $TAG_VERSION 34 | echo "The tag $TAG_VERSION has been created and pushed to the repository" 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Set Configuration File 39 | id: set_config 40 | run: | 41 | echo "config_file=configs/configuration.json" >> $GITHUB_ENV 42 | 43 | - name: "Build Changelog" 44 | id: github_release 45 | uses: mikepenz/release-changelog-builder-action@a57c1b7c90e56d9c8b26a6ed5d1eed159369e117 46 | with: 47 | configuration: ${{ env.config_file }} 48 | toTag: ${{ inputs.tag_version}} 49 | fromTag: ${{ inputs.last_tag}} 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Create Release 54 | uses: mikepenz/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 55 | with: 56 | body: ${{steps.github_release.outputs.changelog}} 57 | tag_name: ${{ inputs.tag_version }} 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-sdk", 3 | "version": "0.0.3", 4 | "private": true, 5 | "repository": "https://github.com/storyprotocol/typescript-sdk", 6 | "author": "storyprotocol engineering ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "turbo run build", 10 | "lint": "turbo run lint", 11 | "fix": "turbo run fix", 12 | "test": "turbo run test --no-cache --concurrency=1", 13 | "test:integration": "turbo run test:integration --no-cache" 14 | }, 15 | "devDependencies": { 16 | "@changesets/cli": "^2.26.2", 17 | "turbo": "^1.10.13" 18 | }, 19 | "engines": { 20 | "node": "^20.0.0" 21 | }, 22 | "pnpm": { 23 | "overrides": { 24 | "braces@<3.0.3": ">=3.0.3", 25 | "ws": "^8.17.1", 26 | "ws@>=7.0.0 <7.5.10": ">=7.5.10", 27 | "ws@>=6.0.0 <6.2.3": ">=6.2.3", 28 | "axios@>=1.3.2 <=1.7.3": ">=1.7.4", 29 | "elliptic@>=4.0.0 <=6.5.6": ">=6.5.7", 30 | "elliptic@>=2.0.0 <=6.5.6": ">=6.5.7", 31 | "elliptic@>=5.2.1 <=6.5.6": ">=6.5.7", 32 | "micromatch@<4.0.8": ">=4.0.8", 33 | "rollup@<2.79.2": ">=2.79.2", 34 | "fast-xml-parser@>=4.3.5 <4.4.1": ">=4.4.1", 35 | "secp256k1@=5.0.0": ">=5.0.1", 36 | "send@<0.19.0": ">=0.19.0", 37 | "serve-static@<1.16.0": ">=1.16.0", 38 | "cross-spawn@<6.0.6": ">=6.0.6", 39 | "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5", 40 | "elliptic@<6.6.0": ">=6.6.0", 41 | "elliptic@<6.5.6": ">=6.5.6", 42 | "path-to-regexp@>=4.0.0 <6.3.0": ">=6.3.0", 43 | "esbuild@<=0.24.2": ">=0.25.0", 44 | "elliptic@<=6.6.0": ">=6.6.1", 45 | "axios@>=1.0.0 <1.8.2": ">=1.8.2", 46 | "image-size@>=1.1.0 <1.2.1": ">=1.2.1", 47 | "@babel/runtime@<7.26.10": ">=7.26.10", 48 | "@babel/helpers@<7.26.10": ">=7.26.10", 49 | "base-x@=5.0.0": ">=5.0.1", 50 | "serialize-javascript@>=6.0.0 <6.0.2": ">=6.0.2", 51 | "brace-expansion@>=1.0.0 <=1.1.11": ">=1.1.12", 52 | "brace-expansion@>=2.0.0 <=2.0.1": ">=2.0.2", 53 | "brace-expansion": "1.1.12", 54 | "@eslint/plugin-kit@<0.3.3": ">=0.3.3", 55 | "form-data@>=4.0.0 <4.0.4": ">=4.0.4" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/wagmi-generator/resolveProxyContracts.ts: -------------------------------------------------------------------------------- 1 | import type { Evaluate } from "@wagmi/cli/src/types"; 2 | import type { ContractConfig } from "@wagmi/cli/src/config"; 3 | import { Address } from "viem"; 4 | 5 | export type Config = { 6 | baseUrl: string; 7 | contracts: Evaluate>[]; 8 | chainId: number; 9 | }; 10 | export type ProxyMap = (config: { 11 | address: NonNullable; 12 | }) => Address; 13 | 14 | interface RpcResponse { 15 | result: Address; 16 | } 17 | 18 | function getAddress( 19 | chainId: number, 20 | address: 21 | | string 22 | | (Record & Partial>) 23 | ): Address { 24 | if (typeof address == "string") { 25 | return address as Address; 26 | } else { 27 | return address[chainId]; 28 | } 29 | } 30 | 31 | function makeProxyMap( 32 | chainId: number, 33 | proxyContractsMap: Record 34 | ): ProxyMap { 35 | return function (config): Address { 36 | let address: Address = getAddress(chainId, config.address); 37 | 38 | if (address in proxyContractsMap) { 39 | return proxyContractsMap[address]; 40 | } else { 41 | return address; 42 | } 43 | }; 44 | } 45 | 46 | export async function resolveProxyContracts(config: Config): Promise { 47 | const proxyContractsMap: Record = {}; 48 | for (let contract of config.contracts) { 49 | const address: Address = getAddress(config.chainId, contract.address); 50 | const resp = await fetch(config.baseUrl, { 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json", 54 | }, 55 | body: JSON.stringify({ 56 | id: 1, 57 | jsonrpc: "2.0", 58 | method: "eth_getStorageAt", 59 | params: [ 60 | address, 61 | "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", 62 | "latest", 63 | ], 64 | }), 65 | }); 66 | const rpcResponse: RpcResponse = await resp.json(); 67 | if (!/^0x0+$/[Symbol.match](rpcResponse.result)) { 68 | proxyContractsMap[address] = `0x${rpcResponse.result.substring( 69 | rpcResponse.result.length - 40 70 | )}`; 71 | } 72 | } 73 | 74 | return makeProxyMap(config.chainId, proxyContractsMap); 75 | } 76 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | 3 | import { stub } from "sinon"; 4 | import { 5 | Address, 6 | createPublicClient, 7 | createWalletClient, 8 | GetBlockReturnType, 9 | Hex, 10 | http, 11 | keccak256, 12 | PublicClient, 13 | SimulateContractReturnType, 14 | WaitForTransactionReceiptReturnType, 15 | WalletClient, 16 | } from "viem"; 17 | import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; 18 | 19 | import { mockAddress, privateKey, txHash } from "./mockData"; 20 | import { aeneid } from "../../src"; 21 | 22 | export const createMockPublicClient = (): PublicClient => { 23 | const publicClient = createPublicClient({ 24 | chain: aeneid, 25 | transport: http(), 26 | }); 27 | publicClient.waitForTransactionReceipt = stub().resolves({ 28 | transactionHash: txHash, 29 | } as unknown as WaitForTransactionReceiptReturnType); 30 | publicClient.getBlock = stub().resolves({ 31 | timestamp: 1629820800n, 32 | } as unknown as GetBlockReturnType); 33 | publicClient.readContract = stub().resolves(txHash); 34 | publicClient.simulateContract = stub().resolves({ 35 | request: {}, 36 | } as unknown as SimulateContractReturnType); 37 | publicClient.getBalance = stub().resolves(1000n); 38 | return publicClient; 39 | }; 40 | 41 | export const createMockWalletClient = (): WalletClient => { 42 | const walletClient = createWalletClient({ 43 | chain: aeneid, 44 | transport: http(), 45 | account: privateKeyToAccount(privateKey), 46 | }); 47 | walletClient.writeContract = stub().resolves(txHash); 48 | walletClient.signTypedData = stub().resolves( 49 | "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", 50 | ); 51 | return walletClient; 52 | }; 53 | 54 | export const generateRandomHash = (): Hex => keccak256(randomBytes(32)); 55 | 56 | export const generateRandomAddress = (): Address => { 57 | const account = privateKeyToAccount(generatePrivateKey()); 58 | const address = account.address; 59 | return address; 60 | }; 61 | 62 | /** 63 | * Create a mock object with a fixed address 64 | * @returns A mock object with the given address 65 | */ 66 | export const createMockWithAddress = (): T => { 67 | const mockObj = { 68 | address: mockAddress, 69 | } as unknown as T; 70 | return mockObj; 71 | }; 72 | -------------------------------------------------------------------------------- /packages/core-sdk/test/integration/wip.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import { parseEther } from "viem"; 4 | 5 | import { StoryClient } from "../../src"; 6 | import { getStoryClient, TEST_WALLET_ADDRESS } from "./utils/util"; 7 | 8 | use(chaiAsPromised); 9 | 10 | describe("WIP Functions", () => { 11 | let client: StoryClient; 12 | 13 | before(() => { 14 | client = getStoryClient(); 15 | }); 16 | 17 | describe("deposit", () => { 18 | it(`should deposit 0.01 WIP`, async () => { 19 | const ipAmt = parseEther("0.01"); 20 | const balanceBefore = await client.getWalletBalance(); 21 | const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); 22 | const rsp = await client.wipClient.deposit({ 23 | amount: ipAmt, 24 | }); 25 | expect(rsp.txHash).to.be.a("string"); 26 | const balanceAfter = await client.getWalletBalance(); 27 | const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); 28 | expect(wipAfter).to.equal(wipBefore + ipAmt); 29 | const gasCost = rsp.receipt.gasUsed * rsp.receipt.effectiveGasPrice; 30 | expect(balanceAfter).to.equal(balanceBefore - ipAmt - gasCost); 31 | }); 32 | }); 33 | describe("transfer", () => { 34 | it("should transfer WIP", async () => { 35 | const rsp = await client.wipClient.transfer({ 36 | to: TEST_WALLET_ADDRESS, 37 | amount: parseEther("0.01"), 38 | }); 39 | expect(rsp.txHash).to.be.a("string"); 40 | //Due to approve cannot approve msy.sender, so skip transferFrom test 41 | }); 42 | }); 43 | 44 | describe("withdraw", () => { 45 | it("should withdrawal WIP", async () => { 46 | const balanceBefore = await client.getWalletBalance(); 47 | const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); 48 | const rsp = await client.wipClient.withdraw({ 49 | amount: wipBefore, 50 | }); 51 | expect(rsp.txHash).to.be.a("string"); 52 | const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS); 53 | expect(wipAfter).to.equal(0n); 54 | const balanceAfter = await client.getWalletBalance(); 55 | const gasCost = rsp.receipt.gasUsed * rsp.receipt.effectiveGasPrice; 56 | expect(balanceAfter).to.equal(balanceBefore + wipBefore - gasCost); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/ipAccount.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, Hex } from "viem"; 2 | 3 | import { EncodedTxData } from "../../abi/generated"; 4 | import { DeadlineInput, TokenAmountInput } from "../common"; 5 | import { TxOptions } from "../options"; 6 | 7 | export type IPAccountExecuteRequest = { 8 | /** The IP ID of the IP Account {@link https://docs.story.foundation/docs/ip-account}. */ 9 | ipId: Address; 10 | /** The recipient of the transaction. */ 11 | to: Address; 12 | /** The amount of IP to send. */ 13 | value: number; 14 | /** The data to send along with the transaction. */ 15 | data: Hex; 16 | txOptions?: TxOptions; 17 | }; 18 | 19 | export type IPAccountExecuteResponse = { 20 | txHash?: Hash; 21 | encodedTxData?: EncodedTxData; 22 | }; 23 | 24 | export type IPAccountExecuteWithSigRequest = { 25 | /** The IP ID of the IP Account {@link https://docs.story.foundation/docs/ip-account}.*/ 26 | ipId: Address; 27 | /** The recipient of the transaction. */ 28 | to: Address; 29 | /** The data to send along with the transaction. */ 30 | data: Hex; 31 | /** The signer of the transaction. */ 32 | signer: Address; 33 | /** The deadline of the transaction signature in seconds. */ 34 | deadline: DeadlineInput; 35 | /** The signature of the transaction, EIP-712 encoded. The helper method `getPermissionSignature` supports generating the signature. */ 36 | signature: Address; 37 | /** The amount of IP to send. */ 38 | value?: number | bigint; 39 | txOptions?: TxOptions; 40 | }; 41 | 42 | export type IPAccountExecuteWithSigResponse = { 43 | txHash?: Hash; 44 | encodedTxData?: EncodedTxData; 45 | }; 46 | 47 | export type IpAccountStateResponse = Hex; 48 | 49 | export type TokenResponse = { 50 | chainId: bigint; 51 | tokenContract: Address; 52 | tokenId: bigint; 53 | }; 54 | 55 | export type SetIpMetadataRequest = { 56 | ipId: Address; 57 | /** The metadataURI to set for the IP asset. */ 58 | metadataURI: string; 59 | /** The hash of metadata at metadataURI. */ 60 | metadataHash: Hex; 61 | txOptions?: Omit; 62 | }; 63 | 64 | export type TransferErc20Request = { 65 | ipId: Address; 66 | tokens: { 67 | /** The address of the ERC20 token including WIP and standard ERC20. */ 68 | address: Address; 69 | amount: TokenAmountInput; 70 | /** The address of the recipient. */ 71 | target: Address; 72 | }[]; 73 | txOptions?: Omit; 74 | }; 75 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/utils/pilFlavor.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "viem"; 2 | 3 | import { FeeInput, RevShareInput } from "../common"; 4 | import { SupportedChainIds } from "../config"; 5 | import { LicenseTermsInput } from "../resources/license"; 6 | import { RoyaltyPolicyInput } from "../resources/royalty"; 7 | 8 | export type NonCommercialSocialRemixingRequest = { 9 | /** Optional overrides for the default license terms. */ 10 | override?: Partial; 11 | /** 12 | * The chain ID to use for this license flavor. 13 | * 14 | * @default aeneid 15 | */ 16 | chainId?: SupportedChainIds; 17 | }; 18 | 19 | export type CommercialRemixRequest = { 20 | /** The fee to be paid when minting a license. */ 21 | defaultMintingFee: FeeInput; 22 | /** Percentage of revenue that must be shared with the licensor. Must be between 0 and 100. */ 23 | commercialRevShare: RevShareInput; 24 | /** The ERC20 token to be used to pay the minting fee */ 25 | currency: Address; 26 | /** 27 | * The type of royalty policy to be used. 28 | * 29 | * @default LAP 30 | */ 31 | royaltyPolicy?: RoyaltyPolicyInput; 32 | 33 | /** 34 | * The chain ID to use for this license flavor. 35 | * 36 | * @default aeneid 37 | */ 38 | chainId?: SupportedChainIds; 39 | /** Optional overrides for the default license terms. */ 40 | override?: Partial; 41 | }; 42 | 43 | export type CommercialUseRequest = { 44 | /** The fee to be paid when minting a license. */ 45 | defaultMintingFee: FeeInput; 46 | /** The ERC20 token to be used to pay the minting fee. */ 47 | currency: Address; 48 | /** 49 | * The type of royalty policy to be used. 50 | * 51 | * @default LAP 52 | */ 53 | royaltyPolicy?: RoyaltyPolicyInput; 54 | /** 55 | * The chain ID to use for this license flavor. 56 | * 57 | * @default aeneid 58 | */ 59 | chainId?: SupportedChainIds; 60 | /** Optional overrides for the default license terms. */ 61 | override?: Partial; 62 | }; 63 | 64 | export type CreativeCommonsAttributionRequest = { 65 | /** The ERC20 token to be used to pay the minting fee. */ 66 | currency: Address; 67 | /** 68 | * The type of royalty policy to be used. 69 | * 70 | * @default LAP 71 | */ 72 | royaltyPolicy?: RoyaltyPolicyInput; 73 | /** 74 | * The chain ID to use for this license flavor. 75 | * 76 | * @default aeneid 77 | */ 78 | chainId?: SupportedChainIds; 79 | /** Optional overrides for the default license terms. */ 80 | override?: Partial; 81 | }; 82 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, PublicClient } from "viem"; 2 | 3 | import { 4 | EncodedTxData, 5 | Erc20Client, 6 | Multicall3Aggregate3Request, 7 | SimpleWalletClient, 8 | } from "../../abi/generated"; 9 | import { TokenClient, WipTokenClient } from "../../utils/token"; 10 | import { ERC20Options, TransactionResponse, TxOptions, WipOptions } from "../options"; 11 | 12 | export type Multicall3ValueCall = Multicall3Aggregate3Request["calls"][0] & { value: bigint }; 13 | 14 | export type Fee = { 15 | token: Address; 16 | amount: bigint; 17 | }; 18 | export type TokenSpender = { 19 | address: Address; 20 | /** 21 | * Amount that the address will spend in erc20 token. 22 | * If not provided, then unlimited amount is assumed. 23 | */ 24 | amount?: bigint; 25 | /** 26 | * The token address that the address will spend. 27 | */ 28 | token: Address; 29 | }; 30 | 31 | export type ApprovalCall = { 32 | spenders: TokenSpender[]; 33 | client: TokenClient; 34 | rpcClient: PublicClient; 35 | /** owner is the address calling the approval */ 36 | owner: Address; 37 | /** when true, will return an array of {@link Multicall3ValueCall} */ 38 | useMultiCall: boolean; 39 | multicallAddress?: Address; 40 | }; 41 | 42 | export type TokenApprovalCall = { 43 | spenders: TokenSpender[]; 44 | client: Erc20Client; 45 | multicallAddress: Address; 46 | rpcClient: PublicClient; 47 | /** owner is the address calling the approval */ 48 | owner: Address; 49 | /** when true, will return an array of {@link Multicall3ValueCall} */ 50 | useMultiCall: boolean; 51 | }; 52 | 53 | export type ContractCallWithFees = { 54 | multicall3Address: Address; 55 | /** all possible spenders of the erc20 tokens. */ 56 | tokenSpenders: TokenSpender[]; 57 | contractCall: () => Promise; 58 | encodedTxs: EncodedTxData[]; 59 | rpcClient: PublicClient; 60 | wallet: SimpleWalletClient; 61 | sender: Address; 62 | options?: { 63 | wipOptions?: WipOptions; 64 | erc20Options?: ERC20Options; 65 | }; 66 | txOptions?: TxOptions; 67 | }; 68 | 69 | export type MulticallWithWrapIp = { 70 | calls: Multicall3ValueCall[]; 71 | ipAmountToWrap: bigint; 72 | contractCall: () => Promise; 73 | wipSpenders: TokenSpender[]; 74 | multicall3Address: Address; 75 | wipClient: WipTokenClient; 76 | rpcClient: PublicClient; 77 | wallet: SimpleWalletClient; 78 | wipOptions?: WipOptions; 79 | }; 80 | 81 | export type ContractCallWithFeesResponse = Promise< 82 | T extends Hash[] ? TransactionResponse[] : TransactionResponse 83 | >; 84 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/options.ts: -------------------------------------------------------------------------------- 1 | import { Hash, PublicClient, TransactionReceipt, WaitForTransactionReceiptParameters } from "viem"; 2 | 3 | export type TxOptions = Omit & { 4 | /** 5 | * When this option is set, the transaction will not submit and execute. 6 | * It will only encode the ABI and function data and return. 7 | */ 8 | encodedTxDataOnly?: boolean; 9 | }; 10 | 11 | export type WithTxOptions = { 12 | txOptions?: TxOptions; 13 | }; 14 | 15 | export type ERC20Options = { 16 | /** 17 | * Automatically approve erc20 usage when erc20 is needed but current allowance 18 | * is not sufficient. 19 | * Set this to `false` to disable this behavior. 20 | * 21 | * @default true 22 | */ 23 | enableAutoApprove?: boolean; 24 | }; 25 | /** 26 | * Options to override the default behavior of the auto approve logic 27 | */ 28 | export type WithERC20Options = { 29 | options?: { 30 | erc20Options?: ERC20Options; 31 | }; 32 | }; 33 | 34 | export type WipOptions = { 35 | /** 36 | * Use multicall to batch the WIP calls into one transaction when possible. 37 | * 38 | * @default true 39 | */ 40 | useMulticallWhenPossible?: boolean; 41 | 42 | /** 43 | * By default IP is converted to WIP if the current WIP 44 | * balance does not cover the fees. 45 | * Set this to `false` to disable this behavior. 46 | * 47 | * @default true 48 | */ 49 | enableAutoWrapIp?: boolean; 50 | 51 | /** 52 | * Automatically approve WIP usage when WIP is needed but current allowance 53 | * is not sufficient. 54 | * Set this to `false` to disable this behavior. 55 | * 56 | * @default true 57 | */ 58 | enableAutoApprove?: boolean; 59 | }; 60 | /** 61 | * Options to override the default behavior of the auto wrapping IP 62 | * and auto approve logic. 63 | */ 64 | export type WithWipOptions = { 65 | options?: { 66 | /** options to configure WIP behavior */ 67 | wipOptions?: WipOptions; 68 | }; 69 | }; 70 | 71 | export type WithErc20AndWipOptions = { 72 | options?: { 73 | /** options to configure ERC20 behavior */ 74 | erc20Options?: ERC20Options; 75 | /** options to configure WIP behavior */ 76 | wipOptions?: WipOptions; 77 | }; 78 | }; 79 | 80 | export type WaitForTransactionReceiptRequest = { 81 | txHash: Hash; 82 | txOptions?: TxOptions; 83 | rpcClient: PublicClient; 84 | }; 85 | 86 | export type WaitForTransactionReceiptsRequest = { 87 | txHashes: Hash[]; 88 | txOptions?: TxOptions; 89 | rpcClient: PublicClient; 90 | }; 91 | 92 | export type TransactionResponse = { 93 | txHash: Hash; 94 | receipt: TransactionReceipt; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, PublicClient } from "viem"; 2 | 3 | import { EncodedTxData, Erc20Client, SimpleWalletClient, WrappedIpClient } from "../abi/generated"; 4 | 5 | export interface TokenClient { 6 | balanceOf(account: Address): Promise; 7 | allowance(owner: string, spender: string): Promise; 8 | approve(spender: string, value: bigint): Promise; 9 | approveEncode(spender: Address, value: bigint): EncodedTxData; 10 | } 11 | 12 | export class ERC20Client implements TokenClient { 13 | private ercClient: Erc20Client; 14 | 15 | constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, address: Address) { 16 | this.ercClient = new Erc20Client(rpcClient, wallet, address); 17 | } 18 | 19 | async balanceOf(account: Address): Promise { 20 | return await this.ercClient.balanceOf({ account }); 21 | } 22 | 23 | async allowance(owner: Address, spender: Address): Promise { 24 | return await this.ercClient.allowance({ owner, spender }); 25 | } 26 | 27 | async approve(spender: Address, value: bigint): Promise { 28 | return await this.ercClient.approve({ spender, value }); 29 | } 30 | 31 | approveEncode(spender: Address, value: bigint): EncodedTxData { 32 | return this.ercClient.approveEncode({ spender, value }); 33 | } 34 | 35 | // The method only will work in test environment 36 | async mint(to: Address, amount: bigint): Promise { 37 | return await this.ercClient.mint({ to, amount }); 38 | } 39 | } 40 | 41 | export class WipTokenClient implements TokenClient { 42 | private wipClient: WrappedIpClient; 43 | 44 | constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { 45 | this.wipClient = new WrappedIpClient(rpcClient, wallet); 46 | } 47 | 48 | async balanceOf(account: Address): Promise { 49 | const { result: balance } = await this.wipClient.balanceOf({ owner: account }); 50 | return balance; 51 | } 52 | 53 | async allowance(owner: Address, spender: Address): Promise { 54 | const { result: allowance } = await this.wipClient.allowance({ owner, spender }); 55 | return allowance; 56 | } 57 | 58 | async approve(spender: Address, value: bigint): Promise { 59 | return await this.wipClient.approve({ spender, amount: value }); 60 | } 61 | 62 | approveEncode(spender: Address, value: bigint): EncodedTxData { 63 | return this.wipClient.approveEncode({ spender, amount: value }); 64 | } 65 | 66 | depositEncode(): EncodedTxData { 67 | return this.wipClient.depositEncode(); 68 | } 69 | 70 | get address(): Address { 71 | return this.wipClient.address; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/validateLicenseConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { zeroAddress, zeroHash } from "viem"; 3 | 4 | import { validateLicenseConfig } from "../../../src/utils/validateLicenseConfig"; 5 | 6 | describe("validateLicenseConfig", () => { 7 | it("should return licenseConfig", () => { 8 | const licensingConfig = { 9 | isSet: true, 10 | mintingFee: 0n, 11 | licensingHook: zeroAddress, 12 | hookData: zeroAddress, 13 | commercialRevShare: 0, 14 | disabled: false, 15 | expectMinimumGroupRewardShare: 10, 16 | expectGroupRewardPool: zeroAddress, 17 | }; 18 | const result = validateLicenseConfig(licensingConfig); 19 | expect(result).to.deep.equal({ 20 | ...licensingConfig, 21 | expectMinimumGroupRewardShare: 10000000, 22 | }); 23 | }); 24 | it("should throw error when expectMinimumGroupRewardShare is greater than 100", () => { 25 | const licensingConfig = { 26 | isSet: true, 27 | mintingFee: 0n, 28 | licensingHook: zeroAddress, 29 | hookData: zeroAddress, 30 | commercialRevShare: 0, 31 | disabled: false, 32 | expectMinimumGroupRewardShare: 101, 33 | expectGroupRewardPool: zeroAddress, 34 | }; 35 | expect(() => validateLicenseConfig(licensingConfig)).to.throw( 36 | "expectMinimumGroupRewardShare must be between 0 and 100.", 37 | ); 38 | }); 39 | it("should throw error when expectMinimumGroupRewardShare is less than 0", () => { 40 | const licensingConfig = { 41 | isSet: true, 42 | mintingFee: 0n, 43 | licensingHook: zeroAddress, 44 | hookData: zeroAddress, 45 | commercialRevShare: 0, 46 | disabled: false, 47 | expectMinimumGroupRewardShare: -1, 48 | expectGroupRewardPool: zeroAddress, 49 | }; 50 | expect(() => validateLicenseConfig(licensingConfig)).to.throw( 51 | "expectMinimumGroupRewardShare must be between 0 and 100.", 52 | ); 53 | }); 54 | it("should throw error when mintingFee is less than 0", () => { 55 | const licensingConfig = { 56 | isSet: true, 57 | mintingFee: -1n, 58 | licensingHook: zeroAddress, 59 | hookData: zeroAddress, 60 | commercialRevShare: 0, 61 | disabled: false, 62 | expectMinimumGroupRewardShare: 0, 63 | expectGroupRewardPool: zeroAddress, 64 | }; 65 | expect(() => validateLicenseConfig(licensingConfig)).to.throw( 66 | "The mintingFee must be greater than 0.", 67 | ); 68 | }); 69 | 70 | it("should return default value when licensingConfig is not provided", () => { 71 | const result = validateLicenseConfig(); 72 | expect(result).to.deep.equal({ 73 | isSet: false, 74 | mintingFee: 0n, 75 | licensingHook: zeroAddress, 76 | hookData: zeroHash, 77 | commercialRevShare: 0, 78 | disabled: false, 79 | expectMinimumGroupRewardShare: 0, 80 | expectGroupRewardPool: zeroAddress, 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/core-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@story-protocol/core-sdk", 3 | "version": "1.4.2", 4 | "description": "Story Protocol Core SDK", 5 | "main": "dist/story-protocol-core-sdk.cjs.js", 6 | "module": "dist/story-protocol-core-sdk.esm.js", 7 | "exports": { 8 | ".": { 9 | "module": "./dist/story-protocol-core-sdk.esm.js", 10 | "default": "./dist/story-protocol-core-sdk.cjs.js" 11 | }, 12 | "./package.json": "./package.json" 13 | }, 14 | "sideEffects": false, 15 | "files": [ 16 | "dist/**/*" 17 | ], 18 | "scripts": { 19 | "build": "pnpm run fix && preconstruct build", 20 | "test": "pnpm run test:unit && pnpm run test:integration", 21 | "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", 22 | "test:integration": "TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register './test/integration/**/*.test.ts' --timeout 300000 --reporter mochawesome", 23 | "fix": "pnpm run format:fix && pnpm run lint:fix", 24 | "format": "prettier --check .", 25 | "format:fix": "prettier --write .", 26 | "lint:fix": "pnpm run lint --fix", 27 | "lint": "eslint", 28 | "tsc": "tsc --noEmit" 29 | }, 30 | "license": "MIT", 31 | "dependencies": { 32 | "@scure/bip32": "^1.6.2", 33 | "abitype": "^0.10.2", 34 | "axios": "^1.5.1", 35 | "bs58": "^6.0.0", 36 | "dotenv": "^16.3.1", 37 | "multiformats": "9.9.0", 38 | "viem": "^2.8.12" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.23.0", 42 | "@babel/preset-env": "^7.22.20", 43 | "@babel/preset-typescript": "^7.23.0", 44 | "@eslint/js": "^9.26.0", 45 | "@preconstruct/cli": "^2.8.1", 46 | "@story-protocol/eslint-config": "workspace:*", 47 | "@story-protocol/prettier-config": "workspace:*", 48 | "@story-protocol/tsconfig": "workspace:*", 49 | "@types/chai": "^4.3.6", 50 | "@types/chai-as-promised": "^7.1.6", 51 | "@types/mocha": "^10.0.2", 52 | "@types/node": "^20.8.2", 53 | "@types/sinon": "^10.0.18", 54 | "c8": "^8.0.1", 55 | "chai": "^4.3.10", 56 | "chai-as-promised": "^7.1.1", 57 | "eslint": "^9.26.0", 58 | "globals": "^16.0.0", 59 | "mocha": "^10.2.0", 60 | "mochawesome": "^7.1.3", 61 | "prettier": "^2.8.8", 62 | "sinon": "^16.0.0", 63 | "ts-node": "^10.9.1", 64 | "typechain": "^8.3.1", 65 | "typescript": "^5.2.2", 66 | "typescript-eslint": "^8.32.0" 67 | }, 68 | "prettier": "@story-protocol/prettier-config", 69 | "babel": { 70 | "presets": [ 71 | "@babel/preset-env", 72 | "@babel/preset-typescript" 73 | ] 74 | }, 75 | "preconstruct": { 76 | "entrypoints": [ 77 | "index.ts" 78 | ], 79 | "exports": true 80 | }, 81 | "c8": { 82 | "exclude": [ 83 | "test/**/*", 84 | "src/abi/**/*", 85 | "src/index.ts", 86 | "src/types/**/*" 87 | ], 88 | "check-coverage": true, 89 | "lines": 90, 90 | "functions": 90, 91 | "branches": 90, 92 | "statements": 90 93 | }, 94 | "typedoc": { 95 | "entryPoint": "./src/index.ts", 96 | "tsconfig": "./tsconfig.json" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/core-sdk/README.md: -------------------------------------------------------------------------------- 1 | # Story Protocol SDK [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/storyprotocol/sdk/blob/main/LICENSE.md)[![npm version](https://img.shields.io/npm/v/@story-protocol/core-sdk)](https://www.npmjs.com/package/@story-protocol/core-sdk)[![npm downloads](https://img.shields.io/npm/dm/@story-protocol/core-sdk)](https://www.npmjs.com/package/@story-protocol/core-sdk) 2 | 3 | Welcome to the Story Protocol SDK - a comprehensive toolkit for building applications on Story Protocol. This SDK empowers developers to seamlessly interact with intellectual property (IP) assets on the blockchain through an intuitive API interface. 4 | 5 | Key Features: 6 | 7 | - IP Asset Module: Register, and manage intellectual property assets on-chain 8 | - License Module: Create customizable license terms, attach them to IP assets, and mint transferable license tokens 9 | - Royalty Module: Claim royalties, and manage payment distributions 10 | - Dispute Module: Initiate, manage and resolve IP-related disputes through on-chain governance 11 | - Group Module: Create IP collections with shared revenue pools 12 | - WIP Module: Wrap native IP into ERC-20 tokens for DeFi integrations 13 | - NFT Client Module: Mint a new SPG collection for use with Story Protocol. 14 | 15 | The SDK provides robust support for the following networks: 16 | 17 | - [aeneid](https://docs.story.foundation/network/network-info/aeneid) - A dedicated testnet environment for development and testing 18 | - [mainnet](https://docs.story.foundation/network/network-info/mainnet) - The production network for live deployments 19 | 20 | # Documentation 21 | 22 | For more detailed information on using the SDK, refer to the [TypeScript SDK Guide](https://docs.story.foundation/developers/typescript-sdk/overview). 23 | 24 | The documentation is divided into the following sections: 25 | 26 | - [Overview](https://docs.story.foundation/developers/typescript-sdk/overview) 27 | - [Setup](https://docs.story.foundation/developers/typescript-sdk/setup) 28 | - [Register an IP Asset](https://docs.story.foundation/developers/typescript-sdk/register-ip-asset) 29 | - [Attach Terms to an IPA](https://docs.story.foundation/developers/typescript-sdk/attach-terms) 30 | - [Mint a License Token](https://docs.story.foundation/developers/typescript-sdk/mint-license) 31 | - [Register a Derivative](https://docs.story.foundation/developers/typescript-sdk/register-derivative) 32 | - [Pay an IPA](https://docs.story.foundation/developers/typescript-sdk/pay-ipa) 33 | - [Claim Revenue](https://docs.story.foundation/developers/typescript-sdk/claim-revenue) 34 | - [Raise a Dispute](https://docs.story.foundation/developers/typescript-sdk/raise-dispute) 35 | 36 | ## Release 37 | 38 | | Package | Description | 39 | | :------------------------------ | :--------------------------------------------- | 40 | | [core-sdk](./packages/core-sdk) | The core sdk for interacting with the protocol | 41 | 42 | ## Contributing 43 | 44 | Pull requests are welcome. For major changes, please open an issue first 45 | to discuss what you would like to change. Details see: [CONTRIBUTING](/CONTRIBUTING.md) 46 | 47 | Please make sure to update tests as appropriate. 48 | 49 | ## License 50 | 51 | [Copyright (c) 2023-Present Story Protocol Inc.](/LICENSE.md) 52 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/oov3.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { stub } from "sinon"; 3 | import * as viem from "viem"; 4 | import { generatePrivateKey } from "viem/accounts"; 5 | 6 | import { ArbitrationPolicyUmaClient } from "../../../src/abi/generated"; 7 | import { getAssertionDetails, getOov3Contract, settleAssertion } from "../../../src/utils/oov3"; 8 | import { mockAddress, txHash } from "../mockData"; 9 | import { createMockPublicClient, createMockWithAddress } from "../testUtils"; 10 | 11 | describe("oov3", () => { 12 | let rpcClient: viem.PublicClient; 13 | let arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient; 14 | 15 | beforeEach(() => { 16 | rpcClient = createMockPublicClient(); 17 | arbitrationPolicyUmaClient = createMockWithAddress(); 18 | arbitrationPolicyUmaClient.oov3 = stub().resolves(mockAddress); 19 | }); 20 | 21 | it("should get assertion details", async () => { 22 | rpcClient.readContract = stub().resolves({ 23 | bond: 1n, 24 | }); 25 | const assertionDetails = await getAssertionDetails( 26 | rpcClient, 27 | arbitrationPolicyUmaClient, 28 | mockAddress, 29 | ); 30 | expect(assertionDetails).to.equal(1n); 31 | }); 32 | 33 | it("should get oov3 contract address", async () => { 34 | const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient); 35 | expect(oov3Contract).to.equal(mockAddress); 36 | }); 37 | 38 | describe("settleAssertion", () => { 39 | const privateKey = generatePrivateKey(); 40 | const originalWalletClient = Object.getOwnPropertyDescriptor(viem, "createWalletClient"); 41 | const originalPublicClient = Object.getOwnPropertyDescriptor(viem, "createPublicClient"); 42 | 43 | afterEach(() => { 44 | // Restore original properties 45 | if (originalWalletClient) { 46 | Object.defineProperty(viem, "createWalletClient", originalWalletClient); 47 | } 48 | 49 | if (originalPublicClient) { 50 | Object.defineProperty(viem, "createPublicClient", originalPublicClient); 51 | } 52 | }); 53 | 54 | it("should throw error when call settleAssertion given privateKey is not valid", async () => { 55 | // It's not possible to stub viem functions, so we need to replace the original functions with stubs. 56 | Object.defineProperty(viem, "createWalletClient", { 57 | value: () => ({ 58 | writeContract: stub().rejects(new Error("rpc error")), 59 | }), 60 | }); 61 | await expect(settleAssertion(privateKey, 1n)).to.be.rejectedWith( 62 | "Failed to settle assertion: rpc error", 63 | ); 64 | }); 65 | 66 | it("should return the hash of the settlement", async () => { 67 | // It's not possible to stub viem functions, so we need to replace the original functions with stubs. 68 | Object.defineProperty(viem, "createWalletClient", { 69 | value: () => ({ 70 | writeContract: stub().resolves(txHash), 71 | }), 72 | }); 73 | Object.defineProperty(viem, "createPublicClient", { 74 | value: () => ({ 75 | waitForTransactionReceipt: stub().resolves(txHash), 76 | readContract: stub().resolves(1n), 77 | }), 78 | }); 79 | const hash = await settleAssertion(privateKey, 1n); 80 | expect(hash).to.equal(hash); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Story Protocol SDK 2 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/storyprotocol/sdk/blob/main/LICENSE.md) 3 | [![npm version](https://img.shields.io/npm/v/@story-protocol/core-sdk)](https://www.npmjs.com/package/@story-protocol/core-sdk) 4 | [![npm downloads](https://img.shields.io/npm/dm/@story-protocol/core-sdk)](https://www.npmjs.com/package/@story-protocol/core-sdk) 5 | [![CodeQL](https://github.com/storyprotocol/sdk/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/storyprotocol/sdk/actions/workflows/github-code-scanning/codeql) 6 | 7 | Welcome to the Story Protocol SDK - a comprehensive toolkit for building applications on Story Protocol. This SDK empowers developers to seamlessly interact with intellectual property (IP) assets on the blockchain through an intuitive API interface. 8 | 9 | Key Features: 10 | 11 | - IP Asset Module: Register, and manage intellectual property assets on-chain 12 | - License Module: Create customizable license terms, attach them to IP assets, and mint transferable license tokens 13 | - Royalty Module: Claim royalties, and manage payment distributions 14 | - Dispute Module: Initiate, manage and resolve IP-related disputes through on-chain governance 15 | - Group Module: Create IP collections with shared revenue pools 16 | - WIP Module: Wrap native IP into ERC-20 tokens for DeFi integrations 17 | - NFT Client Module: Mint a new SPG collection for use with Story Protocol. 18 | 19 | The SDK provides robust support for the following networks: 20 | 21 | - [aeneid](https://docs.story.foundation/network/network-info/aeneid) - A dedicated testnet environment for development and testing 22 | - [mainnet](https://docs.story.foundation/network/network-info/mainnet) - The production network for live deployments 23 | 24 | # Documentation 25 | 26 | For more detailed information on using the SDK, refer to the [TypeScript SDK Guide](https://docs.story.foundation/developers/typescript-sdk/overview). 27 | 28 | The documentation is divided into the following sections: 29 | 30 | - [Overview](https://docs.story.foundation/developers/typescript-sdk/overview) 31 | - [Setup](https://docs.story.foundation/developers/typescript-sdk/setup) 32 | - [Register an IP Asset](https://docs.story.foundation/developers/typescript-sdk/register-ip-asset) 33 | - [Attach Terms to an IPA](https://docs.story.foundation/developers/typescript-sdk/attach-terms) 34 | - [Mint a License Token](https://docs.story.foundation/developers/typescript-sdk/mint-license) 35 | - [Register a Derivative](https://docs.story.foundation/developers/typescript-sdk/register-derivative) 36 | - [Pay an IPA](https://docs.story.foundation/developers/typescript-sdk/pay-ipa) 37 | - [Claim Revenue](https://docs.story.foundation/developers/typescript-sdk/claim-revenue) 38 | - [Raise a Dispute](https://docs.story.foundation/developers/typescript-sdk/raise-dispute) 39 | 40 | ## Release 41 | 42 | | Package | Description | 43 | | :------------------------------ | :--------------------------------------------- | 44 | | [core-sdk](./packages/core-sdk) | The core sdk for interacting with the protocol | 45 | 46 | ## Contributing 47 | 48 | Pull requests are welcome. For major changes, please open an issue first 49 | to discuss what you would like to change. Details see: [CONTRIBUTING](/CONTRIBUTING.md) 50 | 51 | Please make sure to update tests as appropriate. 52 | 53 | ## License 54 | 55 | [Copyright (c) 2023-Present Story Protocol Inc.](/LICENSE.md) 56 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | Address, 4 | Chain, 5 | ContractEventName, 6 | decodeEventLog, 7 | DecodeEventLogReturnType, 8 | formatEther, 9 | Hash, 10 | Hex, 11 | isAddress, 12 | PublicClient, 13 | } from "viem"; 14 | 15 | import { aeneid, mainnet } from "./chain"; 16 | import { ChainIds, SupportedChainIds } from "../types/config"; 17 | 18 | export const waitTxAndFilterLog = async < 19 | const TAbi extends Abi | readonly unknown[], 20 | TEventName extends ContractEventName | undefined = ContractEventName, 21 | TTopics extends Hex[] = Hex[], 22 | TData extends Hex | undefined = undefined, 23 | TStrict extends boolean = true, 24 | >( 25 | client: PublicClient, 26 | txHash: Hash, 27 | params: { 28 | abi: TAbi; 29 | eventName: TEventName; 30 | from?: Hex; 31 | confirmations?: number; 32 | pollingInterval?: number; 33 | timeout?: number; 34 | }, 35 | ): Promise[]> => { 36 | const txReceipt = await client.waitForTransactionReceipt({ 37 | hash: txHash, 38 | confirmations: params.confirmations, 39 | pollingInterval: params.pollingInterval, 40 | timeout: params.timeout, 41 | }); 42 | const targetLogs: DecodeEventLogReturnType[] = []; 43 | for (const log of txReceipt.logs) { 44 | try { 45 | if (params.from && log.address !== params.from.toLowerCase()) { 46 | continue; 47 | } 48 | const currentLog = decodeEventLog({ 49 | abi: params.abi, 50 | eventName: params.eventName, 51 | data: log.data as TData, 52 | topics: log.topics as [signature: Hex, ...args: TTopics], 53 | }); 54 | targetLogs.push(currentLog); 55 | } catch { 56 | continue; 57 | } 58 | } 59 | 60 | if (targetLogs.length === 0) { 61 | throw new Error(`Not found event ${params.eventName} in target transaction`); 62 | } 63 | return targetLogs; 64 | }; 65 | 66 | export const waitTx = async function ( 67 | client: PublicClient, 68 | txHash: Hash, 69 | params?: { 70 | confirmations?: number; 71 | pollingInterval?: number; 72 | timeout?: number; 73 | }, 74 | ): Promise { 75 | await client.waitForTransactionReceipt({ 76 | hash: txHash, 77 | ...params, 78 | }); 79 | }; 80 | 81 | export const chainStringToViemChain = function (chainId: SupportedChainIds): Chain { 82 | switch (chainId) { 83 | case 1315: 84 | case "aeneid": 85 | return aeneid; 86 | case 1514: 87 | case "mainnet": 88 | return mainnet; 89 | default: 90 | throw new Error(`ChainId ${String(chainId)} not supported`); 91 | } 92 | }; 93 | 94 | export const chain: Record = { 95 | aeneid: 1315, 96 | 1315: 1315, 97 | mainnet: 1514, 98 | 1514: 1514, 99 | }; 100 | 101 | export const validateAddress = function (address: string): Address { 102 | if (!isAddress(address, { strict: false })) { 103 | throw Error(`Invalid address: ${address}.`); 104 | } 105 | return address; 106 | }; 107 | 108 | export const validateAddresses = function (addresses: string[]): Address[] { 109 | return addresses.map((address) => validateAddress(address)); 110 | }; 111 | 112 | export const getTokenAmountDisplay = function (amount: bigint, unit = "IP"): string { 113 | return `${formatEther(amount)}${unit}`; 114 | }; 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@storyprotocol.xyz. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/client.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { createWalletClient, http, Transport } from "viem"; 3 | import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; 4 | 5 | import { aeneid, StoryClient, StoryConfig } from "../../src/index"; 6 | 7 | const rpc = "http://127.0.0.1:8545"; 8 | 9 | describe("Test StoryClient", () => { 10 | describe("Test constructor", () => { 11 | it("should succeed when passing in default params", () => { 12 | const client = StoryClient.newClient({ 13 | transport: http(rpc), 14 | account: privateKeyToAccount(generatePrivateKey()), 15 | }); 16 | expect(client).to.be.instanceOf(StoryClient); 17 | }); 18 | 19 | it("should throw transport error when transport field is missing", () => { 20 | expect(() => { 21 | StoryClient.newClient({ 22 | transport: null as unknown as Transport, 23 | account: privateKeyToAccount(generatePrivateKey()), 24 | }); 25 | }).to.throw("transport is null, please pass in a valid RPC Provider URL as the transport."); 26 | }); 27 | 28 | it("should throw error when not specify a wallet or account", () => { 29 | expect(() => { 30 | StoryClient.newClient({ 31 | transport: http(rpc), 32 | }); 33 | }).to.throw("must specify a wallet or account"); 34 | }); 35 | 36 | it("should succeed when passing in wallet", () => { 37 | const client = StoryClient.newClient({ 38 | transport: http(rpc), 39 | wallet: createWalletClient({ 40 | account: privateKeyToAccount(generatePrivateKey()), 41 | chain: aeneid, 42 | transport: http(rpc), 43 | }), 44 | }); 45 | 46 | expect(client).to.be.instanceOf(StoryClient); 47 | }); 48 | 49 | it("should return client storyClient when new newClientUseWallet given wallet config", () => { 50 | const client = StoryClient.newClientUseWallet({ 51 | transport: http(rpc), 52 | wallet: createWalletClient({ 53 | account: privateKeyToAccount(generatePrivateKey()), 54 | chain: aeneid, 55 | transport: http(rpc), 56 | }), 57 | }); 58 | expect(client).to.be.instanceOf(StoryClient); 59 | }); 60 | 61 | it("should return client storyClient when new newClientUseAccount given account config", () => { 62 | const client = StoryClient.newClientUseAccount({ 63 | transport: http(rpc), 64 | account: privateKeyToAccount(generatePrivateKey()), 65 | }); 66 | expect(client).to.be.instanceOf(StoryClient); 67 | }); 68 | }); 69 | 70 | describe("Test getters", () => { 71 | const account = privateKeyToAccount(generatePrivateKey()); 72 | const transport = http(rpc); 73 | const config: StoryConfig = { 74 | chainId: "aeneid", 75 | transport, 76 | account, 77 | }; 78 | 79 | const client: StoryClient = StoryClient.newClient(config); 80 | it("client modules should not be empty", () => { 81 | expect(client.ipAsset).to.not.equal(null).and.to.not.equal(undefined); 82 | expect(client.permission).to.not.equal(null).and.to.not.equal(undefined); 83 | expect(client.license).to.not.equal(null).and.to.not.equal(undefined); 84 | expect(client.ipAccount).to.not.equal(null).and.to.not.equal(undefined); 85 | expect(client.dispute).to.not.equal(null).and.to.not.equal(undefined); 86 | expect(client.royalty).to.not.equal(null).and.to.not.equal(undefined); 87 | expect(client.nftClient).to.not.equal(null).and.to.not.equal(undefined); 88 | expect(client.groupClient).to.not.equal(null).and.to.not.equal(undefined); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/royalty.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { 4 | NativeRoyaltyPolicy, 5 | RevShareType, 6 | royaltyPolicyLapAddress, 7 | royaltyPolicyLrpAddress, 8 | } from "../../../src"; 9 | import { getRevenueShare, royaltyPolicyInputToAddress } from "../../../src/utils/royalty"; 10 | import { aeneid, mockAddress } from "../mockData"; 11 | 12 | describe("royaltyPolicyInputToAddress", () => { 13 | it("should return LAP address if no input is provided", () => { 14 | const address = royaltyPolicyInputToAddress(); 15 | expect(address).to.equal(royaltyPolicyLapAddress[aeneid]); 16 | }); 17 | 18 | it("should return the correct address for a native LAP", () => { 19 | const address = royaltyPolicyInputToAddress(NativeRoyaltyPolicy.LAP); 20 | expect(address).to.equal(royaltyPolicyLapAddress[aeneid]); 21 | }); 22 | it("should return the correct address for a native LRP", () => { 23 | const address = royaltyPolicyInputToAddress(NativeRoyaltyPolicy.LRP); 24 | expect(address).to.equal(royaltyPolicyLrpAddress[aeneid]); 25 | }); 26 | it("should return the correct address for a custom royalty policy address", () => { 27 | const address = royaltyPolicyInputToAddress(mockAddress); 28 | expect(address).to.equal(mockAddress); 29 | }); 30 | it("should return the correct address for a native LAP with a chainId", () => { 31 | const address = royaltyPolicyInputToAddress(NativeRoyaltyPolicy.LAP, "aeneid"); 32 | expect(address).to.equal(royaltyPolicyLapAddress[aeneid]); 33 | }); 34 | it("should return the correct address for a native LRP with a chainId", () => { 35 | const address = royaltyPolicyInputToAddress(NativeRoyaltyPolicy.LRP, "aeneid"); 36 | expect(address).to.equal(royaltyPolicyLrpAddress[aeneid]); 37 | }); 38 | }); 39 | 40 | describe("getRevenueShare", () => { 41 | it("should throw error when call getRevenueShare given revShare is less than 0", () => { 42 | expect(() => getRevenueShare(-1, RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE)).to.throw( 43 | "expectMinimumGroupRewardShare must be between 0 and 100.", 44 | ); 45 | }); 46 | 47 | it("should throw error when call getRevenueShare given revShare is greater than 100", () => { 48 | expect(() => getRevenueShare(101, RevShareType.MAX_REVENUE_SHARE)).to.throw( 49 | "maxRevenueShare must be between 0 and 100.", 50 | ); 51 | }); 52 | 53 | it("should throw error given revShare is greater than 100 and type is maxAllowedRewardSharePercentage", () => { 54 | expect(() => getRevenueShare(101, RevShareType.MAX_ALLOWED_REWARD_SHARE_PERCENTAGE)).to.throw( 55 | "maxAllowedRewardSharePercentage must be between 0 and 100.", 56 | ); 57 | }); 58 | 59 | it("should throw error given revShare is greater than 100 and type is expectMinimumGroupRewardShare", () => { 60 | expect(() => getRevenueShare(101, RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE)).to.throw( 61 | "expectMinimumGroupRewardShare must be between 0 and 100.", 62 | ); 63 | }); 64 | 65 | it("should return correct value when call getRevenueShare given revShare is 10", () => { 66 | expect(getRevenueShare(10)).to.equal(10000000); 67 | }); 68 | 69 | it("should return correct value when call getRevenueShare given revShare is 10.000001", () => { 70 | expect(getRevenueShare(10.000001)).to.equal(10000001); 71 | }); 72 | 73 | it("should return correct value when call getRevenueShare given revShare is 10.00000001", () => { 74 | expect(getRevenueShare(10.00000001)).to.equal(10000000); 75 | }); 76 | 77 | it("should return correct value when call getRevenueShare given revShare is 0", () => { 78 | expect(getRevenueShare(0, RevShareType.MAX_ALLOWED_REWARD_SHARE)).to.equal(0); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/core-sdk/test/integration/utils/BIP32.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | import { HDKey } from "@scure/bip32"; 4 | import { Address, createWalletClient, Hex, http, parseEther } from "viem"; 5 | import { privateKeyToAccount } from "viem/accounts"; 6 | 7 | import { getStoryClient, publicClient, RPC, TEST_PRIVATE_KEY } from "./util"; 8 | import { StoryClient } from "../../../src"; 9 | import { chainStringToViemChain } from "../../../src/utils/utils"; 10 | 11 | /** 12 | * Extract a standard private key directly from an extended private key without derivation. 13 | * 14 | * @param xprv Extended private key 15 | * @returns Extracted standard private key as a hex string 16 | */ 17 | export const getPrivateKeyFromXprv = (xprv: string): Hex => { 18 | // Create HDKey instance from xprv 19 | const hdKey = HDKey.fromExtendedKey(xprv); 20 | // Extract the private key buffer 21 | const privateKeyBuffer = hdKey.privateKey; 22 | 23 | if (!privateKeyBuffer) { 24 | throw new Error("Failed to extract private key"); 25 | } 26 | 27 | // Convert private key buffer to hex string (prefixed with 0x for compatibility with viem) 28 | return `0x${Buffer.from(privateKeyBuffer).toString("hex")}`; 29 | }; 30 | 31 | /** 32 | * Create an extended private key (xprv) deterministically from a standard private key. 33 | * 34 | * @param privateKey Ethereum private key (with or without 0x prefix) 35 | * @returns Deterministically derived extended private key (xprv) 36 | */ 37 | export const getXprvFromPrivateKey = (privateKey: string): string => { 38 | // Remove 0x prefix if present 39 | const pkHex = privateKey.toString().replace(/^0x/, ""); 40 | 41 | // Validate private key length (must be exactly 32 bytes / 64 hex characters) 42 | if (pkHex.length !== 64) { 43 | throw new Error("Private key must be 32 bytes (64 hex characters)"); 44 | } 45 | 46 | // Create a salt by hashing the private key with SHA256 47 | const salt = crypto.createHash("sha256").update(Buffer.from(pkHex, "hex")).digest(); 48 | 49 | // Derive additional entropy using HMAC-SHA512 50 | const hmac = crypto.createHmac("sha512", salt); 51 | hmac.update(Buffer.from(pkHex, "hex")); 52 | const seedBuffer = hmac.digest(); 53 | 54 | // Generate HDKey instance from seed 55 | const hdKey = HDKey.fromMasterSeed(seedBuffer); 56 | 57 | // Return the extended private key (xprv) 58 | return hdKey.privateExtendedKey; 59 | }; 60 | 61 | /** 62 | * Get a StoryClient instance for a new wallet that is deterministically derived from an extended private key (xprv). 63 | * The wallet is ensured to have a minimum balance of 5 tokens before being returned. 64 | * @returns StoryClient instance for the new wallet 65 | */ 66 | export const getDerivedStoryClient = async (): Promise<{ 67 | clientB: StoryClient; 68 | address: Address; 69 | }> => { 70 | const xprv = getXprvFromPrivateKey(TEST_PRIVATE_KEY); 71 | const privateKey = getPrivateKeyFromXprv(xprv); 72 | const clientB = getStoryClient(privateKey); 73 | const walletB = privateKeyToAccount(privateKey); 74 | 75 | // ClientA transfer some funds to walletB 76 | const clientAWalletClient = createWalletClient({ 77 | chain: chainStringToViemChain("aeneid"), 78 | transport: http(RPC), 79 | account: privateKeyToAccount(TEST_PRIVATE_KEY), 80 | }); 81 | const clientBBalance = await publicClient.getBalance({ 82 | address: walletB.address, 83 | }); 84 | 85 | if (clientBBalance < parseEther("5")) { 86 | // Less than 5 tokens (assuming 1 token = 0.01 ETH) 87 | const txHash = await clientAWalletClient.sendTransaction({ 88 | to: walletB.address, 89 | value: parseEther("5"), 90 | }); 91 | await publicClient.waitForTransactionReceipt({ hash: txHash }); 92 | } 93 | 94 | return { clientB, address: walletB.address }; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/calculateMintFee.ts: -------------------------------------------------------------------------------- 1 | import { Address, PublicClient, zeroAddress } from "viem"; 2 | 3 | import { 4 | licensingModuleAbi, 5 | licensingModuleAddress, 6 | LicensingModulePredictMintingLicenseFeeRequest, 7 | LicensingModulePredictMintingLicenseFeeResponse, 8 | SpgnftImplReadOnlyClient, 9 | } from "../abi/generated"; 10 | import { ChainIds } from "../types/config"; 11 | import { CalculateDerivativeMintingFeeConfig } from "../types/utils/registerHelper"; 12 | import { Fee } from "../types/utils/token"; 13 | 14 | export type PredictMintingLicenseFeeParams = { 15 | predictMintingFeeRequest: LicensingModulePredictMintingLicenseFeeRequest; 16 | rpcClient: PublicClient; 17 | chainId: ChainIds; 18 | walletAddress: Address; 19 | }; 20 | 21 | /** 22 | * Predict the minting license fee. 23 | * 24 | * @remarks 25 | * The method passes `walletAddress` to the `readContract` function so the smart contract can verify 26 | * if the wallet is the owner of the IP ID. The wallet address is required when using the default license terms ID. 27 | */ 28 | export const predictMintingLicenseFee = async ({ 29 | predictMintingFeeRequest, 30 | rpcClient, 31 | chainId, 32 | walletAddress, 33 | }: PredictMintingLicenseFeeParams): Promise => { 34 | const result = await rpcClient.readContract({ 35 | abi: licensingModuleAbi, 36 | address: licensingModuleAddress[chainId], 37 | functionName: "predictMintingLicenseFee", 38 | args: [ 39 | predictMintingFeeRequest.licensorIpId, 40 | predictMintingFeeRequest.licenseTemplate, 41 | predictMintingFeeRequest.licenseTermsId, 42 | predictMintingFeeRequest.amount, 43 | predictMintingFeeRequest.receiver, 44 | predictMintingFeeRequest.royaltyContext, 45 | ], 46 | account: walletAddress, 47 | }); 48 | return { 49 | currencyToken: result[0], 50 | tokenAmount: result[1], 51 | }; 52 | }; 53 | 54 | export const calculateDerivativeMintingFee = async ({ 55 | derivData, 56 | rpcClient, 57 | chainId, 58 | wallet, 59 | sender, 60 | }: CalculateDerivativeMintingFeeConfig): Promise => { 61 | const walletAddress = sender || wallet.account!.address; 62 | const mintFees: Fee[] = []; 63 | for (let i = 0; i < derivData.parentIpIds.length; i++) { 64 | const mintFee = await calculateLicenseMintFee({ 65 | predictMintingFeeRequest: { 66 | licensorIpId: derivData.parentIpIds[i], 67 | licenseTemplate: derivData.licenseTemplate, 68 | licenseTermsId: derivData.licenseTermsIds[i], 69 | receiver: walletAddress, 70 | amount: 1n, 71 | royaltyContext: zeroAddress, 72 | }, 73 | rpcClient, 74 | chainId, 75 | walletAddress, 76 | }); 77 | if (mintFee.amount > 0n) { 78 | mintFees.push(mintFee); 79 | } 80 | } 81 | return mintFees; 82 | }; 83 | 84 | export const calculateLicenseMintFee = async ({ 85 | predictMintingFeeRequest, 86 | rpcClient, 87 | chainId, 88 | walletAddress, 89 | }: PredictMintingLicenseFeeParams): Promise => { 90 | const { currencyToken, tokenAmount } = await predictMintingLicenseFee({ 91 | predictMintingFeeRequest, 92 | rpcClient, 93 | chainId, 94 | walletAddress, 95 | }); 96 | return { 97 | token: currencyToken, 98 | amount: tokenAmount, 99 | }; 100 | }; 101 | 102 | export const calculateSPGMintFee = async ( 103 | spgNftClient: SpgnftImplReadOnlyClient, 104 | ): Promise => { 105 | const token = await spgNftClient.mintFeeToken(); 106 | const amount = await spgNftClient.mintFee(); 107 | if (amount > 0n) { 108 | return { 109 | token, 110 | amount, 111 | }; 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /packages/eslint-config-story/index.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import turbo from "eslint-plugin-turbo"; 4 | import tsdocPlugin from "eslint-plugin-tsdoc"; 5 | import eslintConfigPrettier from "eslint-config-prettier"; 6 | import tsParser from "@typescript-eslint/parser"; 7 | import importPlugin from "eslint-plugin-import"; 8 | import tsEslintPlugin from "@typescript-eslint/eslint-plugin"; 9 | import tsStylistic from "@stylistic/eslint-plugin-ts"; 10 | /** 11 | * A shared ESLint configuration for the repository. 12 | * 13 | * @type {import("eslint").Linter.Config[]} 14 | * */ 15 | export default [ 16 | js.configs.recommended, 17 | eslintConfigPrettier, 18 | ...tseslint.configs.recommended, 19 | ...tseslint.configs.recommendedTypeChecked, 20 | importPlugin.flatConfigs.recommended, 21 | importPlugin.flatConfigs.typescript, 22 | { 23 | plugins: { 24 | turbo, 25 | tsdocPlugin, 26 | tsEslintPlugin, 27 | "@stylistic/ts": tsStylistic, 28 | }, 29 | settings: { 30 | "import/resolver": { 31 | // You will also need to install and configure the TypeScript resolver 32 | // See also https://github.com/import-js/eslint-import-resolver-typescript#configuration 33 | typescript: true, 34 | node: true, 35 | }, 36 | }, 37 | languageOptions: { 38 | parser: tsParser, 39 | parserOptions: { 40 | projectService: true, 41 | tsconfigRootDir: import.meta.dirname, 42 | }, 43 | }, 44 | rules: { 45 | // eslint 46 | curly: "error", 47 | eqeqeq: "error", 48 | "no-implicit-coercion": ["error", { boolean: false }], 49 | "no-unused-expressions": "error", 50 | "no-useless-computed-key": "error", 51 | "no-console": "error", 52 | "func-style": ["error", "expression"], 53 | "no-duplicate-imports": "error", 54 | "default-case": "error", 55 | eqeqeq: "error", 56 | "prefer-const": "error", 57 | 58 | // Typescript 59 | "no-shadow": "off", 60 | "@typescript-eslint/no-shadow": "error", 61 | "@typescript-eslint/no-unused-vars": "error", 62 | "@typescript-eslint/no-unsafe-argument": "off", // causing a lot of IDE false positives. 63 | "@typescript-eslint/explicit-function-return-type": "error", 64 | "@typescript-eslint/naming-convention": [ 65 | "error", 66 | { 67 | selector: ["variable", "function", "parameter"], 68 | format: ["camelCase", "UPPER_CASE"], 69 | }, 70 | { 71 | selector: ["enumMember", "enum"], 72 | format: ["UPPER_CASE", "PascalCase"], 73 | }, 74 | { 75 | selector: ["typeLike"], 76 | format: ["PascalCase"], 77 | }, 78 | ], 79 | // import rules 80 | "import/newline-after-import": "error", 81 | "import/no-cycle": "error", 82 | "import/no-useless-path-segments": "error", 83 | "import/order": [ 84 | "error", 85 | { 86 | groups: ["builtin", "external", "internal"], 87 | "newlines-between": "always", 88 | named: true, 89 | alphabetize: { 90 | order: "asc", 91 | caseInsensitive: true, 92 | }, 93 | }, 94 | ], 95 | 96 | // stylistic 97 | "@stylistic/ts/padding-line-between-statements": [ 98 | "error", 99 | { blankLine: "always", prev: "function", next: "function" }, 100 | { blankLine: "always", prev: "class", next: "class" }, 101 | { blankLine: "always", prev: "interface", next: "interface" }, 102 | { blankLine: "always", prev: "type", next: "type" }, 103 | { blankLine: "always", prev: "block-like", next: "block-like" }, 104 | ], 105 | }, 106 | }, 107 | ]; 108 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/permission.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, Hex } from "viem"; 2 | 3 | import { EncodedTxData, SimpleWalletClient } from "../../abi/generated"; 4 | import { DeadlineInput } from "../common"; 5 | import { ChainIds } from "../config"; 6 | import { TxOptions } from "../options"; 7 | 8 | export type SetPermissionsRequest = { 9 | /** The IP ID that grants the permission for `signer`. */ 10 | ipId: Address; 11 | /** 12 | * The address that will be granted permission to execute transactions. 13 | * This address will be able to call functions on `to` 14 | * on behalf of the IP Account {@link https://docs.story.foundation/docs/ip-account}. 15 | */ 16 | signer: Address; 17 | /** The address that can be called by the `signer` (currently only modules can be `to`). */ 18 | to: Address; 19 | /** The new permission level of {@link AccessPermission}. */ 20 | permission: AccessPermission; 21 | /** 22 | * The function selector string of `to` that can be called by the `signer` on behalf of the IP Account {@link https://docs.story.foundation/docs/ip-account}. 23 | * Be default, it allows all functions. 24 | * @default 0x00000000 25 | */ 26 | func?: string; 27 | txOptions?: TxOptions; 28 | }; 29 | 30 | export type SetPermissionsResponse = { 31 | txHash?: Hash; 32 | encodedTxData?: EncodedTxData; 33 | success?: boolean; 34 | }; 35 | 36 | export type SetAllPermissionsRequest = { 37 | /** The IP ID that grants the permission for `signer`. */ 38 | ipId: Address; 39 | /** The address of the signer receiving the permissions. */ 40 | signer: Address; 41 | /** The new permission level of {@link AccessPermission}. */ 42 | permission: AccessPermission; 43 | txOptions?: TxOptions; 44 | }; 45 | /** 46 | * Permission level 47 | */ 48 | export enum AccessPermission { 49 | /** 50 | * ABSTAIN means having not enough information to make decision at 51 | * current level, deferred decision to up. 52 | */ 53 | ABSTAIN, 54 | /** ALLOW means the permission is granted to transaction signer to call the function. */ 55 | ALLOW, 56 | /** DENY means the permission is denied to transaction signer to call the function. */ 57 | DENY, 58 | } 59 | 60 | export type CreateSetPermissionSignatureRequest = { 61 | /** 62 | * The deadline for the signature in seconds. 63 | * @default 1000 64 | */ 65 | deadline?: DeadlineInput; 66 | } & SetPermissionsRequest; 67 | 68 | export type SetBatchPermissionsRequest = { 69 | permissions: Omit[]; 70 | txOptions?: TxOptions; 71 | }; 72 | 73 | export type CreateBatchPermissionSignatureRequest = { 74 | /** The ip id that grants the permission for `signer`. */ 75 | ipId: Address; 76 | /** 77 | * The deadline for the signature in seconds. 78 | * @default 1000 79 | */ 80 | deadline?: DeadlineInput; 81 | } & SetBatchPermissionsRequest; 82 | 83 | export type PermissionSignatureRequest = { 84 | ipId: Address; 85 | state: Hex; 86 | /** The deadline for the signature in seconds. */ 87 | deadline: DeadlineInput; 88 | wallet: SimpleWalletClient; 89 | chainId: ChainIds; 90 | permissions: Omit[]; 91 | }; 92 | 93 | export type SignatureRequest = { 94 | state: Hex; 95 | to: Address; 96 | encodeData: Hex; 97 | wallet: SimpleWalletClient; 98 | /** 99 | * The IP ID (address) of the IP Account that will verify the signature. 100 | * 101 | * Note: IP ID is the address of a specific IP Account's proxy contract. 102 | * Each IP Account has its own unique IP ID address. 103 | * 104 | * @see https://docs.story.foundation/docs/ip-account for more details 105 | */ 106 | verifyingContract: Address; 107 | /** The deadline for the signature in seconds. */ 108 | deadline: DeadlineInput; 109 | chainId: ChainIds; 110 | }; 111 | 112 | export type SignatureResponse = { signature: Hex; nonce: Hex }; 113 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync Labels from Issue to PR 2 | 3 | on: 4 | pull_request: 5 | types: [edited, opened, reopened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | sync-labels: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Get associated issue number 15 | id: get_issue_number 16 | run: | 17 | ISSUE_NUMBERS=$(echo "${{ github.event.pull_request.body }}" | grep -oE "#[0-9]+" | tr -d "#" | tr '\n' ',') 18 | ISSUE_NUMBERS=$(echo $ISSUE_NUMBERS | sed 's/,$//') 19 | echo "ISSUE_NUMBERS=$ISSUE_NUMBERS" >> $GITHUB_ENV 20 | echo "Issue numbers are $ISSUE_NUMBERS" 21 | 22 | - name: Get labels from issue 23 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7 24 | id: get_issue_labels 25 | with: 26 | script: | 27 | const issueNumbers = process.env.ISSUE_NUMBERS.split(","); 28 | let allLabels = new Set(); 29 | for (const issueNumber of issueNumbers) { 30 | try { 31 | const { data: { labels } } = await github.rest.issues.get({ 32 | issue_number: issueNumber, 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | }); 36 | labels.forEach(label => allLabels.add(label.name)); 37 | } catch (error) { 38 | console.error(`Error fetching labels for issue #${issueNumber}: `, error); 39 | } 40 | } 41 | if (allLabels.size === 0) { 42 | core.setOutput('labels', ''); 43 | } else { 44 | const sortedLabels = Array.from(allLabels).sort().join(","); 45 | core.setOutput('labels', sortedLabels); 46 | } 47 | 48 | - name: Check if labels are the same 49 | id: check_labels_are_same 50 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7 51 | with: 52 | script: | 53 | const issueLabels = "${{ steps.get_issue_labels.outputs.labels }}"; 54 | const prLabels = await github.rest.issues.listLabelsOnIssue({ 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | issue_number: context.issue.number, 58 | }); 59 | const prLabelsNames = prLabels.data.map(label => label.name).sort(); 60 | const isSame = issueLabels === prLabelsNames.join(","); 61 | core.setOutput('isSame', isSame.toString()); 62 | core.setOutput('labels', issueLabels); 63 | 64 | - name: Apply labels to PR 65 | if: steps.check_labels_are_same.outputs.isSame == 'false' 66 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7 67 | with: 68 | script: | 69 | const isSame = "${{ steps.check_labels_are_same.outputs.isSame }}"; 70 | const labels = "${{ steps.check_labels_are_same.outputs.labels }}" === "" ? [] : "${{ steps.check_labels_are_same.outputs.labels }}".split(","); 71 | const owner = context.repo.owner; 72 | const repo = context.repo.repo; 73 | const issue_number = context.issue.number; 74 | 75 | if (labels.length === 0) { 76 | const prLabels = await github.rest.issues.listLabelsOnIssue({ 77 | owner: owner, 78 | repo: repo, 79 | issue_number: issue_number, 80 | }); 81 | for (const label of prLabels.data) { 82 | await github.rest.issues.removeLabel({ 83 | owner: owner, 84 | repo: repo, 85 | issue_number: issue_number, 86 | name: label.name, 87 | }); 88 | } 89 | } else { 90 | await github.rest.issues.setLabels({ 91 | owner: owner, 92 | repo: repo, 93 | issue_number: issue_number, 94 | labels: labels, 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/oov3.ts: -------------------------------------------------------------------------------- 1 | import { Address, createPublicClient, createWalletClient, Hex, http, PublicClient } from "viem"; 2 | import { privateKeyToAccount } from "viem/accounts"; 3 | 4 | import { aeneid } from "./chain"; 5 | import { handleError } from "./errors"; 6 | import { chainStringToViemChain } from "./utils"; 7 | import { ArbitrationPolicyUmaClient } from "../abi/generated"; 8 | import { ASSERTION_ABI } from "../abi/oov3Abi"; 9 | import { DisputeId } from "../types/resources/dispute"; 10 | 11 | export const getOov3Contract = async ( 12 | arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient, 13 | ): Promise
=> { 14 | return await arbitrationPolicyUmaClient.oov3(); 15 | }; 16 | 17 | export const getAssertionDetails = async ( 18 | rpcClient: PublicClient, 19 | arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient, 20 | assertionId: Hex, 21 | ): Promise => { 22 | const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient); 23 | const { bond } = await rpcClient.readContract({ 24 | address: oov3Contract, 25 | abi: ASSERTION_ABI, 26 | functionName: "getAssertion", 27 | args: [assertionId], 28 | }); 29 | 30 | return bond; 31 | }; 32 | 33 | export const getMinimumBond = async ( 34 | rpcClient: PublicClient, 35 | arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient, 36 | currency: Address, 37 | ): Promise => { 38 | const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient); 39 | return await rpcClient.readContract({ 40 | address: oov3Contract, 41 | abi: ASSERTION_ABI, 42 | functionName: "getMinimumBond", 43 | args: [currency], 44 | }); 45 | }; 46 | 47 | /** 48 | * Settles an assertion associated with a dispute in the UMA arbitration protocol. 49 | * 50 | * This function takes a dispute ID, resolves it to an assertion ID, and then calls 51 | * the `settleAssertion` function on the Optimistic Oracle V3 contract to finalize 52 | * the arbitration outcome. 53 | * 54 | * The function is specifically designed for testing on the `aeneid` testnet and will 55 | * not work on other chains. It handles the entire settlement process including: 56 | * - Creating the appropriate clients with the provided private key 57 | * - Retrieving the assertion ID from the dispute ID 58 | * - Executing the settlement transaction 59 | * - Waiting for transaction confirmation 60 | * 61 | * @see https://docs.story.foundation/docs/uma-arbitration-policy#/ 62 | * @see https://docs.uma.xyz/developers/optimistic-oracle-v3 63 | * 64 | * @param privateKey - The private key of the wallet that will sign the settlement transaction. 65 | * @param disputeId - The ID of the dispute to be settled. 66 | * @param transport - Optional custom RPC URL; defaults to the aeneid testnet RPC URL. 67 | * @returns A promise that resolves to the transaction hash of the settlement transaction. 68 | */ 69 | export const settleAssertion = async ( 70 | privateKey: Hex, 71 | disputeId: DisputeId, 72 | transport?: string, 73 | ): Promise => { 74 | try { 75 | const baseConfig = { 76 | chain: chainStringToViemChain("aeneid"), 77 | transport: http(transport ?? aeneid.rpcUrls.default.http[0]), 78 | }; 79 | const rpcClient = createPublicClient(baseConfig); 80 | const walletClient = createWalletClient({ 81 | ...baseConfig, 82 | account: privateKeyToAccount(privateKey), 83 | }); 84 | const arbitrationPolicyUmaClient = new ArbitrationPolicyUmaClient(rpcClient, walletClient); 85 | const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient); 86 | const assertionId = await arbitrationPolicyUmaClient.disputeIdToAssertionId({ 87 | disputeId: BigInt(disputeId), 88 | }); 89 | const txHash = await walletClient.writeContract({ 90 | address: oov3Contract, 91 | abi: ASSERTION_ABI, 92 | functionName: "settleAssertion", 93 | args: [assertionId], 94 | }); 95 | await rpcClient.waitForTransactionReceipt({ hash: txHash }); 96 | return txHash; 97 | } catch (error) { 98 | return handleError(error, "Failed to settle assertion"); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/sign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | encodeAbiParameters, 3 | encodeFunctionData, 4 | keccak256, 5 | toFunctionSelector, 6 | WalletClient, 7 | } from "viem"; 8 | 9 | import { validateAddress } from "./utils"; 10 | import { accessControllerAbi, accessControllerAddress, ipAccountImplAbi } from "../abi/generated"; 11 | import { defaultFunctionSelector } from "../constants/common"; 12 | import { DeadlineInput } from "../types/common"; 13 | import { 14 | PermissionSignatureRequest, 15 | SignatureRequest, 16 | SignatureResponse, 17 | } from "../types/resources/permission"; 18 | 19 | /** 20 | * Get the signature for setting permissions. 21 | */ 22 | export const getPermissionSignature = async ( 23 | param: PermissionSignatureRequest, 24 | ): Promise => { 25 | const { ipId, deadline, state, wallet, chainId, permissions } = param; 26 | const accessAddress = accessControllerAddress[chainId]; 27 | const isBatchPermissionFunction = permissions.length >= 2; 28 | const data = encodeFunctionData({ 29 | abi: accessControllerAbi, 30 | functionName: isBatchPermissionFunction 31 | ? "setBatchTransientPermissions" 32 | : "setTransientPermission", 33 | args: isBatchPermissionFunction 34 | ? [ 35 | permissions.map((item) => ({ 36 | ipAccount: validateAddress(item.ipId), 37 | signer: validateAddress(item.signer), 38 | to: validateAddress(item.to), 39 | func: item.func ? toFunctionSelector(item.func) : defaultFunctionSelector, 40 | permission: item.permission, 41 | })), 42 | ] 43 | : [ 44 | validateAddress(permissions[0].ipId), 45 | validateAddress(permissions[0].signer), 46 | validateAddress(permissions[0].to), 47 | permissions[0].func ? toFunctionSelector(permissions[0].func) : defaultFunctionSelector, 48 | permissions[0].permission, 49 | ], 50 | }); 51 | return await getSignature({ 52 | state, 53 | to: accessAddress, 54 | encodeData: data, 55 | wallet, 56 | verifyingContract: ipId, 57 | deadline, 58 | chainId, 59 | }); 60 | }; 61 | 62 | export const getDeadline = (unixTimestamp: bigint, deadline?: DeadlineInput): bigint => { 63 | if (deadline && BigInt(deadline) < 0n) { 64 | throw new Error("Invalid deadline value."); 65 | } 66 | return deadline ? unixTimestamp + BigInt(deadline) : unixTimestamp + 1000n; 67 | }; 68 | 69 | /** 70 | * Get the signature. 71 | */ 72 | export const getSignature = async ({ 73 | state, 74 | to, 75 | encodeData, 76 | wallet, 77 | verifyingContract, 78 | deadline, 79 | chainId, 80 | }: SignatureRequest): Promise => { 81 | if (!(wallet as WalletClient).signTypedData) { 82 | throw new Error("The wallet client does not support signTypedData, please try again."); 83 | } 84 | 85 | if (!wallet.account) { 86 | throw new Error("The wallet client does not have an account, please try again."); 87 | } 88 | const nonce = keccak256( 89 | encodeAbiParameters( 90 | [ 91 | { name: "", type: "bytes32" }, 92 | { name: "", type: "bytes" }, 93 | ], 94 | [ 95 | state, 96 | encodeFunctionData({ 97 | abi: ipAccountImplAbi, 98 | functionName: "execute", 99 | args: [to, 0n, encodeData], 100 | }), 101 | ], 102 | ), 103 | ); 104 | const signature = await (wallet as WalletClient).signTypedData({ 105 | account: wallet.account, 106 | domain: { 107 | name: "Story Protocol IP Account", 108 | version: "1", 109 | chainId, 110 | verifyingContract, 111 | }, 112 | types: { 113 | Execute: [ 114 | { name: "to", type: "address" }, 115 | { name: "value", type: "uint256" }, 116 | { name: "data", type: "bytes" }, 117 | { name: "nonce", type: "bytes32" }, 118 | { name: "deadline", type: "uint256" }, 119 | ], 120 | }, 121 | primaryType: "Execute", 122 | message: { 123 | to, 124 | value: BigInt(0), 125 | data: encodeData, 126 | nonce, 127 | deadline: BigInt(deadline), 128 | }, 129 | }); 130 | return { signature, nonce }; 131 | }; 132 | -------------------------------------------------------------------------------- /packages/core-sdk/src/abi/oov3Abi.ts: -------------------------------------------------------------------------------- 1 | import { Abi } from "viem"; 2 | /** 3 | * The ABI for the OptimisticOracleV3 contract. Contract address may be changed. 4 | * @see https://aeneid.storyscan.io/address/0xABac6a158431edED06EE6cba37eDE8779F599eE4?tab=contract_abi 5 | */ 6 | export const ASSERTION_ABI = [ 7 | { 8 | inputs: [ 9 | { 10 | internalType: "bytes32", 11 | name: "assertionId", 12 | type: "bytes32", 13 | }, 14 | ], 15 | name: "getAssertion", 16 | outputs: [ 17 | { 18 | components: [ 19 | { 20 | components: [ 21 | { 22 | internalType: "bool", 23 | name: "arbitrateViaEscalationManager", 24 | type: "bool", 25 | }, 26 | { 27 | internalType: "bool", 28 | name: "discardOracle", 29 | type: "bool", 30 | }, 31 | { 32 | internalType: "bool", 33 | name: "validateDisputers", 34 | type: "bool", 35 | }, 36 | { 37 | internalType: "address", 38 | name: "assertingCaller", 39 | type: "address", 40 | }, 41 | { 42 | internalType: "address", 43 | name: "escalationManager", 44 | type: "address", 45 | }, 46 | ], 47 | internalType: "struct OptimisticOracleV3Interface.EscalationManagerSettings", 48 | name: "escalationManagerSettings", 49 | type: "tuple", 50 | }, 51 | { 52 | internalType: "address", 53 | name: "asserter", 54 | type: "address", 55 | }, 56 | { 57 | internalType: "uint64", 58 | name: "assertionTime", 59 | type: "uint64", 60 | }, 61 | { 62 | internalType: "bool", 63 | name: "settled", 64 | type: "bool", 65 | }, 66 | { 67 | internalType: "contract IERC20", 68 | name: "currency", 69 | type: "address", 70 | }, 71 | { 72 | internalType: "uint64", 73 | name: "expirationTime", 74 | type: "uint64", 75 | }, 76 | { 77 | internalType: "bool", 78 | name: "settlementResolution", 79 | type: "bool", 80 | }, 81 | { 82 | internalType: "bytes32", 83 | name: "domainId", 84 | type: "bytes32", 85 | }, 86 | { 87 | internalType: "bytes32", 88 | name: "identifier", 89 | type: "bytes32", 90 | }, 91 | { 92 | internalType: "uint256", 93 | name: "bond", 94 | type: "uint256", 95 | }, 96 | { 97 | internalType: "address", 98 | name: "callbackRecipient", 99 | type: "address", 100 | }, 101 | { 102 | internalType: "address", 103 | name: "disputer", 104 | type: "address", 105 | }, 106 | ], 107 | internalType: "struct OptimisticOracleV3Interface.Assertion", 108 | name: "", 109 | type: "tuple", 110 | }, 111 | ], 112 | stateMutability: "view", 113 | type: "function", 114 | }, 115 | { 116 | inputs: [ 117 | { 118 | internalType: "bytes32", 119 | name: "assertionId", 120 | type: "bytes32", 121 | }, 122 | ], 123 | name: "settleAssertion", 124 | outputs: [], 125 | stateMutability: "nonpayable", 126 | type: "function", 127 | }, 128 | { 129 | inputs: [ 130 | { 131 | internalType: "address", 132 | name: "currency", 133 | type: "address", 134 | }, 135 | ], 136 | name: "getMinimumBond", 137 | outputs: [ 138 | { 139 | internalType: "uint256", 140 | name: "", 141 | type: "uint256", 142 | }, 143 | ], 144 | stateMutability: "view", 145 | type: "function", 146 | }, 147 | ] as const satisfies Abi; 148 | -------------------------------------------------------------------------------- /packages/core-sdk/test/integration/utils/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | createPublicClient, 4 | createWalletClient, 5 | Hex, 6 | http, 7 | WalletClient, 8 | zeroHash, 9 | } from "viem"; 10 | import { privateKeyToAccount } from "viem/accounts"; 11 | 12 | import { ChainIds, StoryClient, StoryConfig } from "../../../src"; 13 | import { 14 | licenseTokenAddress, 15 | spgnftBeaconAddress, 16 | SpgnftImplEventClient, 17 | } from "../../../src/abi/generated"; 18 | import { chainStringToViemChain } from "../../../src/utils/utils"; 19 | 20 | export const RPC = "https://aeneid.storyrpc.io"; 21 | export const aeneid: ChainIds = 1315; 22 | export const mockERC721 = "0xa1119092ea911202E0a65B743a13AE28C5CF2f21"; 23 | export const licenseToken = licenseTokenAddress[aeneid]; 24 | export const spgNftBeacon = spgnftBeaconAddress[aeneid]; 25 | export const TEST_WALLET_ADDRESS = process.env.TEST_WALLET_ADDRESS! as Address; 26 | export const TEST_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY! as Hex; 27 | 28 | const baseConfig = { 29 | chain: chainStringToViemChain("aeneid"), 30 | transport: http(RPC), 31 | } as const; 32 | export const publicClient = createPublicClient(baseConfig); 33 | export const walletClient: WalletClient = createWalletClient({ 34 | ...baseConfig, 35 | account: privateKeyToAccount(TEST_PRIVATE_KEY), 36 | }); 37 | 38 | export const getTokenId = async (): Promise => { 39 | const { request } = await publicClient.simulateContract({ 40 | abi: [ 41 | { 42 | inputs: [{ internalType: "address", name: "to", type: "address" }], 43 | name: "mint", 44 | outputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], 45 | stateMutability: "nonpayable", 46 | type: "function", 47 | }, 48 | ], 49 | address: mockERC721, 50 | functionName: "mint", 51 | args: [process.env.TEST_WALLET_ADDRESS as Hex], 52 | account: walletClient.account, 53 | }); 54 | const hash = await walletClient.writeContract(request); 55 | const { logs } = await publicClient.waitForTransactionReceipt({ 56 | hash, 57 | }); 58 | if (logs[0].topics[3]) { 59 | return parseInt(logs[0].topics[3], 16); 60 | } 61 | }; 62 | 63 | export const mintBySpg = async ( 64 | spgNftContract: Hex, 65 | nftMetadataURI?: string, 66 | nftMetadataHash?: Hex, 67 | ): Promise => { 68 | const { request } = await publicClient.simulateContract({ 69 | abi: [ 70 | { 71 | inputs: [ 72 | { 73 | internalType: "address", 74 | name: "to", 75 | type: "address", 76 | }, 77 | { 78 | internalType: "string", 79 | name: "nftMetadataURI", 80 | type: "string", 81 | }, 82 | { 83 | internalType: "bytes32", 84 | name: "nftMetadataHash", 85 | type: "bytes32", 86 | }, 87 | { 88 | internalType: "bool", 89 | name: "allowDuplicates", 90 | type: "bool", 91 | }, 92 | ], 93 | name: "mint", 94 | outputs: [ 95 | { 96 | internalType: "uint256", 97 | name: "tokenId", 98 | type: "uint256", 99 | }, 100 | ], 101 | stateMutability: "nonpayable", 102 | type: "function", 103 | }, 104 | ], 105 | address: spgNftContract, 106 | functionName: "mint", 107 | args: [TEST_WALLET_ADDRESS, nftMetadataURI || "", nftMetadataHash || zeroHash, true], 108 | account: walletClient.account, 109 | }); 110 | 111 | const hash = await walletClient.writeContract(request); 112 | const receipt = await publicClient.waitForTransactionReceipt({ 113 | hash, 114 | }); 115 | const spgnftImplEventClient = new SpgnftImplEventClient(publicClient); 116 | const events = spgnftImplEventClient.parseTxTransferEvent(receipt); 117 | return events[0].tokenId; 118 | }; 119 | 120 | export const getStoryClient = (privateKey?: Address): StoryClient => { 121 | const config: StoryConfig = { 122 | chainId: "aeneid", 123 | transport: http(RPC), 124 | account: privateKeyToAccount(privateKey ?? (process.env.WALLET_PRIVATE_KEY as Address)), 125 | }; 126 | 127 | return StoryClient.newClient(config); 128 | }; 129 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/common.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hex } from "viem"; 2 | 3 | import { WithTxOptions } from "./options"; 4 | import { IpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow"; 5 | 6 | export type TypedData = { 7 | interface: string; 8 | data: unknown[]; 9 | }; 10 | 11 | export type IpMetadataAndTxOptions = WithTxOptions & { 12 | /** The desired metadata for the newly minted NFT and newly registered IP. */ 13 | ipMetadata?: Partial; 14 | }; 15 | 16 | /** 17 | * This data used IP owners to define the configuration 18 | * when others are minting license tokens of their IP through the LicensingModule. 19 | * Contract reference: @see {@link https://github.com/storyprotocol/protocol-core-v1/blob/v1.3.1/contracts/lib/Licensing.sol#L27 | Licensing.sol} 20 | * For detailed documentation on licensing configuration, visit {@link https://docs.story.foundation/concepts/licensing-module/license-config} 21 | */ 22 | export type LicensingConfigInput = { 23 | /** Whether the licensing configuration is active. If false, the configuration is ignored. */ 24 | isSet: boolean; 25 | /** The minting fee to be paid when minting license tokens. */ 26 | mintingFee: FeeInput; 27 | /** 28 | * The licensingHook is an address to a smart contract that implements the `ILicensingHook` interface. 29 | * This contract's `beforeMintLicenseTokens` function is executed before a user mints a License Token, 30 | * allowing for custom validation or business logic to be enforced during the minting process. 31 | * For detailed documentation on licensing hook, visit {@link https://docs.story.foundation/concepts/hooks#licensing-hooks} 32 | */ 33 | licensingHook: Address; 34 | /** 35 | * The data to be used by the licensing hook. 36 | * Set to a zero hash if no data is provided. 37 | */ 38 | hookData: Hex; 39 | /** 40 | * Percentage of revenue that must be shared with the licensor. 41 | * Must be between 0 and 100 (where 100% represents 100_000_000). 42 | */ 43 | commercialRevShare: RevShareInput; 44 | /** Whether the licensing is disabled or not. If this is true, then no licenses can be minted and no more derivatives can be attached at all. */ 45 | disabled: boolean; 46 | /** 47 | * The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100_000_000) that can be allocated to the IP when it is added to the group. 48 | * Must be between 0 and 100 (where 100% represents 100_000_000). 49 | */ 50 | expectMinimumGroupRewardShare: RevShareInput; 51 | /** The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or zero address if the IP does not want to be added to any group. */ 52 | expectGroupRewardPool: Address; 53 | }; 54 | 55 | export type LicensingConfig = LicensingConfigInput & { 56 | mintingFee: bigint; 57 | commercialRevShare: RevShareInput; 58 | expectMinimumGroupRewardShare: number; 59 | }; 60 | 61 | /** 62 | * Input for token amount, can be bigint or number. 63 | * Will be converted to bigint for contract calls. 64 | */ 65 | export type TokenAmountInput = bigint | number; 66 | 67 | /** 68 | * Input for token id, can be bigint or number. 69 | * Will be converted to bigint for contract calls. 70 | */ 71 | export type TokenIdInput = bigint | number; 72 | /** 73 | * The type of revenue share. 74 | * It is used to determine the type of revenue share to be used in the revenue share calculation and throw error when the revenue share is not valid. 75 | */ 76 | export enum RevShareType { 77 | COMMERCIAL_REVENUE_SHARE = "commercialRevShare", 78 | MAX_REVENUE_SHARE = "maxRevenueShare", 79 | MAX_ALLOWED_REWARD_SHARE = "maxAllowedRewardShare", 80 | EXPECT_MINIMUM_GROUP_REWARD_SHARE = "expectMinimumGroupRewardShare", 81 | MAX_ALLOWED_REWARD_SHARE_PERCENTAGE = "maxAllowedRewardSharePercentage", 82 | } 83 | 84 | /** 85 | * Input for license terms id, can be bigint or number. 86 | * Will be converted to bigint for contract calls. 87 | */ 88 | export type LicenseTermsIdInput = number | bigint; 89 | 90 | /** 91 | * Input for deadline, can be bigint or number. 92 | * Will be converted to bigint for contract calls. 93 | */ 94 | export type DeadlineInput = number | bigint; 95 | 96 | /** 97 | * Input for revenue share, can be number. 98 | */ 99 | export type RevShareInput = number; 100 | 101 | /** 102 | * Input for fee, can be bigint or number. 103 | * Will be converted to bigint for contract calls. 104 | */ 105 | export type FeeInput = bigint | number; 106 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/sign.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { createWalletClient, Hex, http, WalletClient, zeroAddress } from "viem"; 3 | import { privateKeyToAccount } from "viem/accounts"; 4 | 5 | import { getDeadline, getPermissionSignature } from "../../../src/utils/sign"; 6 | import { chainStringToViemChain } from "../../../src/utils/utils"; 7 | import { aeneid } from "../../integration/utils/util"; 8 | 9 | describe("Sign", () => { 10 | describe("Get Permission Signature", () => { 11 | it("should throw sign error when call getPermissionSignature given wallet does not support signTypedData", async () => { 12 | try { 13 | await getPermissionSignature({ 14 | ipId: zeroAddress, 15 | state: "0x2e778894d11b5308e4153f094e190496c1e0609652c19f8b87e5176484b9a56e", 16 | deadline: 1000n, 17 | permissions: [{ ipId: zeroAddress, signer: zeroAddress, to: zeroAddress, permission: 0 }], 18 | wallet: {} as WalletClient, 19 | chainId: aeneid, 20 | }); 21 | } catch (e) { 22 | expect((e as Error).message).to.equal( 23 | "The wallet client does not support signTypedData, please try again.", 24 | ); 25 | } 26 | }); 27 | 28 | it("should throw sign error when call getPermissionSignature given wallet does not have an account", async () => { 29 | try { 30 | await getPermissionSignature({ 31 | ipId: zeroAddress, 32 | state: "0x2e778894d11b5308e4153f094e190496c1e0609652c19f8b87e5176484b9a56e", 33 | deadline: 1000n, 34 | permissions: [{ ipId: zeroAddress, signer: zeroAddress, to: zeroAddress, permission: 0 }], 35 | wallet: { signTypedData: () => Promise.resolve("") } as unknown as WalletClient, 36 | chainId: aeneid, 37 | }); 38 | } catch (e) { 39 | expect((e as Error).message).to.equal( 40 | "The wallet client does not have an account, please try again.", 41 | ); 42 | } 43 | }); 44 | 45 | it("should return signature when call getPermissionSignature given account support signTypedData", async () => { 46 | const walletClient = createWalletClient({ 47 | chain: chainStringToViemChain("aeneid"), 48 | transport: http(), 49 | account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), 50 | }); 51 | const result = await getPermissionSignature({ 52 | ipId: zeroAddress, 53 | state: "0x2e778894d11b5308e4153f094e190496c1e0609652c19f8b87e5176484b9a56e", 54 | deadline: 1000n, 55 | permissions: [ 56 | { 57 | ipId: zeroAddress, 58 | signer: zeroAddress, 59 | to: zeroAddress, 60 | permission: 0, 61 | func: "function setAll(address,string,bytes32,bytes32)", 62 | }, 63 | ], 64 | wallet: walletClient, 65 | chainId: aeneid, 66 | }); 67 | expect(result.signature).to.be.a("string"); 68 | expect(result.nonce).to.be.a("string"); 69 | }); 70 | 71 | it("should return signature when call getPermissionSignature given account support signTypedData and multiple permissions", async () => { 72 | const walletClient = createWalletClient({ 73 | chain: chainStringToViemChain("aeneid"), 74 | transport: http(), 75 | account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex), 76 | }); 77 | const result = await getPermissionSignature({ 78 | ipId: zeroAddress, 79 | state: "0x2e778894d11b5308e4153f094e190496c1e0609652c19f8b87e5176484b9a56e", 80 | deadline: 1000n, 81 | permissions: [ 82 | { ipId: zeroAddress, signer: zeroAddress, to: zeroAddress, permission: 0 }, 83 | { 84 | ipId: zeroAddress, 85 | signer: zeroAddress, 86 | to: zeroAddress, 87 | permission: 0, 88 | func: "function setAll(address,string,bytes32,bytes32)", 89 | }, 90 | ], 91 | wallet: walletClient, 92 | chainId: aeneid, 93 | }); 94 | expect(result.signature).to.be.a("string"); 95 | expect(result.nonce).to.be.a("string"); 96 | }); 97 | }); 98 | describe("Get Deadline", () => { 99 | it("should throw invalid deadline value when call getDeadline given deadline is less than 0", () => { 100 | try { 101 | getDeadline(12n, -1); 102 | } catch (e) { 103 | expect((e as Error).message).to.equal("Invalid deadline value."); 104 | } 105 | }); 106 | 107 | it("should return 2000 when call getDeadline", () => { 108 | const result = getDeadline(12n); 109 | expect(result).to.equal(1012n); 110 | }); 111 | 112 | it("should return timestamp plus deadline when call getDeadline given deadline", () => { 113 | const result = getDeadline(12n, 3000); 114 | expect(result).to.equal(3012n); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/core-sdk/src/utils/registrationUtils/registerHelper.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, Hex } from "viem"; 2 | 3 | import { TransactionResponse } from "../../types/options"; 4 | import { TransformedIpRegistrationWorkflowRequest } from "../../types/resources/ipAsset"; 5 | import { 6 | AggregateRegistrationRequest, 7 | MulticallConfigRequest, 8 | MulticallConfigResponse, 9 | } from "../../types/utils/registerHelper"; 10 | import { contractCallWithFees } from "../feeUtils"; 11 | import { mergeSpenders } from "./registerValidation"; 12 | /** 13 | * Aggregates the registration requests for the given workflow responses. 14 | * 15 | * This function combines multiple workflow responses into a consolidated request structure, 16 | * aggregating: 17 | * - Token spenders and their allowances 18 | * - Total fees required for all operations 19 | * - Encoded transaction data 20 | * - Contract calls to be executed 21 | * 22 | * @remarks 23 | * The function handles two execution modes: 24 | * 1. If `disableMulticallWhenPossible` is true or a workflow response supports multicall3 25 | * (indicated by `isUseMulticall3`), individual contract calls are added to the `contractCall` array. 26 | * 2. Otherwise, it concatenates all `encodedTxData` and passes them as parameters to the 27 | * workflowClient's `multicall` method, optimizing gas usage by batching transactions. 28 | * 29 | * This approach allows for flexible transaction handling based on contract capabilities 30 | * and user preferences. 31 | */ 32 | const aggregateTransformIpRegistrationWorkflow = ( 33 | transferWorkflowRequests: TransformedIpRegistrationWorkflowRequest[], 34 | multicall3Address: Address, 35 | disableMulticallWhenPossible: boolean, 36 | ): AggregateRegistrationRequest => { 37 | const aggregateRegistrationRequest: AggregateRegistrationRequest = {}; 38 | for (const res of transferWorkflowRequests) { 39 | const { spenders, encodedTxData, workflowClient, isUseMulticall3, extraData } = res; 40 | let shouldUseMulticall = isUseMulticall3; 41 | if (disableMulticallWhenPossible) { 42 | shouldUseMulticall = false; 43 | } 44 | 45 | const targetAddress = shouldUseMulticall ? multicall3Address : workflowClient.address; 46 | if (!aggregateRegistrationRequest[targetAddress]) { 47 | aggregateRegistrationRequest[targetAddress] = { 48 | spenders: [], 49 | encodedTxData: [], 50 | contractCall: [], 51 | extraData: [], 52 | }; 53 | } 54 | 55 | const currentRequest = aggregateRegistrationRequest[targetAddress]; 56 | currentRequest.spenders = mergeSpenders(currentRequest.spenders, spenders || []); 57 | currentRequest.encodedTxData = currentRequest.encodedTxData.concat(encodedTxData); 58 | currentRequest.extraData = currentRequest.extraData?.concat(extraData || undefined); 59 | if (isUseMulticall3 || disableMulticallWhenPossible) { 60 | currentRequest.contractCall = currentRequest.contractCall.concat(res.contractCall); 61 | } else { 62 | currentRequest.contractCall = [ 63 | (): Promise => { 64 | return workflowClient.multicall({ 65 | data: currentRequest.encodedTxData.map((tx) => tx.data), 66 | }); 67 | }, 68 | ]; 69 | } 70 | } 71 | 72 | return aggregateRegistrationRequest; 73 | }; 74 | 75 | export const handleMulticall = async ({ 76 | transferWorkflowRequests, 77 | multicall3Address, 78 | options, 79 | rpcClient, 80 | wallet, 81 | walletAddress, 82 | }: MulticallConfigRequest): Promise => { 83 | const aggregateRegistrationRequest = aggregateTransformIpRegistrationWorkflow( 84 | transferWorkflowRequests, 85 | multicall3Address, 86 | options?.wipOptions?.useMulticallWhenPossible === false, 87 | ); 88 | const txResponses: TransactionResponse[] = []; 89 | for (const key in aggregateRegistrationRequest) { 90 | const { spenders, encodedTxData, contractCall } = aggregateRegistrationRequest[key]; 91 | const contractCalls = async (): Promise => { 92 | const txHashes: Hex[] = []; 93 | for (const call of contractCall) { 94 | const txHash = await call(); 95 | txHashes.push(txHash); 96 | } 97 | return txHashes; 98 | }; 99 | const useMulticallWhenPossible = key === multicall3Address ? true : false; 100 | const txResponse = await contractCallWithFees({ 101 | options: { 102 | ...options, 103 | wipOptions: { 104 | ...options?.wipOptions, 105 | useMulticallWhenPossible, 106 | }, 107 | }, 108 | multicall3Address, 109 | rpcClient, 110 | tokenSpenders: spenders, 111 | contractCall: contractCalls, 112 | sender: walletAddress, 113 | wallet, 114 | encodedTxs: encodedTxData, 115 | }); 116 | txResponses.push(...(Array.isArray(txResponse) ? txResponse : [txResponse])); 117 | } 118 | return { 119 | response: txResponses, 120 | aggregateRegistrationRequest, 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Code of Conduct 4 | 5 | Please make sure to read and observe our [Code of Conduct](/CODE_OF_CONDUCT.md). 6 | 7 | ## Engineering Guideline 8 | 9 | [Typescript coding guidelines][3] 10 | 11 | [Good commit messages][2] 12 | 13 | ## Open Development 14 | 15 | ### How To Build and Test Story Protocol SDK for local testing 16 | 17 | This section provides the instructions on how to build Story Protocol SDK from source code. 18 | 19 | #### Prerequisite 20 | 21 | - Install PNPM: Execute `npm install -g pnpm` 22 | - Install TypeScript: Run `pnpm add typescript -D` 23 | - Install Yalc: Use `npm install -g yalc` 24 | 25 | #### Steps for Using Yalc for Local Testing of Core-SDK 26 | 27 | For manual testing of the core-sdk, set up a separate web project. The guide below uses `yalc` to link the `core-sdk` locally, enabling its installation and import for testing. 28 | 29 | Under the `typescript-sdk/packages/core-sdk` directory: 30 | 31 | - Navigate to the `core-sdk` directory. 32 | - Execute `npm run build` to build your latest code. 33 | - Run `yalc publish`. You should see a message like `@story-protocol/core-sdk@ published in store.` (Note: The version number may vary). 34 | 35 | To set up your testing environment (e.g., a new Next.js project), use `yalc add @story-protocol/core-sdk@` (ensure the version number is updated accordingly). 36 | 37 | - Run `pnpm install`. This installs `@story-protocol/core-sdk@` with your local changes. 38 | 39 | #### Steps to Refresh the Changes 40 | 41 | Under the `typescript-sdk/packages/core-sdk` directory: 42 | 43 | - Execute `npm run build` to build your latest code. 44 | - Run `yalc push`. 45 | 46 | In your testing environment: 47 | 48 | - Run `yalc update` to pull the latest changes. 49 | 50 | ### How to update the latest Protocol Core & Periphery Smart Contract methods 51 | 52 | 1. Install the dependencies: `cd packages/wagmi-generator && pnpm install` 53 | 2. Update the `wagmi.config.ts` file with the latest contract addresses and chain IDs. 54 | 3. Run the generator: `cd packages/wagmi-generator && pnpm run generate` 55 | 56 | It will generate the latest contract methods and events in the `packages/core-sdk/src/abi/generated` directory. 57 | 58 | ## Bug Reports 59 | 60 | - Ensure your issue [has not already been reported][1]. It may already be fixed! 61 | - Include the steps you carried out to produce the problem. 62 | - Include the behavior you observed along with the behavior you expected, and 63 | why you expected it. 64 | - Include any relevant stack traces or debugging output. 65 | 66 | ## Feature Requests 67 | 68 | We welcome feedback with or without pull requests. If you have an idea for how 69 | to improve the project, great! All we ask is that you take the time to write a 70 | clear and concise explanation of what need you are trying to solve. If you have 71 | thoughts on _how_ it can be solved, include those too! 72 | 73 | The best way to see a feature added, however, is to submit a pull request. 74 | 75 | ## Pull Requests 76 | 77 | - Before creating your pull request, it's usually worth asking if the code 78 | you're planning on writing will actually be considered for merging. You can 79 | do this by [opening an issue][1] and asking. It may also help give the 80 | maintainers context for when the time comes to review your code. 81 | 82 | - Ensure your [commit messages are well-written][2]. This can double as your 83 | pull request message, so it pays to take the time to write a clear message. 84 | 85 | - Add tests for your feature. You should be able to look at other tests for 86 | examples. If you're unsure, don't hesitate to [open an issue][1] and ask! 87 | 88 | - Submit your pull request! 89 | - Fork the repository on GitHub. 90 | - Make your changes on your fork repository. 91 | - Submit a PR. 92 | 93 | ## Find something to work on 94 | 95 | To help you get started contributing, we maintain a list of `good first issues` that contain bugs and features with relatively limited scope. These issues are specifically curated to help new contributors understand our codebase and development process. 96 | 97 | We welcome contributions of all kinds, including: 98 | 99 | - Documentation improvements and fixes 100 | - Bug reports and fixes 101 | - New features and enhancements 102 | - Code refactoring and test coverage 103 | - Best practices and code quality improvements 104 | 105 | Feel free to browse our issues list and find something that interests you. If you're unsure where to start, look for issues tagged with `good first issue` or `help wanted`. 106 | 107 | If you have questions about the development process, 108 | feel free to [file an issue](https://github.com/storyprotocol/sdk/issues/new). 109 | 110 | ## Code Review 111 | 112 | To make it easier for your PR to receive reviews, consider the reviewers will need you to: 113 | 114 | - follow [good coding guidelines][3]. 115 | - write [good commit messages][2]. 116 | - break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue. 117 | 118 | [1]: https://github.com/storyprotocol/sdk/issues 119 | [2]: https://chris.beams.io/posts/git-commit/#seven-rules 120 | [3]: https://google.github.io/styleguide/tsguide.html 121 | -------------------------------------------------------------------------------- /packages/core-sdk/test/integration/permission.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import { Address } from "viem"; 4 | 5 | import { StoryClient } from "../../src"; 6 | import { aeneid, getStoryClient, getTokenId, mockERC721, TEST_WALLET_ADDRESS } from "./utils/util"; 7 | import { coreMetadataModuleAddress } from "../../src/abi/generated"; 8 | import { AccessPermission } from "../../src/types/resources/permission"; 9 | 10 | use(chaiAsPromised); 11 | 12 | describe("Permission Functions", () => { 13 | let client: StoryClient; 14 | let ipId: Address; 15 | const coreMetadataModule = coreMetadataModuleAddress[aeneid]; 16 | 17 | before(async () => { 18 | client = getStoryClient(); 19 | const tokenId = await getTokenId(); 20 | const response = await client.ipAsset.register({ 21 | nftContract: mockERC721, 22 | tokenId: tokenId!, 23 | }); 24 | ipId = response.ipId!; 25 | }); 26 | 27 | describe("Single Permission Operations", () => { 28 | it("should set permission successfully", async () => { 29 | const response = await client.permission.setPermission({ 30 | ipId: ipId, 31 | signer: TEST_WALLET_ADDRESS, 32 | to: coreMetadataModule, 33 | permission: AccessPermission.ALLOW, 34 | func: "function setAll(address,string,bytes32,bytes32)", 35 | }); 36 | 37 | expect(response.txHash).to.be.a("string"); 38 | expect(response.success).to.equal(true); 39 | }); 40 | 41 | it("should set all permissions successfully", async () => { 42 | const response = await client.permission.setAllPermissions({ 43 | ipId: ipId, 44 | signer: TEST_WALLET_ADDRESS, 45 | permission: AccessPermission.ALLOW, 46 | }); 47 | 48 | expect(response.txHash).to.be.a("string"); 49 | expect(response.success).to.equal(true); 50 | }); 51 | }); 52 | 53 | describe("Permission Signatures", () => { 54 | it("should create set permission signature", async () => { 55 | const response = await client.permission.createSetPermissionSignature({ 56 | ipId: ipId, 57 | signer: TEST_WALLET_ADDRESS, 58 | to: coreMetadataModule, 59 | func: "function setAll(address,string,bytes32,bytes32)", 60 | permission: AccessPermission.ALLOW, 61 | deadline: 60000n, 62 | }); 63 | 64 | expect(response.txHash).to.be.a("string"); 65 | expect(response.success).to.equal(true); 66 | }); 67 | }); 68 | 69 | describe("Batch Operations", () => { 70 | it("should set batch permissions successfully", async () => { 71 | const response = await client.permission.setBatchPermissions({ 72 | permissions: [ 73 | { 74 | ipId: ipId, 75 | signer: TEST_WALLET_ADDRESS, 76 | to: coreMetadataModule, 77 | permission: AccessPermission.ALLOW, 78 | func: "function setAll(address,string,bytes32,bytes32)", 79 | }, 80 | { 81 | ipId: ipId, 82 | signer: TEST_WALLET_ADDRESS, 83 | to: coreMetadataModule, 84 | permission: AccessPermission.DENY, 85 | func: "function freezeMetadata(address)", 86 | }, 87 | ], 88 | }); 89 | 90 | expect(response.txHash).to.be.a("string"); 91 | expect(response.success).to.equal(true); 92 | }); 93 | 94 | it("should create batch permission signature", async () => { 95 | const response = await client.permission.createBatchPermissionSignature({ 96 | ipId: ipId, 97 | permissions: [ 98 | { 99 | ipId: ipId, 100 | signer: TEST_WALLET_ADDRESS, 101 | to: coreMetadataModule, 102 | permission: AccessPermission.ALLOW, 103 | func: "function setAll(address,string,bytes32,bytes32)", 104 | }, 105 | { 106 | ipId: ipId, 107 | signer: TEST_WALLET_ADDRESS, 108 | to: coreMetadataModule, 109 | permission: AccessPermission.DENY, 110 | func: "function freezeMetadata(address)", 111 | }, 112 | ], 113 | deadline: 60000n, 114 | }); 115 | 116 | expect(response.txHash).to.be.a("string"); 117 | expect(response.success).to.equal(true); 118 | }); 119 | }); 120 | 121 | describe("Error Cases", () => { 122 | it("should fail when setting permission for unregistered IP", async () => { 123 | const unregisteredIpId = "0x1234567890123456789012345678901234567890"; 124 | await expect( 125 | client.permission.setPermission({ 126 | ipId: unregisteredIpId as Address, 127 | signer: TEST_WALLET_ADDRESS, 128 | to: coreMetadataModule, 129 | permission: AccessPermission.ALLOW, 130 | }), 131 | ).to.be.rejectedWith(`IP id with ${unregisteredIpId} is not registered.`); 132 | }); 133 | 134 | it("should fail with invalid function signature", async () => { 135 | await expect( 136 | client.permission.setPermission({ 137 | ipId: ipId, 138 | signer: TEST_WALLET_ADDRESS, 139 | to: coreMetadataModule, 140 | permission: AccessPermission.ALLOW, 141 | func: "invalid_function_signature", 142 | }), 143 | ).to.be.rejected; 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /packages/core-sdk/src/resources/wip.ts: -------------------------------------------------------------------------------- 1 | import { Address, PublicClient, WriteContractParameters } from "viem"; 2 | 3 | import { SimpleWalletClient, wrappedIpAbi, WrappedIpClient } from "../abi/generated"; 4 | import { WIP_TOKEN_ADDRESS } from "../constants/common"; 5 | import { TransactionResponse } from "../types/options"; 6 | import { 7 | ApproveRequest, 8 | DepositRequest, 9 | TransferFromRequest, 10 | TransferRequest, 11 | WithdrawRequest, 12 | } from "../types/resources/wip"; 13 | import { handleError } from "../utils/errors"; 14 | import { waitForTxReceipt } from "../utils/txOptions"; 15 | import { validateAddress } from "../utils/utils"; 16 | 17 | export class WipClient { 18 | public wrappedIpClient: WrappedIpClient; 19 | private readonly rpcClient: PublicClient; 20 | private readonly wallet: SimpleWalletClient; 21 | 22 | constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { 23 | this.wrappedIpClient = new WrappedIpClient(rpcClient, wallet, WIP_TOKEN_ADDRESS); 24 | this.rpcClient = rpcClient; 25 | this.wallet = wallet; 26 | } 27 | 28 | /** 29 | * Wraps the selected amount of IP to WIP. 30 | * The WIP will be deposited to the wallet that transferred the IP. 31 | */ 32 | public async deposit({ amount, txOptions }: DepositRequest): Promise { 33 | try { 34 | if (amount <= 0) { 35 | throw new Error("WIP deposit amount must be greater than 0."); 36 | } 37 | const { request: call } = await this.rpcClient.simulateContract({ 38 | abi: wrappedIpAbi, 39 | address: WIP_TOKEN_ADDRESS, 40 | functionName: "deposit", 41 | account: this.wallet.account, 42 | value: BigInt(amount), 43 | }); 44 | const txHash = await this.wallet.writeContract(call as WriteContractParameters); 45 | return waitForTxReceipt({ 46 | txHash, 47 | txOptions, 48 | rpcClient: this.rpcClient, 49 | }); 50 | } catch (error) { 51 | return handleError(error, "Failed to deposit IP for WIP"); 52 | } 53 | } 54 | 55 | /** 56 | * Unwraps the selected amount of WIP to IP. 57 | */ 58 | public async withdraw({ amount, txOptions }: WithdrawRequest): Promise { 59 | try { 60 | const targetAmt = BigInt(amount); 61 | if (targetAmt <= 0) { 62 | throw new Error("WIP withdraw amount must be greater than 0."); 63 | } 64 | const txHash = await this.wrappedIpClient.withdraw({ value: targetAmt }); 65 | return waitForTxReceipt({ 66 | txHash, 67 | txOptions, 68 | rpcClient: this.rpcClient, 69 | }); 70 | } catch (error) { 71 | return handleError(error, "Failed to withdraw WIP"); 72 | } 73 | } 74 | 75 | /** 76 | * Approve a spender to use the wallet's WIP balance. 77 | */ 78 | public async approve(req: ApproveRequest): Promise { 79 | try { 80 | const amount = BigInt(req.amount); 81 | if (amount <= 0) { 82 | throw new Error("WIP approve amount must be greater than 0."); 83 | } 84 | const spender = validateAddress(req.spender); 85 | const txHash = await this.wrappedIpClient.approve({ 86 | spender, 87 | amount, 88 | }); 89 | return waitForTxReceipt({ 90 | txHash, 91 | txOptions: req.txOptions, 92 | rpcClient: this.rpcClient, 93 | }); 94 | } catch (error) { 95 | return handleError(error, "Failed to approve WIP"); 96 | } 97 | } 98 | 99 | /** 100 | * Returns the balance of WIP for an address. 101 | */ 102 | public async balanceOf(addr: Address): Promise { 103 | const owner = validateAddress(addr); 104 | const ret = await this.wrappedIpClient.balanceOf({ owner }); 105 | return ret.result; 106 | } 107 | 108 | /** 109 | * Transfers `amount` of WIP to a recipient `to`. 110 | */ 111 | public async transfer(request: TransferRequest): Promise { 112 | try { 113 | const amount = BigInt(request.amount); 114 | if (amount <= 0) { 115 | throw new Error("WIP transfer amount must be greater than 0."); 116 | } 117 | const txHash = await this.wrappedIpClient.transfer({ 118 | to: validateAddress(request.to), 119 | amount, 120 | }); 121 | return waitForTxReceipt({ 122 | txHash, 123 | txOptions: request.txOptions, 124 | rpcClient: this.rpcClient, 125 | }); 126 | } catch (error) { 127 | return handleError(error, "Failed to transfer WIP"); 128 | } 129 | } 130 | 131 | /** 132 | * Transfers `amount` of WIP from `from` to a recipient `to`. 133 | */ 134 | public async transferFrom(request: TransferFromRequest): Promise { 135 | try { 136 | const amount = BigInt(request.amount); 137 | if (amount <= 0) { 138 | throw new Error("WIP transfer amount must be greater than 0."); 139 | } 140 | const txHash = await this.wrappedIpClient.transferFrom({ 141 | to: validateAddress(request.to), 142 | amount, 143 | from: validateAddress(request.from), 144 | }); 145 | return waitForTxReceipt({ 146 | txHash, 147 | txOptions: request.txOptions, 148 | rpcClient: this.rpcClient, 149 | }); 150 | } catch (error) { 151 | return handleError(error, "Failed to transfer WIP"); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/core-sdk/src/resources/nftClient.ts: -------------------------------------------------------------------------------- 1 | import { Address, isAddress, maxUint32, PublicClient, zeroAddress } from "viem"; 2 | 3 | import { 4 | RegistrationWorkflowsClient, 5 | RegistrationWorkflowsCreateCollectionRequest, 6 | SimpleWalletClient, 7 | SpgnftImplClient, 8 | SpgnftImplReadOnlyClient, 9 | } from "../abi/generated"; 10 | import { TransactionResponse } from "../types/options"; 11 | import { 12 | CreateNFTCollectionRequest, 13 | CreateNFTCollectionResponse, 14 | GetTokenURIRequest, 15 | SetTokenURIRequest, 16 | } from "../types/resources/nftClient"; 17 | import { handleError } from "../utils/errors"; 18 | import { waitForTxReceipt } from "../utils/txOptions"; 19 | import { validateAddress } from "../utils/utils"; 20 | 21 | export class NftClient { 22 | public registrationWorkflowsClient: RegistrationWorkflowsClient; 23 | 24 | private readonly rpcClient: PublicClient; 25 | private readonly wallet: SimpleWalletClient; 26 | 27 | constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) { 28 | this.rpcClient = rpcClient; 29 | this.wallet = wallet; 30 | this.registrationWorkflowsClient = new RegistrationWorkflowsClient(rpcClient, wallet); 31 | } 32 | 33 | /** 34 | * Creates a new SPG NFT Collection. 35 | * 36 | * Emits an on-chain `CollectionCreated` event. 37 | * @see {@link https://github.com/storyprotocol/protocol-periphery-v1/blob/v1.3.1/contracts/interfaces/workflows/IRegistrationWorkflows.sol#L12 | IRegistrationWorkflows} 38 | */ 39 | public async createNFTCollection( 40 | request: CreateNFTCollectionRequest, 41 | ): Promise { 42 | try { 43 | if ( 44 | request.mintFee !== undefined && 45 | (request.mintFee < 0n || 46 | request.mintFeeToken === zeroAddress || 47 | !isAddress(request.mintFeeToken || "")) 48 | ) { 49 | throw new Error("Invalid mint fee token address, mint fee is greater than 0."); 50 | } 51 | 52 | const object: RegistrationWorkflowsCreateCollectionRequest = { 53 | spgNftInitParams: { 54 | name: request.name, 55 | symbol: request.symbol, 56 | baseURI: request.baseURI ?? "", 57 | maxSupply: request.maxSupply ?? Number(maxUint32), 58 | mintFee: BigInt(request.mintFee ?? 0), 59 | mintFeeToken: request.mintFeeToken ?? zeroAddress, 60 | owner: validateAddress(request.owner || this.wallet.account!.address), 61 | mintFeeRecipient: validateAddress(request.mintFeeRecipient), 62 | mintOpen: request.mintOpen, 63 | isPublicMinting: request.isPublicMinting, 64 | contractURI: request.contractURI, 65 | }, 66 | }; 67 | 68 | if (request.txOptions?.encodedTxDataOnly) { 69 | return { 70 | encodedTxData: this.registrationWorkflowsClient.createCollectionEncode(object), 71 | }; 72 | } else { 73 | const txHash = await this.registrationWorkflowsClient.createCollection(object); 74 | const txReceipt = await this.rpcClient.waitForTransactionReceipt({ 75 | ...request.txOptions, 76 | hash: txHash, 77 | }); 78 | const targetLogs = 79 | this.registrationWorkflowsClient.parseTxCollectionCreatedEvent(txReceipt); 80 | return { 81 | txHash: txHash, 82 | spgNftContract: targetLogs[0].spgNftContract, 83 | }; 84 | } 85 | } catch (error) { 86 | return handleError(error, "Failed to create an SPG NFT collection"); 87 | } 88 | } 89 | 90 | /** 91 | * Returns the current mint token of the collection. 92 | */ 93 | public async getMintFeeToken(spgNftContract: Address): Promise
{ 94 | const spgNftClient = new SpgnftImplReadOnlyClient( 95 | this.rpcClient, 96 | validateAddress(spgNftContract), 97 | ); 98 | return spgNftClient.mintFeeToken(); 99 | } 100 | 101 | /** 102 | * Returns the current mint fee of the collection. 103 | */ 104 | public async getMintFee(spgNftContract: Address): Promise { 105 | const spgNftClient = new SpgnftImplReadOnlyClient( 106 | this.rpcClient, 107 | validateAddress(spgNftContract), 108 | ); 109 | return spgNftClient.mintFee(); 110 | } 111 | 112 | /** 113 | * Sets the token URI for a specific token id. 114 | * 115 | * @remarks 116 | * Only callable by the owner of the token. 117 | */ 118 | public async setTokenURI({ 119 | tokenId, 120 | tokenURI, 121 | spgNftContract, 122 | txOptions, 123 | }: SetTokenURIRequest): Promise { 124 | try { 125 | const spgNftClient = new SpgnftImplClient( 126 | this.rpcClient, 127 | this.wallet, 128 | validateAddress(spgNftContract), 129 | ); 130 | const txHash = await spgNftClient.setTokenUri({ 131 | tokenId: BigInt(tokenId), 132 | tokenUri: tokenURI, 133 | }); 134 | return waitForTxReceipt({ 135 | txHash, 136 | txOptions, 137 | rpcClient: this.rpcClient, 138 | }); 139 | } catch (error) { 140 | return handleError(error, "Failed to set token URI"); 141 | } 142 | } 143 | 144 | /** 145 | * Returns the token URI for a specific token id. 146 | */ 147 | public async getTokenURI({ tokenId, spgNftContract }: GetTokenURIRequest): Promise { 148 | const spgNftClient = new SpgnftImplReadOnlyClient(this.rpcClient, spgNftContract); 149 | return await spgNftClient.tokenUri({ tokenId: BigInt(tokenId) }); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/royalty.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, TransactionReceipt } from "viem"; 2 | 3 | import { 4 | EncodedTxData, 5 | IpAccountImplClient, 6 | IpRoyaltyVaultImplRevenueTokenClaimedEvent, 7 | } from "../../abi/generated"; 8 | import { TokenAmountInput } from "../common"; 9 | import { WithErc20AndWipOptions, WithTxOptions } from "../options"; 10 | 11 | export type ClaimableRevenueRequest = { 12 | /** The IP ID of the royalty vault. */ 13 | ipId: Address; 14 | /** The address of the royalty token holder. */ 15 | claimer: Address; 16 | /** The revenue token to claim. */ 17 | token: Address; 18 | }; 19 | 20 | /** The amount of revenue token claimable. */ 21 | export type ClaimableRevenueResponse = bigint; 22 | 23 | export type PayRoyaltyOnBehalfRequest = WithTxOptions & 24 | WithErc20AndWipOptions & { 25 | /** The IP ID that receives the royalties. */ 26 | receiverIpId: Address; 27 | /** The IP ID that pays the royalties. */ 28 | payerIpId: Address; 29 | /** The token to use to pay the royalties. */ 30 | token: Address; 31 | /** The amount to pay. */ 32 | amount: TokenAmountInput; 33 | }; 34 | 35 | export type PayRoyaltyOnBehalfResponse = { 36 | txHash?: Hash; 37 | receipt?: TransactionReceipt; 38 | encodedTxData?: EncodedTxData; 39 | }; 40 | 41 | export type ClaimAllRevenueRequest = WithClaimOptions & { 42 | /** The address of the ancestor IP from which the revenue is being claimed. */ 43 | ancestorIpId: Address; 44 | /** 45 | * The address of the claimer of the currency (revenue) tokens. 46 | * 47 | * This is normally the ipId of the ancestor IP if the IP has all royalty tokens. 48 | * Otherwise, this would be the address that is holding the ancestor IP royalty tokens. 49 | */ 50 | claimer: Address; 51 | /** The addresses of the child IPs from which royalties are derived. */ 52 | childIpIds: Address[]; 53 | /** 54 | * The addresses of the royalty policies, where 55 | * royaltyPolicies[i] governs the royalty flow for childIpIds[i]. 56 | */ 57 | royaltyPolicies: Address[]; 58 | /** The addresses of the currency tokens in which royalties will be claimed */ 59 | currencyTokens: Address[]; 60 | }; 61 | 62 | export type WithClaimOptions = { 63 | claimOptions?: { 64 | /** 65 | * When enabled, all claimed tokens on the claimer are transferred to the 66 | * wallet address if the wallet owns the IP. If the wallet is the claimer 67 | * or if the claimer is not an IP owned by the wallet, then the tokens 68 | * will not be transferred. 69 | * Set to false to disable auto transferring claimed tokens from the claimer. 70 | * 71 | * @default true 72 | */ 73 | autoTransferAllClaimedTokensFromIp?: boolean; 74 | 75 | /** 76 | * By default all claimed WIP tokens are converted back to IP after 77 | * they are transferred. 78 | * Set this to false to disable this behavior. 79 | * 80 | * @default true 81 | */ 82 | autoUnwrapIpTokens?: boolean; 83 | }; 84 | }; 85 | export type BatchClaimAllRevenueRequest = WithClaimOptions & { 86 | /** The ancestor IPs from which the revenue is being claimed. */ 87 | ancestorIps: (Omit & { 88 | /** The address of the ancestor IP from which the revenue is being claimed. */ 89 | ipId: Address; 90 | })[]; 91 | options?: { 92 | /** 93 | * Use multicall to batch the calls `claimAllRevenue` into one transaction when possible. 94 | * 95 | * If only 1 ancestorIp is provided, multicall will not be used. 96 | * @default true 97 | */ 98 | useMulticallWhenPossible?: boolean; 99 | }; 100 | }; 101 | 102 | export type BatchClaimAllRevenueResponse = { 103 | txHashes: Hash[]; 104 | receipts: TransactionReceipt[]; 105 | claimedTokens?: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; 106 | }; 107 | 108 | export type ClaimAllRevenueResponse = { 109 | txHashes: Hash[]; 110 | receipt: TransactionReceipt; 111 | /** 112 | * Aggregate list of all tokens claimed across all transactions in the batch. 113 | * Events are aggregated by unique combinations of claimer and token addresses, 114 | * summing up the amounts for the same claimer-token pairs. 115 | */ 116 | claimedTokens: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; 117 | }; 118 | 119 | export type TransferClaimedTokensFromIpToWalletParams = { 120 | ipAccount: IpAccountImplClient; 121 | claimedTokens: IpRoyaltyVaultImplRevenueTokenClaimedEvent[]; 122 | }; 123 | 124 | /** 125 | * Native royalty policy created by the Story team 126 | * - LAP: {@link https://docs.story.foundation/concepts/royalty-module/liquid-absolute-percentage | Liquid Absolute Percentage} 127 | * - LRP: {@link https://docs.story.foundation/concepts/royalty-module/liquid-relative-percentage | Liquid Relative Percentage} 128 | */ 129 | export enum NativeRoyaltyPolicy { 130 | LAP = 0, 131 | LRP, 132 | } 133 | 134 | /** 135 | * Allow custom royalty policy address or use a native royalty policy enum. 136 | * For custom royalty policy, @see {@link https://docs.story.foundation/concepts/royalty-module/external-royalty-policies | External Royalty Policies} 137 | */ 138 | export type RoyaltyPolicyInput = Address | NativeRoyaltyPolicy; 139 | 140 | export type TransferToVaultRequest = WithTxOptions & { 141 | royaltyPolicy: RoyaltyPolicyInput; 142 | ipId: Address; 143 | ancestorIpId: Address; 144 | /** the token address to transfer */ 145 | token: Address; 146 | }; 147 | 148 | export type ClaimerInfo = { 149 | ownsClaimer: boolean; 150 | isClaimerIp: boolean; 151 | ipAccount: IpAccountImplClient; 152 | }; 153 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to npm, Tag and create GH Release 2 | 3 | on: 4 | workflow_dispatch: 5 | # This allows manual triggering of the workflow 6 | 7 | permissions: 8 | contents: write 9 | actions: write 10 | 11 | jobs: 12 | timestamp: 13 | uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main 14 | 15 | print_version_to_publish: 16 | needs: [timestamp] 17 | runs-on: ubuntu-latest 18 | outputs: 19 | CORE_SDK_VERSION_TO_BE_PUBLISHED: ${{ steps.get_version_to_publish.outputs.CORE_SDK_VERSION_TO_BE_PUBLISHED }} 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 23 | 24 | - name: Get version to publish 25 | id: get_version_to_publish 26 | run: | 27 | content=$(cat packages/core-sdk/package.json) 28 | echo "CORE_SDK_VERSION_TO_BE_PUBLISHED=$(echo $content | jq -r '.version')" >> $GITHUB_OUTPUT 29 | 30 | # Fetch the latest version from NPM 31 | fetch_latest_version: 32 | needs: [timestamp, print_version_to_publish] 33 | runs-on: ubuntu-latest 34 | outputs: 35 | CORE_SDK_LATEST_VERSION: ${{ steps.get_latest_version.outputs.CORE_SDK_LATEST_VERSION }} 36 | steps: 37 | - name: Get latest package version 38 | id: get_latest_version 39 | run: | 40 | CORE_SDK_LATEST_VERSION=$(npm view @story-protocol/core-sdk version --silent) 41 | echo "Latest version of @story-protocol/core-sdk on NPMJS is $CORE_SDK_LATEST_VERSION" 42 | echo "CORE_SDK_LATEST_VERSION=$CORE_SDK_LATEST_VERSION" >> $GITHUB_OUTPUT 43 | 44 | # Fail the PR if the version to be published is the same as the latest version on NPM 45 | fail_if_version_is_same: 46 | needs: [print_version_to_publish, fetch_latest_version] 47 | runs-on: ubuntu-latest 48 | outputs: 49 | IS_PUBLISH_CORE_SDK: ${{ steps.check_publish_condition.outputs.IS_PUBLISH_CORE_SDK }} 50 | steps: 51 | - name: check publish condition 52 | id: check_publish_condition 53 | run: | 54 | CORE_SDK_LATEST_VERSION="${{ needs.fetch_latest_version.outputs.CORE_SDK_LATEST_VERSION }}" 55 | CORE_SDK_VERSION_TO_BE_PUBLISHED="${{ needs.print_version_to_publish.outputs.CORE_SDK_VERSION_TO_BE_PUBLISHED }}" 56 | 57 | IS_PUBLISH_CORE_SDK=false 58 | 59 | if [ "$CORE_SDK_LATEST_VERSION" == "$CORE_SDK_VERSION_TO_BE_PUBLISHED" ]; then 60 | echo "The @story-protocol/core-sdk version to be published is the same as the latest version on NPM." 61 | exit 1 62 | else 63 | IS_PUBLISH_CORE_SDK=true 64 | fi 65 | 66 | echo "IS_PUBLISH_CORE_SDK=$IS_PUBLISH_CORE_SDK" >> $GITHUB_OUTPUT 67 | 68 | fetch_last_tag: 69 | needs: [fail_if_version_is_same] 70 | if: ${{ needs.fail_if_version_is_same.outputs.IS_PUBLISH_CORE_SDK == 'true' && github.event_name == 'workflow_dispatch' }} 71 | runs-on: ubuntu-latest 72 | outputs: 73 | CORE_SDK_LATEST_TAG: ${{ steps.get_last_tag.outputs.CORE_SDK_LATEST_TAG }} 74 | steps: 75 | - name: Checkout 76 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 77 | 78 | - name: Get last tag 79 | id: get_last_tag 80 | run: | 81 | git fetch --tags 82 | CORE_SDK_LATEST_TAG=$(git tag --sort=creatordate | grep -E "@story-protocol/core-sdk|core-sdk|^v[0-9]" | tail -n1) 83 | echo "CORE_SDK_LATEST_TAG=$CORE_SDK_LATEST_TAG" >> $GITHUB_OUTPUT 84 | echo "Last tag for @story-protocol/core-sdk is $CORE_SDK_LATEST_TAG" 85 | 86 | build-test-publish: 87 | needs: [fail_if_version_is_same, fetch_last_tag] 88 | if: ${{ needs.fail_if_version_is_same.outputs.IS_PUBLISH_CORE_SDK == 'true' && github.event_name == 'workflow_dispatch' }} 89 | runs-on: ubuntu-latest 90 | environment: "odyssey" 91 | env: 92 | WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }} 93 | TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }} 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 97 | 98 | - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 99 | with: 100 | version: 8.8.0 101 | 102 | - name: Setup Node.js environment 103 | uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 104 | with: 105 | node-version: 20.0.0 106 | cache: pnpm 107 | registry-url: https://registry.npmjs.org/ 108 | 109 | - name: Install dependencies 110 | run: pnpm install 111 | 112 | - name: Build 113 | run: pnpm build 114 | 115 | - name: Publish core-sdk package to npm 116 | if: ${{ github.event_name == 'workflow_dispatch' && needs.fail_if_version_is_same.outputs.IS_PUBLISH_CORE_SDK == 'true'}} 117 | run: | 118 | cd packages/core-sdk 119 | npm publish 120 | env: 121 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 122 | 123 | send_slack_notif-core-sdk: 124 | runs-on: ubuntu-latest 125 | environment: "odyssey" 126 | steps: 127 | - name: Send Slack Notification 128 | run: | 129 | curl -X POST \ 130 | -H 'Content-type: application/json' \ 131 | --data '{ 132 | "channel": "proj-sdk", 133 | "text":"${{ github.repository }}: @story-protocol/core-sdk package has been published to NPM Registry, version: ${{ needs.print_version_to_publish.outputs.CORE_SDK_VERSION_TO_BE_PUBLISHED}}" 134 | }' \ 135 | ${{ secrets.SLACK_SDK_MESSENGER_WEBHOOK_URL }} -------------------------------------------------------------------------------- /packages/core-sdk/src/types/resources/dispute.ts: -------------------------------------------------------------------------------- 1 | import { Address, Hash, Hex } from "viem"; 2 | 3 | import { EncodedTxData } from "../../abi/generated"; 4 | import { TxOptions, WipOptions, WithTxOptions } from "../options"; 5 | 6 | export type DisputeId = number | bigint; 7 | 8 | export type RaiseDisputeRequest = WithTxOptions & { 9 | /** The IP ID that is the target of the dispute. */ 10 | targetIpId: Address; 11 | /** 12 | * Content Identifier (CID) for the dispute evidence. 13 | * This should be obtained by uploading your dispute evidence (documents, images, etc.) to IPFS. 14 | * @example "QmX4zdp8VpzqvtKuEqMo6gfZPdoUx9TeHXCgzKLcFfSUbk" 15 | */ 16 | cid: string; 17 | /** 18 | * The target tag of the dispute. 19 | * @see {@link https://docs.story.foundation/docs/dispute-module#dispute-tags | Dispute Tags} 20 | * @example {@link DisputeTargetTag.IMPROPER_REGISTRATION} 21 | */ 22 | targetTag: DisputeTargetTag; 23 | /** The liveness is the time window (in seconds) in which a counter dispute can be presented (30days). */ 24 | liveness: bigint | number; 25 | /** 26 | * The amount of wrapper IP that the dispute initiator pays upfront into a pool. 27 | * To counter that dispute the opposite party of the dispute has to place a bond of the same amount. 28 | * The winner of the dispute gets the original bond back + 50% of the other party bond. The remaining 50% of the loser party bond goes to the reviewer. 29 | * The bond amount must be between the minimum and maximum bond values defined in the Optimistic Oracle V3 (OOV3) contract. If not specified, it defaults to the minimum bond value. 30 | */ 31 | bond?: bigint | number; 32 | /** 33 | * Omit {@link WipOptions.useMulticallWhenPossible} for this function due to disputeInitiator issue. 34 | * It will be executed sequentially with several transactions. 35 | */ 36 | wipOptions?: Omit; 37 | }; 38 | 39 | export type RaiseDisputeResponse = { 40 | txHash?: Hash; 41 | encodedTxData?: EncodedTxData; 42 | disputeId?: bigint; 43 | }; 44 | 45 | export type CancelDisputeRequest = { 46 | disputeId: DisputeId; 47 | /** 48 | * Additional data used in the cancellation process. 49 | * 50 | * @default 0x 51 | */ 52 | data?: Hex; 53 | txOptions?: TxOptions; 54 | }; 55 | 56 | export type CancelDisputeResponse = { 57 | txHash?: Hash; 58 | encodedTxData?: EncodedTxData; 59 | }; 60 | 61 | export type ResolveDisputeRequest = { 62 | disputeId: DisputeId; 63 | /** 64 | * Additional data used in the resolution process. 65 | * 66 | * @default 0x 67 | */ 68 | data?: Hex; 69 | txOptions?: TxOptions; 70 | }; 71 | 72 | export type ResolveDisputeResponse = { 73 | txHash?: Hash; 74 | encodedTxData?: EncodedTxData; 75 | }; 76 | 77 | export type TagIfRelatedIpInfringedRequest = { 78 | infringementTags: { 79 | /** The ipId to tag */ 80 | ipId: Address; 81 | /** The dispute id that tagged the related infringing ipId */ 82 | disputeId: DisputeId; 83 | }[]; 84 | options?: { 85 | /** 86 | * Use multicall to batch the calls into one transaction when possible. 87 | * 88 | * If only 1 infringementTag is provided, multicall will not be used. 89 | * @default true 90 | */ 91 | useMulticallWhenPossible?: boolean; 92 | }; 93 | } & WithTxOptions; 94 | 95 | export type DisputeAssertionRequest = { 96 | /** 97 | * The IP ID that is the target of the dispute. 98 | */ 99 | ipId: Address; 100 | /** 101 | * The identifier of the assertion that was disputed. 102 | * 103 | * You can get this from the `disputeId` by calling `dispute.disputeIdToAssertionId`. 104 | */ 105 | assertionId: Hex; 106 | /** 107 | * Content Identifier (CID) for the counter evidence. 108 | * This should be obtained by uploading your dispute evidence (documents, images, etc.) to IPFS. 109 | * 110 | * @example "QmX4zdp8VpzqvtKuEqMo6gfZPdoUx9TeHXCgzKLcFfSUbk" 111 | */ 112 | counterEvidenceCID: string; 113 | /** 114 | * Omit {@link WipOptions.useMulticallWhenPossible} for this function due to disputeInitiator issue. 115 | * It will be executed sequentially with several transactions. 116 | */ 117 | wipOptions?: Omit; 118 | } & WithTxOptions; 119 | 120 | /** 121 | * Tags refer to the “labels” that can be applied to IP Assets in the protocol when raising a dispute. 122 | * Tags must be whitelisted by protocol governance to be used in a dispute. 123 | * @see {@link https://docs.story.foundation/docs/dispute-module#dispute-tags | Dispute Tags} 124 | */ 125 | export enum DisputeTargetTag { 126 | /** Refers to registration of IP that already exists. */ 127 | IMPROPER_REGISTRATION = "IMPROPER_REGISTRATION", 128 | /** 129 | * Refers to improper use of an IP Asset across multiple items. 130 | * For more details, @see {@link https://docs.story.foundation/concepts/programmable-ip-license/overview | Programmable IP License (PIL)} documentation. 131 | */ 132 | IMPROPER_USAGE = "IMPROPER_USAGE", 133 | /** Refers to missing payments associated with an IP. */ 134 | IMPROPER_PAYMENT = "IMPROPER_PAYMENT", 135 | /** 136 | * Refers to “No-Hate”, “Suitable-for-All-Ages”, “No-Drugs-or-Weapons” and “No-Pornography”. 137 | * These items can be found in more detail in the {@link https://docs.story.foundation/concepts/programmable-ip-license/overview | 💊 Programmable IP License (PIL) } legal document. 138 | */ 139 | CONTENT_STANDARDS_VIOLATION = "CONTENT_STANDARDS_VIOLATION", 140 | /** 141 | * Different from the other 4, this is a temporary tag that goes away 142 | * at the end of a dispute and is replaced by “0x” in case of no infringement or is replaced by one of the other tags. 143 | */ 144 | IN_DISPUTE = "IN_DISPUTE", 145 | } 146 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/resources/nftClient.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import { stub } from "sinon"; 4 | import { PublicClient, WalletClient } from "viem"; 5 | 6 | import { NftClient } from "../../../src"; 7 | import { SpgnftImplClient, SpgnftImplReadOnlyClient } from "../../../src/abi/generated"; 8 | import { mockERC20, txHash } from "../mockData"; 9 | import { createMockPublicClient, createMockWalletClient } from "../testUtils"; 10 | 11 | use(chaiAsPromised); 12 | 13 | describe("Test NftClient", () => { 14 | let nftClient: NftClient; 15 | let rpcMock: PublicClient; 16 | let walletMock: WalletClient; 17 | const mintFeeToken = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c"; 18 | 19 | beforeEach(() => { 20 | rpcMock = createMockPublicClient(); 21 | walletMock = createMockWalletClient(); 22 | nftClient = new NftClient(rpcMock, walletMock); 23 | }); 24 | 25 | describe("test for CreateNFTCollection", () => { 26 | it("should throw mint fee error when call createNFTCollection given mintFee less than 0", async () => { 27 | try { 28 | await nftClient.createNFTCollection({ 29 | name: "name", 30 | symbol: "symbol", 31 | maxSupply: 1, 32 | mintFee: -1, 33 | isPublicMinting: true, 34 | mintOpen: true, 35 | mintFeeRecipient: "0x", 36 | contractURI: "test-uri", 37 | }); 38 | } catch (e) { 39 | expect((e as Error).message).equal( 40 | "Failed to create an SPG NFT collection: Invalid mint fee token address, mint fee is greater than 0.", 41 | ); 42 | } 43 | }); 44 | 45 | it("should throw mint fee error when call createNFTCollection given mintFeeToken is invalid", async () => { 46 | try { 47 | await nftClient.createNFTCollection({ 48 | name: "name", 49 | symbol: "symbol", 50 | maxSupply: 1, 51 | mintFee: 1, 52 | isPublicMinting: true, 53 | mintOpen: true, 54 | mintFeeRecipient: "0x", 55 | contractURI: "test-uri", 56 | }); 57 | } catch (e) { 58 | expect((e as Error).message).equal( 59 | "Failed to create an SPG NFT collection: Invalid mint fee token address, mint fee is greater than 0.", 60 | ); 61 | } 62 | }); 63 | 64 | it("should return txHash when call createNFTCollection successfully", async () => { 65 | const spgNftContract = "0x73fcb515cee99e4991465ef586cfe2b072ebb512"; 66 | stub(nftClient.registrationWorkflowsClient, "createCollection").resolves(txHash); 67 | 68 | stub(nftClient.registrationWorkflowsClient, "parseTxCollectionCreatedEvent").returns([ 69 | { spgNftContract }, 70 | ]); 71 | const result = await nftClient.createNFTCollection({ 72 | name: "name", 73 | symbol: "symbol", 74 | owner: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", 75 | isPublicMinting: true, 76 | mintOpen: true, 77 | contractURI: "test-uri", 78 | mintFeeRecipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", 79 | }); 80 | 81 | expect(result.txHash).equal(txHash); 82 | expect(result.spgNftContract).equal(spgNftContract); 83 | }); 84 | 85 | it("should return encodedTxData when call createNFTCollection successfully with encodedTxDataOnly", async () => { 86 | stub(nftClient.registrationWorkflowsClient, "createCollectionEncode").returns({ 87 | data: "0x", 88 | to: "0x", 89 | }); 90 | 91 | const result = await nftClient.createNFTCollection({ 92 | name: "name", 93 | symbol: "symbol", 94 | maxSupply: 1, 95 | mintFee: 1, 96 | mintFeeToken: mintFeeToken, 97 | isPublicMinting: true, 98 | contractURI: "test-uri", 99 | mintOpen: true, 100 | mintFeeRecipient: "0x73fcb515cee99e4991465ef586cfe2b072ebb512", 101 | txOptions: { 102 | encodedTxDataOnly: true, 103 | }, 104 | }); 105 | 106 | expect(result.encodedTxData?.data).to.be.a("string"); 107 | }); 108 | }); 109 | 110 | describe("test for getMintFeeToken", () => { 111 | it("should successfully get mint fee token", async () => { 112 | stub(SpgnftImplReadOnlyClient.prototype, "mintFeeToken").resolves(mockERC20); 113 | const result = await nftClient.getMintFeeToken(mockERC20); 114 | expect(result).equal(mockERC20); 115 | }); 116 | }); 117 | 118 | describe("test for getMintFee", () => { 119 | it("should successfully get mint fee", async () => { 120 | stub(SpgnftImplReadOnlyClient.prototype, "mintFee").resolves(1n); 121 | const mintFee = await nftClient.getMintFee(mockERC20); 122 | expect(mintFee).equal(1n); 123 | }); 124 | }); 125 | 126 | describe("test for setTokenURI", () => { 127 | it("should throw error when call setTokenURI fail", async () => { 128 | stub(SpgnftImplClient.prototype, "setTokenUri").throws(new Error("rpc error")); 129 | await expect( 130 | nftClient.setTokenURI({ 131 | tokenId: 1n, 132 | tokenURI: "test-uri", 133 | spgNftContract: mockERC20, 134 | }), 135 | ).to.be.rejectedWith("Failed to set token URI: rpc error"); 136 | }); 137 | it("should successfully set token URI", async () => { 138 | stub(SpgnftImplClient.prototype, "setTokenUri").resolves(txHash); 139 | const result = await nftClient.setTokenURI({ 140 | tokenId: 1n, 141 | tokenURI: "test-uri", 142 | spgNftContract: mockERC20, 143 | }); 144 | expect(result.txHash).equal(txHash); 145 | }); 146 | }); 147 | 148 | describe("test for getTokenURI", () => { 149 | it("should successfully get token URI", async () => { 150 | stub(SpgnftImplReadOnlyClient.prototype, "tokenUri").resolves("test-uri"); 151 | const tokenURI = await nftClient.getTokenURI({ tokenId: 1n, spgNftContract: mockERC20 }); 152 | expect(tokenURI).equal("test-uri"); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/utils/getFunctionSignature.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { coreMetadataModuleAbi, groupingWorkflowsAbi } from "../../../src/abi/generated"; 4 | import { getFunctionSignature } from "../../../src/utils/getFunctionSignature"; 5 | 6 | describe("Test getFunctionSignature", () => { 7 | it("should return function signature", () => { 8 | const signature = getFunctionSignature(coreMetadataModuleAbi, "setAll"); 9 | expect(signature).equal("setAll(address,string,bytes32,bytes32)"); 10 | }); 11 | 12 | it("should throw error when method not found", () => { 13 | expect(() => getFunctionSignature(coreMetadataModuleAbi, "notFound")).to.throw( 14 | "Method notFound not found in ABI.", 15 | ); 16 | }); 17 | 18 | it("should throw error when method has multiple overloads", () => { 19 | expect(() => 20 | getFunctionSignature( 21 | [ 22 | ...groupingWorkflowsAbi, 23 | { 24 | type: "function", 25 | inputs: [ 26 | { name: "groupPool", internalType: "address", type: "address" }, 27 | { name: "ipIds", internalType: "address[]", type: "address[]" }, 28 | { 29 | name: "licenseData", 30 | internalType: "struct WorkflowStructs.LicenseData", 31 | type: "tuple", 32 | components: [ 33 | { name: "licenseTemplate", internalType: "address", type: "address" }, 34 | { name: "licenseTermsId", internalType: "uint256", type: "uint256" }, 35 | { 36 | name: "licensingConfig", 37 | internalType: "struct Licensing.LicensingConfig", 38 | type: "tuple", 39 | components: [ 40 | { name: "isSet", internalType: "bool", type: "bool" }, 41 | { name: "mintingFee", internalType: "uint256", type: "uint256" }, 42 | { 43 | name: "licensingHook", 44 | internalType: "address", 45 | type: "address", 46 | }, 47 | { name: "hookData", internalType: "bytes", type: "bytes" }, 48 | { 49 | name: "commercialRevShare", 50 | internalType: "uint32", 51 | type: "uint32", 52 | }, 53 | { name: "disabled", internalType: "bool", type: "bool" }, 54 | { 55 | name: "expectMinimumGroupRewardShare", 56 | internalType: "uint32", 57 | type: "uint32", 58 | }, 59 | { 60 | name: "expectGroupRewardPool", 61 | internalType: "address", 62 | type: "address", 63 | }, 64 | ], 65 | }, 66 | ], 67 | }, 68 | ], 69 | name: "registerGroupAndAttachLicenseAndAddIps", 70 | outputs: [{ name: "groupId", internalType: "address", type: "address" }], 71 | stateMutability: "nonpayable", 72 | }, 73 | ], 74 | "registerGroupAndAttachLicenseAndAddIps", 75 | ), 76 | ).to.throw( 77 | "Method registerGroupAndAttachLicenseAndAddIps has 2 overloads. Please specify overloadIndex (0-1).", 78 | ); 79 | }); 80 | 81 | it("should return function signature when method has multiple overloads", () => { 82 | const signature = getFunctionSignature( 83 | [ 84 | ...groupingWorkflowsAbi, 85 | { 86 | type: "function", 87 | inputs: [ 88 | { name: "groupPool", internalType: "address", type: "address" }, 89 | { name: "ipIds", internalType: "address[]", type: "address[]" }, 90 | { 91 | name: "licenseData", 92 | internalType: "struct WorkflowStructs.LicenseData", 93 | type: "tuple", 94 | components: [ 95 | { name: "licenseTemplate", internalType: "address", type: "address" }, 96 | { name: "licenseTermsId", internalType: "uint256", type: "uint256" }, 97 | { 98 | name: "licensingConfig", 99 | internalType: "struct Licensing.LicensingConfig", 100 | type: "tuple", 101 | components: [ 102 | { name: "isSet", internalType: "bool", type: "bool" }, 103 | { name: "mintingFee", internalType: "uint256", type: "uint256" }, 104 | { 105 | name: "licensingHook", 106 | internalType: "address", 107 | type: "address", 108 | }, 109 | { name: "hookData", internalType: "bytes", type: "bytes" }, 110 | { 111 | name: "commercialRevShare", 112 | internalType: "uint32", 113 | type: "uint32", 114 | }, 115 | { name: "disabled", internalType: "bool", type: "bool" }, 116 | { 117 | name: "expectMinimumGroupRewardShare", 118 | internalType: "uint32", 119 | type: "uint32", 120 | }, 121 | ], 122 | }, 123 | ], 124 | }, 125 | ], 126 | name: "registerGroupAndAttachLicenseAndAddIps", 127 | outputs: [{ name: "groupId", internalType: "address", type: "address" }], 128 | stateMutability: "nonpayable", 129 | }, 130 | ], 131 | "registerGroupAndAttachLicenseAndAddIps", 132 | 1, 133 | ); 134 | expect(signature).equal( 135 | "registerGroupAndAttachLicenseAndAddIps(address,address[],(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32)))", 136 | ); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/core-sdk/test/unit/resources/wip.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import { stub } from "sinon"; 4 | import { PublicClient, WalletClient } from "viem"; 5 | 6 | import { WipClient } from "../../../src/resources/wip"; 7 | import { txHash } from "../mockData"; 8 | import { createMockPublicClient, createMockWalletClient } from "../testUtils"; 9 | 10 | use(chaiAsPromised); 11 | 12 | describe("WIP Functions", () => { 13 | let wipClient: WipClient; 14 | let rpcMock: PublicClient; 15 | let walletMock: WalletClient; 16 | 17 | before(() => { 18 | rpcMock = createMockPublicClient(); 19 | walletMock = createMockWalletClient(); 20 | wipClient = new WipClient(rpcMock, walletMock); 21 | }); 22 | 23 | describe("deposit", () => { 24 | before(() => { 25 | wipClient = new WipClient(rpcMock, walletMock); 26 | }); 27 | 28 | it("should throw an error when call deposit give amount is less than 0", async () => { 29 | try { 30 | await wipClient.deposit({ 31 | amount: 0, 32 | }); 33 | } catch (error) { 34 | expect((error as Error).message).equals( 35 | "Failed to deposit IP for WIP: WIP deposit amount must be greater than 0.", 36 | ); 37 | } 38 | }); 39 | it("should deposit successfully when call deposit given amount is 1 ", async () => { 40 | const rsp = await wipClient.deposit({ 41 | amount: 1, 42 | }); 43 | expect(rsp.txHash).to.be.a("string"); 44 | }); 45 | }); 46 | 47 | describe("withdraw", () => { 48 | it("should throw an error when call withdraw given amount is less than 0", async () => { 49 | try { 50 | await wipClient.withdraw({ 51 | amount: 0, 52 | }); 53 | } catch (error) { 54 | expect((error as Error).message).equals( 55 | "Failed to withdraw WIP: WIP withdraw amount must be greater than 0.", 56 | ); 57 | } 58 | }); 59 | 60 | it("should withdraw successfully when call withdraw given amount is 1", async () => { 61 | stub(wipClient.wrappedIpClient, "withdraw").resolves(txHash); 62 | const rsp = await wipClient.withdraw({ 63 | amount: 1, 64 | }); 65 | expect(rsp.txHash).to.be.a("string"); 66 | }); 67 | }); 68 | 69 | describe("approve", () => { 70 | it("should throw an error when call approve given amount is 0", async () => { 71 | try { 72 | await wipClient.approve({ 73 | amount: 0, 74 | spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 75 | }); 76 | } catch (error) { 77 | expect((error as Error).message).equals( 78 | "Failed to approve WIP: WIP approve amount must be greater than 0.", 79 | ); 80 | } 81 | }); 82 | 83 | it("should approve successfully when call approve given amount is 1", async () => { 84 | stub(wipClient.wrappedIpClient, "approve").resolves(txHash); 85 | const rsp = await wipClient.approve({ 86 | amount: 1, 87 | spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 88 | }); 89 | expect(rsp.txHash).to.be.a("string"); 90 | }); 91 | 92 | it("should approve successfully when call approve given amount is 1", async () => { 93 | stub(wipClient.wrappedIpClient, "approve").resolves(txHash); 94 | const rsp = await wipClient.approve({ 95 | amount: 1, 96 | spender: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 97 | }); 98 | expect(rsp.txHash).to.be.a("string"); 99 | }); 100 | }); 101 | 102 | describe("getBalance", () => { 103 | it("should throw an error when call getBalance given address is invalid", async () => { 104 | try { 105 | await wipClient.balanceOf("0x"); 106 | } catch (error) { 107 | expect((error as Error).message).equals("Invalid address: 0x."); 108 | } 109 | }); 110 | 111 | it("should get balance successfully when call getBalance given address is valid", async () => { 112 | stub(wipClient.wrappedIpClient, "balanceOf").resolves({ result: 0n }); 113 | const rsp = await wipClient.balanceOf("0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91"); 114 | expect(rsp).to.be.a("bigint"); 115 | }); 116 | }); 117 | 118 | describe("transfer", () => { 119 | it("should throw an error when call transfer given amount is 0", async () => { 120 | try { 121 | await wipClient.transfer({ 122 | amount: 0, 123 | to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 124 | }); 125 | } catch (error) { 126 | expect((error as Error).message).equals( 127 | "Failed to transfer WIP: WIP transfer amount must be greater than 0.", 128 | ); 129 | } 130 | }); 131 | 132 | it("should transfer successfully when call transfer given amount is 1", async () => { 133 | stub(wipClient.wrappedIpClient, "transfer").resolves(txHash); 134 | const rsp = await wipClient.transfer({ 135 | amount: 1, 136 | to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 137 | }); 138 | 139 | expect(rsp.txHash).to.be.a("string"); 140 | }); 141 | }); 142 | 143 | describe("transferFrom", () => { 144 | it("should throw an error when call transferFrom given amount is 0", async () => { 145 | try { 146 | await wipClient.transferFrom({ 147 | amount: 0, 148 | from: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 149 | to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 150 | }); 151 | } catch (error) { 152 | expect((error as Error).message).equals( 153 | "Failed to transfer WIP: WIP transfer amount must be greater than 0.", 154 | ); 155 | } 156 | }); 157 | 158 | it("should transfer successfully when call transferFrom given amount is 1", async () => { 159 | stub(wipClient.wrappedIpClient, "transferFrom").resolves(txHash); 160 | const rsp = await wipClient.transferFrom({ 161 | amount: 1, 162 | from: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 163 | to: "0x12fcbf7d94388da4D4a38bEF15B19289a00e6c91", 164 | }); 165 | expect(rsp.txHash).to.be.a("string"); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /packages/core-sdk/src/client.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import { createPublicClient, createWalletClient, PublicClient } from "viem"; 3 | 4 | import { SimpleWalletClient } from "./abi/generated"; 5 | import { DisputeClient } from "./resources/dispute"; 6 | import { GroupClient } from "./resources/group"; 7 | import { IPAccountClient } from "./resources/ipAccount"; 8 | import { IPAssetClient } from "./resources/ipAsset"; 9 | import { LicenseClient } from "./resources/license"; 10 | import { NftClient } from "./resources/nftClient"; 11 | import { PermissionClient } from "./resources/permission"; 12 | import { RoyaltyClient } from "./resources/royalty"; 13 | import { WipClient } from "./resources/wip"; 14 | import { ChainIds, StoryConfig, UseAccountStoryConfig, UseWalletStoryConfig } from "./types/config"; 15 | import { chain, chainStringToViemChain, validateAddress } from "./utils/utils"; 16 | 17 | if (typeof process !== "undefined") { 18 | dotenv.config(); 19 | } 20 | /** 21 | * The StoryClient is the main entry point for the SDK. 22 | */ 23 | export class StoryClient { 24 | private readonly config: StoryConfig; 25 | private readonly rpcClient: PublicClient; 26 | private readonly wallet: SimpleWalletClient; 27 | private _ipAsset: IPAssetClient | null = null; 28 | private _permission: PermissionClient | null = null; 29 | private _license: LicenseClient | null = null; 30 | private _dispute: DisputeClient | null = null; 31 | private _ipAccount: IPAccountClient | null = null; 32 | private _royalty: RoyaltyClient | null = null; 33 | private _nftClient: NftClient | null = null; 34 | private _group: GroupClient | null = null; 35 | private _wip: WipClient | null = null; 36 | 37 | private constructor(config: StoryConfig) { 38 | this.config = { 39 | ...config, 40 | chainId: chain[config.chainId || 1315], 41 | }; 42 | if (!this.config.transport) { 43 | throw new Error( 44 | "transport is null, please pass in a valid RPC Provider URL as the transport.", 45 | ); 46 | } 47 | 48 | const clientConfig = { 49 | chain: chainStringToViemChain(this.chainId), 50 | transport: this.config.transport, 51 | }; 52 | 53 | this.rpcClient = createPublicClient(clientConfig); 54 | 55 | if (this.config.wallet) { 56 | this.wallet = this.config.wallet; 57 | } else if (this.config.account) { 58 | const account = this.config.account; 59 | 60 | this.wallet = createWalletClient({ 61 | ...clientConfig, 62 | account: account, 63 | }); 64 | } else { 65 | throw new Error("must specify a wallet or account"); 66 | } 67 | } 68 | 69 | private get chainId(): ChainIds { 70 | return this.config.chainId as ChainIds; 71 | } 72 | 73 | /** 74 | * Factory method for creating an SDK client with a signer. 75 | * 76 | */ 77 | static newClient(config: StoryConfig): StoryClient { 78 | return new StoryClient(config); 79 | } 80 | 81 | /** 82 | * Factory method for creating an SDK client with a signer. 83 | */ 84 | static newClientUseWallet(config: UseWalletStoryConfig): StoryClient { 85 | return new StoryClient({ 86 | chainId: config.chainId, 87 | transport: config.transport, 88 | wallet: config.wallet, 89 | }); 90 | } 91 | 92 | /** 93 | * Factory method for creating an SDK client with a signer. 94 | */ 95 | static newClientUseAccount(config: UseAccountStoryConfig): StoryClient { 96 | return new StoryClient({ 97 | account: config.account, 98 | chainId: config.chainId, 99 | transport: config.transport, 100 | }); 101 | } 102 | 103 | /** 104 | * Getter for the ip asset client. The client is lazily created when 105 | * this method is called. 106 | */ 107 | public get ipAsset(): IPAssetClient { 108 | if (this._ipAsset === null) { 109 | this._ipAsset = new IPAssetClient(this.rpcClient, this.wallet, this.chainId); 110 | } 111 | 112 | return this._ipAsset; 113 | } 114 | 115 | /** 116 | * Getter for the permission client. The client is lazily created when 117 | * this method is called. 118 | */ 119 | public get permission(): PermissionClient { 120 | if (this._permission === null) { 121 | this._permission = new PermissionClient(this.rpcClient, this.wallet, this.chainId); 122 | } 123 | 124 | return this._permission; 125 | } 126 | 127 | /** 128 | * Getter for the license client. The client is lazily created when 129 | * this method is called. 130 | */ 131 | public get license(): LicenseClient { 132 | if (this._license === null) { 133 | this._license = new LicenseClient(this.rpcClient, this.wallet, this.chainId); 134 | } 135 | 136 | return this._license; 137 | } 138 | 139 | /** 140 | * Getter for the dispute client. The client is lazily created when 141 | * this method is called. 142 | */ 143 | public get dispute(): DisputeClient { 144 | if (this._dispute === null) { 145 | this._dispute = new DisputeClient(this.rpcClient, this.wallet, this.chainId); 146 | } 147 | 148 | return this._dispute; 149 | } 150 | 151 | /** 152 | * Getter for the ip account client. The client is lazily created when 153 | * this method is called. 154 | */ 155 | public get ipAccount(): IPAccountClient { 156 | if (this._ipAccount === null) { 157 | this._ipAccount = new IPAccountClient(this.rpcClient, this.wallet, this.chainId); 158 | } 159 | 160 | return this._ipAccount; 161 | } 162 | 163 | /** 164 | * Getter for the royalty client. The client is lazily created when 165 | * this method is called. 166 | */ 167 | public get royalty(): RoyaltyClient { 168 | if (this._royalty === null) { 169 | this._royalty = new RoyaltyClient(this.rpcClient, this.wallet, this.chainId); 170 | } 171 | 172 | return this._royalty; 173 | } 174 | 175 | /** 176 | * Getter for the NFT client. The client is lazily created when 177 | * this method is called. 178 | */ 179 | public get nftClient(): NftClient { 180 | if (this._nftClient === null) { 181 | this._nftClient = new NftClient(this.rpcClient, this.wallet); 182 | } 183 | 184 | return this._nftClient; 185 | } 186 | 187 | /** 188 | * Getter for the group client. The client is lazily created when 189 | * this method is called. 190 | */ 191 | public get groupClient(): GroupClient { 192 | if (this._group === null) { 193 | this._group = new GroupClient(this.rpcClient, this.wallet, this.chainId); 194 | } 195 | 196 | return this._group; 197 | } 198 | 199 | public get wipClient(): WipClient { 200 | if (this._wip === null) { 201 | this._wip = new WipClient(this.rpcClient, this.wallet); 202 | } 203 | return this._wip; 204 | } 205 | 206 | public async getWalletBalance(): Promise { 207 | if (!this.wallet.account) { 208 | throw new Error("No account found in wallet"); 209 | } 210 | return await this.getBalance(this.wallet.account.address); 211 | } 212 | 213 | public async getBalance(address: string): Promise { 214 | const validAddress = validateAddress(address); 215 | return await this.rpcClient.getBalance({ 216 | address: validAddress, 217 | }); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /packages/core-sdk/src/resources/ipAccount.ts: -------------------------------------------------------------------------------- 1 | import { Address, encodeFunctionData, Hex, PublicClient } from "viem"; 2 | 3 | import { 4 | coreMetadataModuleAbi, 5 | coreMetadataModuleAddress, 6 | Erc20Client, 7 | IpAccountImplClient, 8 | SimpleWalletClient, 9 | WrappedIpClient, 10 | } from "../abi/generated"; 11 | import { WIP_TOKEN_ADDRESS } from "../constants/common"; 12 | import { ChainIds } from "../types/config"; 13 | import { TransactionResponse } from "../types/options"; 14 | import { 15 | IPAccountExecuteRequest, 16 | IPAccountExecuteResponse, 17 | IPAccountExecuteWithSigRequest, 18 | IPAccountExecuteWithSigResponse, 19 | IpAccountStateResponse, 20 | SetIpMetadataRequest, 21 | TokenResponse, 22 | TransferErc20Request, 23 | } from "../types/resources/ipAccount"; 24 | import { handleError } from "../utils/errors"; 25 | import { waitForTxReceipt } from "../utils/txOptions"; 26 | import { validateAddress } from "../utils/utils"; 27 | 28 | export class IPAccountClient { 29 | public wrappedIpClient: WrappedIpClient; 30 | public erc20Client: Erc20Client; 31 | private readonly wallet: SimpleWalletClient; 32 | private readonly rpcClient: PublicClient; 33 | private readonly chainId: ChainIds; 34 | constructor(rpcClient: PublicClient, wallet: SimpleWalletClient, chainId: ChainIds) { 35 | this.wallet = wallet; 36 | this.rpcClient = rpcClient; 37 | this.chainId = chainId; 38 | this.wrappedIpClient = new WrappedIpClient(rpcClient, wallet); 39 | this.erc20Client = new Erc20Client(rpcClient, wallet); 40 | } 41 | 42 | /** 43 | * Executes a transaction from the IP Account. 44 | */ 45 | public async execute(request: IPAccountExecuteRequest): Promise { 46 | try { 47 | const ipAccountClient = new IpAccountImplClient( 48 | this.rpcClient, 49 | this.wallet, 50 | validateAddress(request.ipId), 51 | ); 52 | 53 | const req = { 54 | to: request.to, 55 | value: BigInt(0), 56 | data: request.data, 57 | }; 58 | 59 | if (request.txOptions?.encodedTxDataOnly) { 60 | return { encodedTxData: ipAccountClient.executeEncode({ ...req, operation: 0 }) }; 61 | } else { 62 | const txHash = await ipAccountClient.execute({ ...req, operation: 0 }); 63 | 64 | await this.rpcClient.waitForTransactionReceipt({ 65 | ...request.txOptions, 66 | hash: txHash, 67 | }); 68 | return { txHash: txHash }; 69 | } 70 | } catch (error) { 71 | return handleError(error, "Failed to execute the IP Account transaction"); 72 | } 73 | } 74 | 75 | /** 76 | * Executes a transaction from the IP Account with a signature. 77 | */ 78 | public async executeWithSig( 79 | request: IPAccountExecuteWithSigRequest, 80 | ): Promise { 81 | try { 82 | const ipAccountClient = new IpAccountImplClient( 83 | this.rpcClient, 84 | this.wallet, 85 | validateAddress(request.ipId), 86 | ); 87 | 88 | const req = { 89 | to: validateAddress(request.to), 90 | value: BigInt(request.value || 0), 91 | data: request.data, 92 | signer: validateAddress(request.signer), 93 | deadline: BigInt(request.deadline), 94 | signature: request.signature, 95 | }; 96 | if (request.txOptions?.encodedTxDataOnly) { 97 | return { encodedTxData: ipAccountClient.executeWithSigEncode(req) }; 98 | } else { 99 | const txHash = await ipAccountClient.executeWithSig(req); 100 | await this.rpcClient.waitForTransactionReceipt({ 101 | ...request.txOptions, 102 | hash: txHash, 103 | }); 104 | return { txHash: txHash }; 105 | } 106 | } catch (error) { 107 | return handleError(error, "Failed to execute with signature for the IP Account transaction"); 108 | } 109 | } 110 | 111 | /** 112 | * Returns the IPAccount's internal nonce for transaction ordering. 113 | */ 114 | public async getIpAccountNonce(ipId: Address): Promise { 115 | try { 116 | const ipAccount = new IpAccountImplClient(this.rpcClient, this.wallet, validateAddress(ipId)); 117 | const { result: state } = await ipAccount.state(); 118 | return state; 119 | } catch (error) { 120 | return handleError(error, "Failed to get the IP Account nonce"); 121 | } 122 | } 123 | 124 | /** 125 | * Returns the identifier of the non-fungible token which owns the account 126 | */ 127 | public async getToken(ipId: Address): Promise { 128 | try { 129 | const ipAccount = new IpAccountImplClient(this.rpcClient, this.wallet, validateAddress(ipId)); 130 | const [chainId, tokenContract, tokenId] = await ipAccount.token(); 131 | return { 132 | chainId, 133 | tokenContract, 134 | tokenId, 135 | }; 136 | } catch (error) { 137 | return handleError(error, "Failed to get the token"); 138 | } 139 | } 140 | 141 | /** 142 | * Sets the metadataURI for an IP asset. 143 | */ 144 | public async setIpMetadata({ 145 | ipId, 146 | metadataURI, 147 | metadataHash, 148 | txOptions, 149 | }: SetIpMetadataRequest): Promise { 150 | try { 151 | const data = encodeFunctionData({ 152 | abi: coreMetadataModuleAbi, 153 | functionName: "setMetadataURI", 154 | args: [validateAddress(ipId), metadataURI, metadataHash], 155 | }); 156 | const { txHash } = await this.execute({ 157 | ipId: ipId, 158 | to: coreMetadataModuleAddress[this.chainId], 159 | data: data, 160 | value: 0, 161 | txOptions: { 162 | ...txOptions, 163 | encodedTxDataOnly: false, 164 | }, 165 | }); 166 | return txHash!; 167 | } catch (error) { 168 | return handleError(error, "Failed to set the IP metadata"); 169 | } 170 | } 171 | 172 | /** 173 | * Transfers ERC20 tokens from the IP Account to the target address. 174 | */ 175 | public async transferErc20({ 176 | ipId, 177 | tokens, 178 | txOptions, 179 | }: TransferErc20Request): Promise { 180 | try { 181 | const ipAccount = new IpAccountImplClient(this.rpcClient, this.wallet, validateAddress(ipId)); 182 | const calls = tokens.map(({ address: token, target, amount }) => { 183 | let encodedData: Hex; 184 | if (validateAddress(token) === WIP_TOKEN_ADDRESS) { 185 | encodedData = this.wrappedIpClient.transferEncode({ 186 | to: validateAddress(target), 187 | amount: BigInt(amount), 188 | }).data; 189 | } else { 190 | encodedData = this.erc20Client.transferEncode({ 191 | to: validateAddress(target), 192 | value: BigInt(amount), 193 | }).data; 194 | } 195 | return { 196 | target: token, 197 | data: encodedData, 198 | value: 0n, 199 | }; 200 | }); 201 | const txHash = await ipAccount.executeBatch({ calls, operation: 0 }); 202 | return waitForTxReceipt({ 203 | txHash, 204 | txOptions, 205 | rpcClient: this.rpcClient, 206 | }); 207 | } catch (error) { 208 | return handleError(error, "Failed to transfer Erc20"); 209 | } 210 | } 211 | } 212 | --------------------------------------------------------------------------------