├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yaml │ ├── feature_request.md │ ├── task.md │ └── tracking_issue.md └── pull_request_template.md ├── .gitignore ├── .prettierrc ├── README.md ├── assets └── test-audio.mp3 ├── package.json ├── scripts ├── derivative │ ├── registerDerivativeCommercial.ts │ ├── registerDerivativeCommercialCustom.ts │ └── registerDerivativeNonCommercial.ts ├── dispute │ └── disputeIp.ts ├── licenses │ ├── mintLicense.ts │ └── oneTimeUseLicense.ts ├── misc │ └── sendRawTransaction.ts ├── registration │ ├── register.ts │ └── registerCustom.ts └── royalty │ ├── claimRevenue.ts │ ├── licenseRevenue.ts │ ├── payRevenue.ts │ └── transferRoyaltyTokens.ts ├── tsconfig.json └── utils ├── abi ├── defaultNftContractAbi.ts ├── licenseAttachmentWorkflowsAbi.ts └── totalLicenseTokenLimitHook.ts ├── config.ts ├── functions ├── createSpgNftCollection.ts ├── mintNFT.ts └── uploadToIpfs.ts └── utils.ts /.env.example: -------------------------------------------------------------------------------- 1 | # required 2 | WALLET_PRIVATE_KEY= 3 | PINATA_JWT= 4 | 5 | # all of the below are optional 6 | STORY_NETWORK= # 'aeneid' by default 7 | RPC_PROVIDER_URL= # uses public rpc provider by default 8 | NFT_CONTRACT_ADDRESS= 9 | SPG_NFT_CONTRACT_ADDRESS= -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | node_modules 4 | .yalc 5 | pnpm-lock.yaml 6 | yalc.lock 7 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 140 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Story TypeScript SDK Examples 2 | 3 | ## Get Started 4 | 5 | 1. Install the dependencies: 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | 2. Rename the `.env.example` file to `.env` 12 | 13 | 3. Add your Story Network Testnet wallet's private key to `.env` file: 14 | 15 | ``` 16 | WALLET_PRIVATE_KEY= 17 | ``` 18 | 19 | 4. [REQUIRED FOR `register` and `register-custom` SCRIPTS] Go to [Pinata](https://pinata.cloud/) and create a new API key. Add the JWT to your `.env` file: 20 | 21 | ``` 22 | PINATA_JWT= 23 | ``` 24 | 25 | 5. [OPTIONAL] We have already configured a public SPG NFT collection for you (`0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc`). If you want to create your own collection for your IPs, create a new SPG NFT collection by running `npm run create-spg-collection` in your terminal. 26 | 27 | 3a. Look at the console output, and copy the NFT contract address. Add that value as `SPG_NFT_CONTRACT_ADDRESS` to your `.env` file: 28 | 29 | ``` 30 | SPG_NFT_CONTRACT_ADDRESS= 31 | ``` 32 | 33 | **NOTE: You will only have to do this one time. Once you create an SPG collection, you can run this script as many times as you'd like.** 34 | 35 | ## Available Scripts 36 | 37 | Below are all of the available scripts to help you build on Story. 38 | 39 | ### Registration 40 | 41 | - `register`: This mints an NFT and registers it in the same transaction, using a public SPG collection. 42 | - `register-custom`: This mints an NFT using a custom ERC-721 contract and then registers it in a separate transaction. 43 | 44 | ### Licenses 45 | 46 | - `mint-license`: Mints a license token from an IP Asset. 47 | - `limit-license`: Registers a new IP and attaches license terms that only allow you to mint 1 license token. This is an example for limiting the amount of licenses you can mint. 48 | 49 | ### Royalty 50 | 51 | - `pay-revenue`: This is an example of registering a derivative, paying the derivative, and then allowing derivative and parent to claim their revenues. 52 | - `license-revenue`: This is an example of registering a derivative, minting a paid license from the derivative, and then allowing derivative and parent to claim their revenues. 53 | - `transfer-royalty-tokens`: This shows you how to transfer Royalty Tokens from an IP Account to any external wallet. Royalty Tokens are used to claim a % of revenue from an IP Asset. 54 | 55 | ### Derivative 56 | 57 | - `derivative-commercial`: This mints an NFT and registers it as a derivative of an IP Asset in the same transaction, using a public SPG collection. It costs 1 $WIP to register as derivative and also includes an example of the parent claiming its revenue. 58 | - `derivative-non-commercial`: This mints an NFT and registesr it as a derivative of an IP Asset in the same transaction, using a public SPG collection. It's free to register as derivative. 59 | - `derivative-commercial-custom`: This mints an NFT using a custom ERC-721 contract and then registers it as a derivative of an IP Asset in a separate transaction. It costs 1 $WIP to register as derivative and also includes an example of the parent claiming its revenue. 60 | 61 | ### Dispute 62 | 63 | - `dispute`: This disputes an IP Asset. 64 | 65 | ### Misc 66 | 67 | - `send-raw-transaction`: An example of sending a transaction using viem's `encodeFunctionData`. 68 | -------------------------------------------------------------------------------- /assets/test-audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyprotocol/typescript-tutorial/f55cc92f1b8ad7a8ef5e6f5a65f8124347efcad2/assets/test-audio.mp3 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-story-protocol-example-beta", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "register": "npx ts-node ./scripts/registration/register.ts", 9 | "register-custom": "npx ts-node ./scripts/registration/registerCustom.ts", 10 | "derivative-commercial": "npx ts-node ./scripts/derivative/registerDerivativeCommercial.ts", 11 | "derivative-non-commercial": "npx ts-node ./scripts/derivative/registerDerivativeNonCommercial.ts", 12 | "derivative-commercial-custom": "npx ts-node ./scripts/derivative/registerDerivativeCommercialCustom.ts", 13 | "dispute": "npx ts-node ./scripts/dispute/disputeIp.ts", 14 | "mint-license": "npx ts-node ./scripts/licenses/mintLicense.ts", 15 | "license-limit": "npx ts-node ./scripts/licenses/oneTimeUseLicense.ts", 16 | "pay-revenue": "npx ts-node ./scripts/royalty/payRevenue.ts", 17 | "license-revenue": "npx ts-node ./scripts/royalty/licenseRevenue.ts", 18 | "transfer-royalty-tokens": "npx ts-node ./scripts/royalty/transferRoyaltyTokens.ts", 19 | "claim-revenue": "npx ts-node ./scripts/royalty/claimRevenue.ts", 20 | "create-spg-collection": "npx ts-node ./utils/functions/createSpgNftCollection.ts", 21 | "send-raw-transaction": "npx ts-node ./scripts/misc/sendRawTransaction.ts" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@story-protocol/core-sdk": "1.3.1", 27 | "dotenv": "^16.4.7", 28 | "multiformats": "^9.9.0", 29 | "viem": "^2.8.12" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20.11.17", 33 | "prettier": "3.2.5", 34 | "ts-node": "^10.9.2", 35 | "typescript": "^5.8.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/derivative/registerDerivativeCommercial.ts: -------------------------------------------------------------------------------- 1 | import { Address, toHex } from 'viem' 2 | import { RoyaltyPolicyLRP, SPGNFTContractAddress } from '../../utils/utils' 3 | import { client } from '../../utils/config' 4 | import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 5 | 6 | // TODO: This is Ippy on Aeneid. The license terms specify 1 $WIP mint fee 7 | // and a 5% commercial rev share. You can change these. 8 | const PARENT_IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 9 | const PARENT_LICENSE_TERMS_ID: string = '96' 10 | 11 | const main = async function () { 12 | // 1. Mint and Register IP asset and make it a derivative of the parent IP Asset 13 | // 14 | // You will be paying for the License Token using $WIP: 15 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 16 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 17 | // $WIP for you. 18 | // 19 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#mintandregisteripandmakederivative 20 | const childIp = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ 21 | spgNftContract: SPGNFTContractAddress, 22 | derivData: { 23 | parentIpIds: [PARENT_IP_ID], 24 | licenseTermsIds: [PARENT_LICENSE_TERMS_ID], 25 | }, 26 | // NOTE: The below metadata is not configured properly. It is just to make things simple. 27 | // See `simpleMintAndRegister.ts` for a proper example. 28 | ipMetadata: { 29 | ipMetadataURI: 'test-uri', 30 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 31 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 32 | nftMetadataURI: 'test-nft-uri', 33 | }, 34 | txOptions: { waitForTransaction: true }, 35 | }) 36 | console.log('Derivative IPA created and linked:', { 37 | 'Transaction Hash': childIp.txHash, 38 | 'IPA ID': childIp.ipId, 39 | }) 40 | 41 | // 2. Parent Claim Revenue 42 | // 43 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 44 | const parentClaimRevenue = await client.royalty.claimAllRevenue({ 45 | ancestorIpId: PARENT_IP_ID, 46 | claimer: PARENT_IP_ID, 47 | childIpIds: [childIp.ipId as Address], 48 | royaltyPolicies: [RoyaltyPolicyLRP], 49 | currencyTokens: [WIP_TOKEN_ADDRESS], 50 | }) 51 | console.log('Parent claimed revenue receipt:', parentClaimRevenue) 52 | } 53 | 54 | main() 55 | -------------------------------------------------------------------------------- /scripts/derivative/registerDerivativeCommercialCustom.ts: -------------------------------------------------------------------------------- 1 | import { Address, toHex } from 'viem' 2 | import { mintNFT } from '../../utils/functions/mintNFT' 3 | import { NFTContractAddress, RoyaltyPolicyLRP } from '../../utils/utils' 4 | import { account, client } from '../../utils/config' 5 | import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 6 | 7 | // TODO: This is Ippy on Aeneid. The license terms specify 1 $WIP mint fee 8 | // and a 5% commercial rev share. You can change these. 9 | const PARENT_IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 10 | const PARENT_LICENSE_TERMS_ID: string = '96' 11 | 12 | const main = async function () { 13 | // 1. Register another (child) IP Asset 14 | // 15 | // You will be paying for the License Token using $WIP: 16 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 17 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 18 | // $WIP for you. 19 | // 20 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#registerderivativeip 21 | const childTokenId = await mintNFT(account.address, 'test-uri') 22 | const childIp = await client.ipAsset.registerDerivativeIp({ 23 | nftContract: NFTContractAddress, 24 | tokenId: childTokenId!, 25 | derivData: { 26 | parentIpIds: [PARENT_IP_ID], 27 | licenseTermsIds: [PARENT_LICENSE_TERMS_ID], 28 | }, 29 | // NOTE: The below metadata is not configured properly. It is just to make things simple. 30 | // See `simpleMintAndRegister.ts` for a proper example. 31 | ipMetadata: { 32 | ipMetadataURI: 'test-uri', 33 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 34 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 35 | nftMetadataURI: 'test-nft-uri', 36 | }, 37 | txOptions: { waitForTransaction: true }, 38 | }) 39 | console.log('Derivative IPA created:', { 40 | 'Transaction Hash': childIp.txHash, 41 | 'IPA ID': childIp.ipId, 42 | }) 43 | 44 | // 2. Parent Claim Revenue 45 | // 46 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 47 | const parentClaimRevenue = await client.royalty.claimAllRevenue({ 48 | ancestorIpId: PARENT_IP_ID, 49 | claimer: PARENT_IP_ID, 50 | childIpIds: [childIp.ipId as Address], 51 | royaltyPolicies: [RoyaltyPolicyLRP], 52 | currencyTokens: [WIP_TOKEN_ADDRESS], 53 | }) 54 | console.log('Parent claimed revenue receipt:', parentClaimRevenue) 55 | } 56 | 57 | main() 58 | -------------------------------------------------------------------------------- /scripts/derivative/registerDerivativeNonCommercial.ts: -------------------------------------------------------------------------------- 1 | import { Address, toHex } from 'viem' 2 | import { SPGNFTContractAddress, NonCommercialSocialRemixingTermsId } from '../../utils/utils' 3 | import { client } from '../../utils/config' 4 | 5 | // TODO: You can change this 6 | const PARENT_IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 7 | 8 | const main = async function () { 9 | // 1. Mint and Register IP asset and make it a derivative of the parent IP Asset 10 | // 11 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#mintandregisteripandmakederivative 12 | const childIp = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ 13 | spgNftContract: SPGNFTContractAddress, 14 | derivData: { 15 | parentIpIds: [PARENT_IP_ID], 16 | licenseTermsIds: [NonCommercialSocialRemixingTermsId], 17 | }, 18 | // NOTE: The below metadata is not configured properly. It is just to make things simple. 19 | // See `simpleMintAndRegister.ts` for a proper example. 20 | ipMetadata: { 21 | ipMetadataURI: 'test-uri', 22 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 23 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 24 | nftMetadataURI: 'test-nft-uri', 25 | }, 26 | txOptions: { waitForTransaction: true }, 27 | }) 28 | console.log('Derivative IPA created and linked:', { 29 | 'Transaction Hash': childIp.txHash, 30 | 'IPA ID': childIp.ipId, 31 | }) 32 | } 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /scripts/dispute/disputeIp.ts: -------------------------------------------------------------------------------- 1 | import { Address, parseEther } from 'viem' 2 | import { client } from '../../utils/config' 3 | import * as sha256 from 'multiformats/hashes/sha2' 4 | import { CID } from 'multiformats/cid' 5 | import { uploadTextToIPFS } from '../../utils/functions/uploadToIpfs' 6 | 7 | // TODO: Replace with your own IP ID and fill out your evidence 8 | const IP_ID: Address = '0x876B03d1e756C5C24D4b9A1080387098Fcc380f5' 9 | const EVIDENCE: string = 'Fill out your evidence here.' 10 | 11 | const main = async function () { 12 | const disputeHash = await uploadTextToIPFS(EVIDENCE) 13 | console.log(`Dispute evidence uploaded to IPFS: ${disputeHash}`) 14 | 15 | // Raise a Dispute 16 | // 17 | // Docs: https://docs.story.foundation/sdk-reference/dispute#raisedispute 18 | const disputeResponse = await client.dispute.raiseDispute({ 19 | targetIpId: IP_ID, 20 | cid: disputeHash, 21 | // you must pick from one of the whitelisted tags here: 22 | // https://docs.story.foundation/concepts/dispute-module/overview#dispute-tags 23 | targetTag: 'IMPROPER_REGISTRATION', 24 | bond: parseEther('0.1'), 25 | liveness: 2592000, // 30 days 26 | txOptions: { waitForTransaction: true }, 27 | }) 28 | console.log(`Dispute raised at transaction hash ${disputeResponse.txHash}, Dispute ID: ${disputeResponse.disputeId}`) 29 | } 30 | 31 | // example function you can use for testing purposes if you want 32 | const generateCID = async () => { 33 | // Generate a random 32-byte buffer 34 | const randomBytes = crypto.getRandomValues(new Uint8Array(32)) 35 | // Hash the bytes using SHA-256 36 | const hash = await sha256.sha256.digest(randomBytes) 37 | // Create a CIDv1 in dag-pb format 38 | const cidv1 = CID.createV1(0x70, hash) // 0x70 = dag-pb codec 39 | // Convert CIDv1 to CIDv0 (Base58-encoded) 40 | return cidv1.toV0().toString() 41 | } 42 | 43 | main() 44 | -------------------------------------------------------------------------------- /scripts/licenses/mintLicense.ts: -------------------------------------------------------------------------------- 1 | import { Address } from 'viem' 2 | import { client } from '../../utils/config' 3 | 4 | // TODO: Replace with your own IP ID and license terms id 5 | const IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 6 | const LICENSE_TERMS_ID: string = '1' 7 | 8 | const main = async function () { 9 | // 1. Mint License Tokens 10 | // 11 | // Docs: https://docs.story.foundation/sdk-reference/license#mintlicensetokens 12 | const response = await client.license.mintLicenseTokens({ 13 | licenseTermsId: LICENSE_TERMS_ID, 14 | licensorIpId: IP_ID, 15 | amount: 1, 16 | maxMintingFee: BigInt(0), // disabled 17 | maxRevenueShare: 100, // default 18 | txOptions: { waitForTransaction: true }, 19 | }) 20 | 21 | console.log('License minted:', { 22 | 'Transaction Hash': response.txHash, 23 | 'License Token IDs': response.licenseTokenIds, 24 | }) 25 | } 26 | 27 | main() 28 | -------------------------------------------------------------------------------- /scripts/licenses/oneTimeUseLicense.ts: -------------------------------------------------------------------------------- 1 | import { SPGNFTContractAddress, createCommercialRemixTerms } from '../../utils/utils' 2 | import { client, account, publicClient, walletClient } from '../../utils/config' 3 | import { toHex } from 'viem' 4 | import { LicensingConfig } from '@story-protocol/core-sdk' 5 | import { zeroAddress } from 'viem' 6 | import { totalLicenseTokenLimitHook } from '../../utils/abi/totalLicenseTokenLimitHook' 7 | 8 | const main = async function () { 9 | // 1. Set up Licensing Config 10 | // 11 | // Docs: https://docs.story.foundation/concepts/licensing-module/license-config-hook#license-config 12 | const licensingConfig: LicensingConfig = { 13 | isSet: true, 14 | mintingFee: 0n, 15 | // address of TotalLicenseTokenLimitHook 16 | // from https://docs.story.foundation/developers/deployed-smart-contracts 17 | licensingHook: '0xaBAD364Bfa41230272b08f171E0Ca939bD600478', 18 | hookData: zeroAddress, 19 | commercialRevShare: 0, 20 | disabled: false, 21 | expectMinimumGroupRewardShare: 0, 22 | expectGroupRewardPool: zeroAddress, 23 | } 24 | 25 | // 2. Mint and register IP with the licensing config 26 | // 27 | // Docs: https://docs.story.foundation/sdk-reference/ipasset#mintandregisteripassetwithpilterms 28 | const response = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ 29 | spgNftContract: SPGNFTContractAddress, 30 | licenseTermsData: [ 31 | { 32 | terms: createCommercialRemixTerms({ commercialRevShare: 0, defaultMintingFee: 0 }), 33 | // set the licensing config here 34 | licensingConfig, 35 | }, 36 | ], 37 | ipMetadata: { 38 | ipMetadataURI: 'test-uri', 39 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 40 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 41 | nftMetadataURI: 'test-nft-uri', 42 | }, 43 | txOptions: { waitForTransaction: true }, 44 | }) 45 | console.log('Root IPA created:', { 46 | 'Transaction Hash': response.txHash, 47 | 'IPA ID': response.ipId, 48 | 'License Term IDs': response.licenseTermsIds, 49 | }) 50 | 51 | // 3. Set Total License Token Limit 52 | const { request } = await publicClient.simulateContract({ 53 | // address of TotalLicenseTokenLimitHook 54 | // from https://docs.story.foundation/developers/deployed-smart-contracts 55 | address: '0xaBAD364Bfa41230272b08f171E0Ca939bD600478', 56 | abi: totalLicenseTokenLimitHook, 57 | functionName: 'setTotalLicenseTokenLimit', 58 | args: [ 59 | response.ipId, // licensorIpId 60 | '0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316', // licenseTemplate 61 | response.licenseTermsIds![0], // licenseTermsId 62 | 1n, // limit (as BigInt) 63 | ], 64 | account: account, // Specify the account to use for permission checking 65 | }) 66 | 67 | // Prepare transaction 68 | const hash = await walletClient.writeContract({ ...request, account: account }) 69 | 70 | // Wait for transaction to be mined 71 | const receipt = await publicClient.waitForTransactionReceipt({ 72 | hash, 73 | }) 74 | 75 | console.log('Total license token limit set:', { 76 | Receipt: receipt, 77 | }) 78 | } 79 | 80 | main() 81 | -------------------------------------------------------------------------------- /scripts/misc/sendRawTransaction.ts: -------------------------------------------------------------------------------- 1 | import { createCommercialRemixTerms, defaultLicensingConfig, SPGNFTContractAddress } from '../../utils/utils' 2 | import { account, client, networkInfo, walletClient } from '../../utils/config' 3 | import { uploadJSONToIPFS } from '../../utils/functions/uploadToIpfs' 4 | import { createHash } from 'crypto' 5 | import { IpMetadata } from '@story-protocol/core-sdk' 6 | import { encodeFunctionData } from 'viem' 7 | import { licenseAttachmentWorkflowsAbi } from '../../utils/abi/licenseAttachmentWorkflowsAbi' 8 | 9 | const main = async function () { 10 | const ipMetadata: IpMetadata = client.ipAsset.generateIpMetadata({ 11 | title: 'Midnight Marriage', 12 | description: 'This is a house-style song generated on suno.', 13 | createdAt: '1740005219', 14 | creators: [ 15 | { 16 | name: 'Jacob Tucker', 17 | address: '0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112', 18 | contributionPercent: 100, 19 | }, 20 | ], 21 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 22 | imageHash: '0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc', 23 | mediaUrl: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 24 | mediaHash: '0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee', 25 | mediaType: 'audio/mpeg', 26 | }) 27 | 28 | const nftMetadata = { 29 | name: 'Midnight Marriage', 30 | description: 'This is a house-style song generated on suno. This NFT represents ownership of the IP Asset.', 31 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 32 | animation_url: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 33 | attributes: [ 34 | { 35 | key: 'Suno Artist', 36 | value: 'amazedneurofunk956', 37 | }, 38 | { 39 | key: 'Artist ID', 40 | value: '4123743b-8ba6-4028-a965-75b79a3ad424', 41 | }, 42 | { 43 | key: 'Source', 44 | value: 'Suno.com', 45 | }, 46 | ], 47 | } 48 | 49 | const ipIpfsHash = await uploadJSONToIPFS(ipMetadata) 50 | const ipHash = createHash('sha256').update(JSON.stringify(ipMetadata)).digest('hex') 51 | const nftIpfsHash = await uploadJSONToIPFS(nftMetadata) 52 | const nftHash = createHash('sha256').update(JSON.stringify(nftMetadata)).digest('hex') 53 | 54 | const transactionRequest = { 55 | to: '0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8' as `0x${string}`, // example nft contract 56 | data: encodeFunctionData({ 57 | abi: licenseAttachmentWorkflowsAbi, // abi from another file 58 | functionName: 'mintAndRegisterIpAndAttachPILTerms', 59 | args: [ 60 | SPGNFTContractAddress, 61 | account.address, 62 | { 63 | ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`, 64 | ipMetadataHash: `0x${ipHash}`, 65 | nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`, 66 | nftMetadataHash: `0x${nftHash}`, 67 | }, 68 | [ 69 | { 70 | terms: createCommercialRemixTerms({ defaultMintingFee: 0, commercialRevShare: 0 }), 71 | licensingConfig: defaultLicensingConfig, 72 | }, 73 | ], 74 | true, 75 | ], 76 | }), 77 | } 78 | 79 | try { 80 | const txHash = await walletClient.sendTransaction({ 81 | ...transactionRequest, 82 | account, 83 | chain: networkInfo.chain, 84 | }) 85 | 86 | console.log(`Transaction sent: ${networkInfo.blockExplorer}/tx/${txHash}`) 87 | } catch (error) { 88 | console.error(error) 89 | } 90 | } 91 | 92 | main() 93 | -------------------------------------------------------------------------------- /scripts/registration/register.ts: -------------------------------------------------------------------------------- 1 | import { createCommercialRemixTerms, SPGNFTContractAddress } from '../../utils/utils' 2 | import { client, networkInfo } from '../../utils/config' 3 | import { uploadJSONToIPFS } from '../../utils/functions/uploadToIpfs' 4 | import { createHash } from 'crypto' 5 | import { IpMetadata } from '@story-protocol/core-sdk' 6 | 7 | const main = async function () { 8 | // 1. Set up your IP Metadata 9 | // 10 | // Docs: https://docs.story.foundation/concepts/ip-asset/ipa-metadata-standard 11 | const ipMetadata: IpMetadata = client.ipAsset.generateIpMetadata({ 12 | title: 'Midnight Marriage', 13 | description: 'This is a house-style song generated on suno.', 14 | createdAt: '1740005219', 15 | creators: [ 16 | { 17 | name: 'Jacob Tucker', 18 | address: '0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112', 19 | contributionPercent: 100, 20 | }, 21 | ], 22 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 23 | imageHash: '0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc', 24 | mediaUrl: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 25 | mediaHash: '0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee', 26 | mediaType: 'audio/mpeg', 27 | }) 28 | 29 | // 2. Set up your NFT Metadata 30 | // 31 | // Docs: https://docs.opensea.io/docs/metadata-standards#metadata-structure 32 | const nftMetadata = { 33 | name: 'Midnight Marriage', 34 | description: 'This is a house-style song generated on suno. This NFT represents ownership of the IP Asset.', 35 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 36 | animation_url: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 37 | attributes: [ 38 | { 39 | key: 'Suno Artist', 40 | value: 'amazedneurofunk956', 41 | }, 42 | { 43 | key: 'Artist ID', 44 | value: '4123743b-8ba6-4028-a965-75b79a3ad424', 45 | }, 46 | { 47 | key: 'Source', 48 | value: 'Suno.com', 49 | }, 50 | ], 51 | } 52 | 53 | // 3. Upload your IP and NFT Metadata to IPFS 54 | const ipIpfsHash = await uploadJSONToIPFS(ipMetadata) 55 | const ipHash = createHash('sha256').update(JSON.stringify(ipMetadata)).digest('hex') 56 | const nftIpfsHash = await uploadJSONToIPFS(nftMetadata) 57 | const nftHash = createHash('sha256').update(JSON.stringify(nftMetadata)).digest('hex') 58 | 59 | // 4. Register the NFT as an IP Asset 60 | // 61 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#mintandregisterip 62 | const response = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ 63 | spgNftContract: SPGNFTContractAddress, 64 | licenseTermsData: [ 65 | { 66 | terms: createCommercialRemixTerms({ defaultMintingFee: 1, commercialRevShare: 5 }), 67 | }, 68 | ], 69 | ipMetadata: { 70 | ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`, 71 | ipMetadataHash: `0x${ipHash}`, 72 | nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`, 73 | nftMetadataHash: `0x${nftHash}`, 74 | }, 75 | txOptions: { waitForTransaction: true }, 76 | }) 77 | console.log('Root IPA created:', { 78 | 'Transaction Hash': response.txHash, 79 | 'IPA ID': response.ipId, 80 | 'License Terms IDs': response.licenseTermsIds, 81 | }) 82 | console.log(`View on the explorer: ${networkInfo.protocolExplorer}/ipa/${response.ipId}`) 83 | } 84 | 85 | main() 86 | -------------------------------------------------------------------------------- /scripts/registration/registerCustom.ts: -------------------------------------------------------------------------------- 1 | import { mintNFT } from '../../utils/functions/mintNFT' 2 | import { createCommercialRemixTerms, NFTContractAddress } from '../../utils/utils' 3 | import { client, account, networkInfo } from '../../utils/config' 4 | import { uploadJSONToIPFS } from '../../utils/functions/uploadToIpfs' 5 | import { createHash } from 'crypto' 6 | import { IpMetadata } from '@story-protocol/core-sdk' 7 | 8 | const main = async function () { 9 | // 1. Set up your IP Metadata 10 | // 11 | // Docs: https://docs.story.foundation/concepts/ip-asset/ipa-metadata-standard 12 | const ipMetadata: IpMetadata = client.ipAsset.generateIpMetadata({ 13 | title: 'Midnight Marriage', 14 | description: 'This is a house-style song generated on suno.', 15 | createdAt: '1740005219', 16 | creators: [ 17 | { 18 | name: 'Jacob Tucker', 19 | address: '0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112', 20 | contributionPercent: 100, 21 | }, 22 | ], 23 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 24 | imageHash: '0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc', 25 | mediaUrl: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 26 | mediaHash: '0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee', 27 | mediaType: 'audio/mpeg', 28 | }) 29 | 30 | // 2. Set up your NFT Metadata 31 | // 32 | // Docs: https://docs.opensea.io/docs/metadata-standards#metadata-structure 33 | const nftMetadata = { 34 | name: 'Midnight Marriage', 35 | description: 'This is a house-style song generated on suno. This NFT represents ownership of the IP Asset.', 36 | image: 'https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg', 37 | animation_url: 'https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3', 38 | attributes: [ 39 | { 40 | key: 'Suno Artist', 41 | value: 'amazedneurofunk956', 42 | }, 43 | { 44 | key: 'Artist ID', 45 | value: '4123743b-8ba6-4028-a965-75b79a3ad424', 46 | }, 47 | { 48 | key: 'Source', 49 | value: 'Suno.com', 50 | }, 51 | ], 52 | } 53 | 54 | // 3. Upload your IP and NFT Metadata to IPFS 55 | const ipIpfsHash = await uploadJSONToIPFS(ipMetadata) 56 | const ipHash = createHash('sha256').update(JSON.stringify(ipMetadata)).digest('hex') 57 | const nftIpfsHash = await uploadJSONToIPFS(nftMetadata) 58 | const nftHash = createHash('sha256').update(JSON.stringify(nftMetadata)).digest('hex') 59 | 60 | // 4. Mint an NFT 61 | const tokenId = await mintNFT(account.address, `https://ipfs.io/ipfs/${nftIpfsHash}`) 62 | console.log(`NFT minted with tokenId ${tokenId}`) 63 | 64 | // 5. Register an IP Asset 65 | // 66 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#register 67 | const response = await client.ipAsset.registerIpAndAttachPilTerms({ 68 | nftContract: NFTContractAddress, 69 | tokenId: tokenId!, 70 | licenseTermsData: [ 71 | { 72 | terms: createCommercialRemixTerms({ defaultMintingFee: 1, commercialRevShare: 5 }), 73 | }, 74 | ], 75 | ipMetadata: { 76 | ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`, 77 | ipMetadataHash: `0x${ipHash}`, 78 | nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`, 79 | nftMetadataHash: `0x${nftHash}`, 80 | }, 81 | txOptions: { waitForTransaction: true }, 82 | }) 83 | console.log('Root IPA created:', { 84 | 'Transaction Hash': response.txHash, 85 | 'IPA ID': response.ipId, 86 | }) 87 | console.log(`View on the explorer: ${networkInfo.protocolExplorer}/ipa/${response.ipId}`) 88 | } 89 | 90 | main() 91 | -------------------------------------------------------------------------------- /scripts/royalty/claimRevenue.ts: -------------------------------------------------------------------------------- 1 | import { Address } from 'viem' 2 | import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 3 | import { client } from '../../utils/config' 4 | 5 | // TODO: You can change this. 6 | const IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 7 | 8 | const main = async function () { 9 | // 1. Claim Revenue 10 | // 11 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 12 | const response = await client.royalty.claimAllRevenue({ 13 | ancestorIpId: IP_ID, 14 | claimer: IP_ID, 15 | childIpIds: [], 16 | royaltyPolicies: [], 17 | currencyTokens: [WIP_TOKEN_ADDRESS], 18 | }) 19 | console.log('Claimed revenue:', response.claimedTokens) 20 | } 21 | 22 | main() 23 | -------------------------------------------------------------------------------- /scripts/royalty/licenseRevenue.ts: -------------------------------------------------------------------------------- 1 | import { Address, toHex } from 'viem' 2 | import { RoyaltyPolicyLRP, SPGNFTContractAddress } from '../../utils/utils' 3 | import { client } from '../../utils/config' 4 | import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 5 | 6 | // TODO: This is Ippy on Aeneid. The license terms specify 1 $WIP mint fee 7 | // and a 5% commercial rev share. You can change these. 8 | const PARENT_IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 9 | const LICENSE_TERMS_ID: string = '96' 10 | 11 | const main = async function () { 12 | // 1. Mint and Register IP asset and make it a derivative of the parent IP Asset 13 | // 14 | // You will be paying for the License Token using $WIP: 15 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 16 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 17 | // $WIP for you. 18 | // 19 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#mintandregisteripandmakederivative 20 | const childIp = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ 21 | spgNftContract: SPGNFTContractAddress, 22 | derivData: { 23 | parentIpIds: [PARENT_IP_ID], 24 | licenseTermsIds: [LICENSE_TERMS_ID], 25 | }, 26 | // NOTE: The below metadata is not configured properly. It is just to make things simple. 27 | // See `simpleMintAndRegister.ts` for a proper example. 28 | ipMetadata: { 29 | ipMetadataURI: 'test-uri', 30 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 31 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 32 | nftMetadataURI: 'test-nft-uri', 33 | }, 34 | txOptions: { waitForTransaction: true }, 35 | }) 36 | console.log('Derivative IPA created and linked:', { 37 | 'Transaction Hash': childIp.txHash, 38 | 'IPA ID': childIp.ipId, 39 | }) 40 | 41 | // 2. Mint license tokens from the child 42 | // 43 | // You will be paying for the License Token using $WIP: 44 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 45 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 46 | // $WIP for you. 47 | // 48 | // Docs: https://docs.story.foundation/sdk-reference/license#mintlicensetokens 49 | const mintTokens = await client.license.mintLicenseTokens({ 50 | licenseTermsId: LICENSE_TERMS_ID, 51 | licensorIpId: childIp.ipId as Address, 52 | amount: 1, 53 | maxMintingFee: BigInt(0), // disabled 54 | maxRevenueShare: 100, // default 55 | txOptions: { waitForTransaction: true }, 56 | }) 57 | console.log('Minted license from child:', { 58 | 'Transaction Hash': mintTokens.txHash, 59 | }) 60 | 61 | // 3. Child Claim Revenue 62 | // 63 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 64 | const childClaimRevenue = await client.royalty.claimAllRevenue({ 65 | ancestorIpId: childIp.ipId as Address, 66 | claimer: childIp.ipId as Address, 67 | childIpIds: [], 68 | royaltyPolicies: [], 69 | currencyTokens: [WIP_TOKEN_ADDRESS], 70 | }) 71 | console.log('Child claimed revenue:', childClaimRevenue.claimedTokens) 72 | 73 | // 4. Parent Claim Revenue 74 | // 75 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 76 | const parentClaimRevenue = await client.royalty.claimAllRevenue({ 77 | ancestorIpId: PARENT_IP_ID, 78 | claimer: PARENT_IP_ID, 79 | childIpIds: [childIp.ipId as Address], 80 | royaltyPolicies: [RoyaltyPolicyLRP], 81 | currencyTokens: [WIP_TOKEN_ADDRESS], 82 | }) 83 | console.log('Parent claimed revenue receipt:', parentClaimRevenue) 84 | } 85 | 86 | main() 87 | -------------------------------------------------------------------------------- /scripts/royalty/payRevenue.ts: -------------------------------------------------------------------------------- 1 | import { Address, parseEther, toHex, zeroAddress } from 'viem' 2 | import { RoyaltyPolicyLRP, SPGNFTContractAddress } from '../../utils/utils' 3 | import { client } from '../../utils/config' 4 | import { WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 5 | 6 | // TODO: This is Ippy on Aeneid. The license terms specify 1 $WIP mint fee 7 | // and a 5% commercial rev share. You can change these. 8 | const PARENT_IP_ID: Address = '0x641E638e8FCA4d4844F509630B34c9D524d40BE5' 9 | const LICENSE_TERMS_ID: string = '96' 10 | 11 | const main = async function () { 12 | // 1. Mint and Register IP asset and make it a derivative of the parent IP Asset 13 | // 14 | // You will be paying for the License Token using $WIP: 15 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 16 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 17 | // $WIP for you. 18 | // 19 | // Docs: https://docs.story.foundation/sdk-reference/ip-asset#mintandregisteripandmakederivative 20 | const childIp = await client.ipAsset.mintAndRegisterIpAndMakeDerivative({ 21 | spgNftContract: SPGNFTContractAddress, 22 | derivData: { 23 | parentIpIds: [PARENT_IP_ID], 24 | licenseTermsIds: [LICENSE_TERMS_ID], 25 | }, 26 | // NOTE: The below metadata is not configured properly. It is just to make things simple. 27 | // See `simpleMintAndRegister.ts` for a proper example. 28 | ipMetadata: { 29 | ipMetadataURI: 'test-uri', 30 | ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), 31 | nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), 32 | nftMetadataURI: 'test-nft-uri', 33 | }, 34 | txOptions: { waitForTransaction: true }, 35 | }) 36 | console.log('Derivative IPA created and linked:', { 37 | 'Transaction Hash': childIp.txHash, 38 | 'IPA ID': childIp.ipId, 39 | }) 40 | 41 | // 2. Pay Royalty 42 | // 43 | // You will be paying for this royalty using $WIP: 44 | // https://aeneid.storyscan.xyz/address/0x1514000000000000000000000000000000000000 45 | // If you don't have enough $WIP, the function will auto wrap an equivalent amount of $IP into 46 | // $WIP for you. 47 | // 48 | // Docs: https://docs.story.foundation/sdk-reference/royalty#payroyaltyonbehalf 49 | const payRoyalty = await client.royalty.payRoyaltyOnBehalf({ 50 | receiverIpId: childIp.ipId as Address, 51 | payerIpId: zeroAddress, 52 | token: WIP_TOKEN_ADDRESS, 53 | amount: parseEther('2'), // 2 $WIP 54 | txOptions: { waitForTransaction: true }, 55 | }) 56 | console.log('Paid royalty:', { 57 | 'Transaction Hash': payRoyalty.txHash, 58 | }) 59 | 60 | // 3. Child Claim Revenue 61 | // 62 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 63 | const childClaimRevenue = await client.royalty.claimAllRevenue({ 64 | ancestorIpId: childIp.ipId as Address, 65 | claimer: childIp.ipId as Address, 66 | childIpIds: [], 67 | royaltyPolicies: [], 68 | currencyTokens: [WIP_TOKEN_ADDRESS], 69 | }) 70 | console.log('Child claimed revenue:', childClaimRevenue.claimedTokens) 71 | 72 | // 4. Parent Claim Revenue 73 | // 74 | // Docs: https://docs.story.foundation/sdk-reference/royalty#claimallrevenue 75 | const parentClaimRevenue = await client.royalty.claimAllRevenue({ 76 | ancestorIpId: PARENT_IP_ID, 77 | claimer: PARENT_IP_ID, 78 | childIpIds: [childIp.ipId as Address], 79 | royaltyPolicies: [RoyaltyPolicyLRP], 80 | currencyTokens: [WIP_TOKEN_ADDRESS], 81 | }) 82 | console.log('Parent claimed revenue receipt:', parentClaimRevenue) 83 | } 84 | 85 | main() 86 | -------------------------------------------------------------------------------- /scripts/royalty/transferRoyaltyTokens.ts: -------------------------------------------------------------------------------- 1 | import { Address } from 'viem' 2 | import { convertRoyaltyPercentToTokens, createCommercialRemixTerms, SPGNFTContractAddress } from '../../utils/utils' 3 | import { account, client } from '../../utils/config' 4 | 5 | const main = async function () { 6 | // FOR SETUP: Create a new IP Asset we can use 7 | const parentIp = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ 8 | spgNftContract: SPGNFTContractAddress, 9 | licenseTermsData: [ 10 | { 11 | terms: createCommercialRemixTerms({ defaultMintingFee: 0, commercialRevShare: 0 }), 12 | }, 13 | ], 14 | txOptions: { waitForTransaction: true }, 15 | }) 16 | console.log('Parent IPA created:', { 17 | 'Transaction Hash': parentIp.txHash, 18 | 'IPA ID': parentIp.ipId, 19 | 'License Terms ID': parentIp.licenseTermsIds?.[0], 20 | }) 21 | 22 | // FOR SETUP: Mint a license token in order to trigger IP Royalty Vault deployment 23 | const mintLicense = await client.license.mintLicenseTokens({ 24 | licenseTermsId: parentIp.licenseTermsIds?.[0]!, 25 | licensorIpId: parentIp.ipId as Address, 26 | amount: 1, 27 | maxMintingFee: BigInt(0), // disabled 28 | maxRevenueShare: 100, // default 29 | txOptions: { waitForTransaction: true }, 30 | }) 31 | console.log('Minted license:', { 32 | 'Transaction Hash': mintLicense.txHash, 33 | 'License Token ID': mintLicense.licenseTokenIds?.[0], 34 | }) 35 | 36 | // Get the IP Royalty Vault Address 37 | // Note: This is equivalent to the currency address of the ERC-20 38 | // Royalty Tokens. 39 | const royaltyVaultAddress = await client.royalty.getRoyaltyVaultAddress(parentIp.ipId as Address) 40 | console.log('Royalty Vault Address:', royaltyVaultAddress) 41 | 42 | // Transfer the Royalty Tokens from the IP Account to the address 43 | // executing this transaction (you could use any other address as well) 44 | const transferRoyaltyTokens = await client.ipAccount.transferErc20({ 45 | ipId: parentIp.ipId as Address, 46 | tokens: [ 47 | { 48 | address: royaltyVaultAddress, 49 | amount: convertRoyaltyPercentToTokens(1), 50 | target: account.address, 51 | }, 52 | ], 53 | txOptions: { waitForTransaction: true }, 54 | }) 55 | console.log('Transferred royalty tokens:', { 'Transaction Hash': transferRoyaltyTokens.txHash }) 56 | } 57 | 58 | main() 59 | -------------------------------------------------------------------------------- /tsconfig.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 | "resolveJsonModule": true, 20 | "module": "CommonJS", 21 | "outDir": "dist" 22 | }, 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /utils/abi/defaultNftContractAbi.ts: -------------------------------------------------------------------------------- 1 | export const defaultNftContractAbi = [ 2 | { 3 | inputs: [], 4 | stateMutability: "nonpayable", 5 | type: "constructor", 6 | }, 7 | { 8 | inputs: [ 9 | { 10 | internalType: "address", 11 | name: "recipient", 12 | type: "address", 13 | }, 14 | { 15 | internalType: "string", 16 | name: "tokenURI", 17 | type: "string", 18 | }, 19 | ], 20 | name: "mintNFT", 21 | outputs: [ 22 | { 23 | internalType: "uint256", 24 | name: "", 25 | type: "uint256", 26 | }, 27 | ], 28 | stateMutability: "nonpayable", 29 | type: "function", 30 | }, 31 | { 32 | inputs: [ 33 | { 34 | internalType: "uint256", 35 | name: "tokenId", 36 | type: "uint256", 37 | }, 38 | ], 39 | name: "tokenURI", 40 | outputs: [ 41 | { 42 | internalType: "string", 43 | name: "", 44 | type: "string", 45 | }, 46 | ], 47 | stateMutability: "view", 48 | type: "function", 49 | }, 50 | { 51 | inputs: [ 52 | { 53 | internalType: "uint256", 54 | name: "tokenId", 55 | type: "uint256", 56 | }, 57 | ], 58 | name: "ownerOf", 59 | outputs: [ 60 | { 61 | internalType: "address", 62 | name: "", 63 | type: "address", 64 | }, 65 | ], 66 | stateMutability: "view", 67 | type: "function", 68 | }, 69 | { 70 | inputs: [], 71 | name: "symbol", 72 | outputs: [ 73 | { 74 | internalType: "string", 75 | name: "", 76 | type: "string", 77 | }, 78 | ], 79 | stateMutability: "view", 80 | type: "function", 81 | }, 82 | { 83 | inputs: [], 84 | name: "name", 85 | outputs: [ 86 | { 87 | internalType: "string", 88 | name: "", 89 | type: "string", 90 | }, 91 | ], 92 | stateMutability: "view", 93 | type: "function", 94 | }, 95 | { 96 | inputs: [], 97 | name: "totalSupply", 98 | outputs: [ 99 | { 100 | internalType: "uint256", 101 | name: "", 102 | type: "uint256", 103 | }, 104 | ], 105 | stateMutability: "view", 106 | type: "function", 107 | }, 108 | ]; 109 | -------------------------------------------------------------------------------- /utils/abi/licenseAttachmentWorkflowsAbi.ts: -------------------------------------------------------------------------------- 1 | export const licenseAttachmentWorkflowsAbi = [ 2 | { 3 | inputs: [ 4 | { internalType: 'address', name: 'accessController', type: 'address' }, 5 | { internalType: 'address', name: 'coreMetadataModule', type: 'address' }, 6 | { internalType: 'address', name: 'ipAssetRegistry', type: 'address' }, 7 | { internalType: 'address', name: 'licenseRegistry', type: 'address' }, 8 | { internalType: 'address', name: 'licensingModule', type: 'address' }, 9 | { internalType: 'address', name: 'pilTemplate', type: 'address' }, 10 | ], 11 | stateMutability: 'nonpayable', 12 | type: 'constructor', 13 | }, 14 | { inputs: [{ internalType: 'address', name: 'authority', type: 'address' }], name: 'AccessManagedInvalidAuthority', type: 'error' }, 15 | { 16 | inputs: [ 17 | { internalType: 'address', name: 'caller', type: 'address' }, 18 | { internalType: 'uint32', name: 'delay', type: 'uint32' }, 19 | ], 20 | name: 'AccessManagedRequiredDelay', 21 | type: 'error', 22 | }, 23 | { inputs: [{ internalType: 'address', name: 'caller', type: 'address' }], name: 'AccessManagedUnauthorized', type: 'error' }, 24 | { inputs: [{ internalType: 'address', name: 'target', type: 'address' }], name: 'AddressEmptyCode', type: 'error' }, 25 | { inputs: [{ internalType: 'address', name: 'implementation', type: 'address' }], name: 'ERC1967InvalidImplementation', type: 'error' }, 26 | { inputs: [], name: 'ERC1967NonPayable', type: 'error' }, 27 | { inputs: [], name: 'FailedCall', type: 'error' }, 28 | { inputs: [], name: 'InvalidInitialization', type: 'error' }, 29 | { 30 | inputs: [ 31 | { internalType: 'address', name: 'caller', type: 'address' }, 32 | { internalType: 'address', name: 'signer', type: 'address' }, 33 | ], 34 | name: 'LicenseAttachmentWorkflows__CallerNotSigner', 35 | type: 'error', 36 | }, 37 | { inputs: [], name: 'LicenseAttachmentWorkflows__NoLicenseTermsData', type: 'error' }, 38 | { inputs: [], name: 'LicenseAttachmentWorkflows__ZeroAddressParam', type: 'error' }, 39 | { inputs: [], name: 'NotInitializing', type: 'error' }, 40 | { inputs: [], name: 'PermissionHelper__ModulesAndSelectorsMismatch', type: 'error' }, 41 | { inputs: [], name: 'UUPSUnauthorizedCallContext', type: 'error' }, 42 | { inputs: [{ internalType: 'bytes32', name: 'slot', type: 'bytes32' }], name: 'UUPSUnsupportedProxiableUUID', type: 'error' }, 43 | { inputs: [], name: 'Workflow__CallerNotAuthorizedToMint', type: 'error' }, 44 | { 45 | anonymous: false, 46 | inputs: [{ indexed: false, internalType: 'address', name: 'authority', type: 'address' }], 47 | name: 'AuthorityUpdated', 48 | type: 'event', 49 | }, 50 | { 51 | anonymous: false, 52 | inputs: [{ indexed: false, internalType: 'uint64', name: 'version', type: 'uint64' }], 53 | name: 'Initialized', 54 | type: 'event', 55 | }, 56 | { 57 | anonymous: false, 58 | inputs: [{ indexed: true, internalType: 'address', name: 'implementation', type: 'address' }], 59 | name: 'Upgraded', 60 | type: 'event', 61 | }, 62 | { 63 | inputs: [], 64 | name: 'ACCESS_CONTROLLER', 65 | outputs: [{ internalType: 'contract IAccessController', name: '', type: 'address' }], 66 | stateMutability: 'view', 67 | type: 'function', 68 | }, 69 | { 70 | inputs: [], 71 | name: 'CORE_METADATA_MODULE', 72 | outputs: [{ internalType: 'contract ICoreMetadataModule', name: '', type: 'address' }], 73 | stateMutability: 'view', 74 | type: 'function', 75 | }, 76 | { 77 | inputs: [], 78 | name: 'IP_ASSET_REGISTRY', 79 | outputs: [{ internalType: 'contract IIPAssetRegistry', name: '', type: 'address' }], 80 | stateMutability: 'view', 81 | type: 'function', 82 | }, 83 | { 84 | inputs: [], 85 | name: 'LICENSE_REGISTRY', 86 | outputs: [{ internalType: 'contract ILicenseRegistry', name: '', type: 'address' }], 87 | stateMutability: 'view', 88 | type: 'function', 89 | }, 90 | { 91 | inputs: [], 92 | name: 'LICENSING_MODULE', 93 | outputs: [{ internalType: 'contract ILicensingModule', name: '', type: 'address' }], 94 | stateMutability: 'view', 95 | type: 'function', 96 | }, 97 | { 98 | inputs: [], 99 | name: 'PIL_TEMPLATE', 100 | outputs: [{ internalType: 'contract IPILicenseTemplate', name: '', type: 'address' }], 101 | stateMutability: 'view', 102 | type: 'function', 103 | }, 104 | { 105 | inputs: [], 106 | name: 'UPGRADE_INTERFACE_VERSION', 107 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 108 | stateMutability: 'view', 109 | type: 'function', 110 | }, 111 | { 112 | inputs: [], 113 | name: 'authority', 114 | outputs: [{ internalType: 'address', name: '', type: 'address' }], 115 | stateMutability: 'view', 116 | type: 'function', 117 | }, 118 | { 119 | inputs: [{ internalType: 'address', name: 'accessManager', type: 'address' }], 120 | name: 'initialize', 121 | outputs: [], 122 | stateMutability: 'nonpayable', 123 | type: 'function', 124 | }, 125 | { 126 | inputs: [], 127 | name: 'isConsumingScheduledOp', 128 | outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }], 129 | stateMutability: 'view', 130 | type: 'function', 131 | }, 132 | { 133 | inputs: [ 134 | { internalType: 'address', name: 'spgNftContract', type: 'address' }, 135 | { internalType: 'address', name: 'recipient', type: 'address' }, 136 | { 137 | components: [ 138 | { internalType: 'string', name: 'ipMetadataURI', type: 'string' }, 139 | { internalType: 'bytes32', name: 'ipMetadataHash', type: 'bytes32' }, 140 | { internalType: 'string', name: 'nftMetadataURI', type: 'string' }, 141 | { internalType: 'bytes32', name: 'nftMetadataHash', type: 'bytes32' }, 142 | ], 143 | internalType: 'struct WorkflowStructs.IPMetadata', 144 | name: 'ipMetadata', 145 | type: 'tuple', 146 | }, 147 | { internalType: 'bool', name: 'allowDuplicates', type: 'bool' }, 148 | ], 149 | name: 'mintAndRegisterIpAndAttachDefaultTerms', 150 | outputs: [ 151 | { internalType: 'address', name: 'ipId', type: 'address' }, 152 | { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, 153 | ], 154 | stateMutability: 'nonpayable', 155 | type: 'function', 156 | }, 157 | { 158 | inputs: [ 159 | { internalType: 'address', name: 'spgNftContract', type: 'address' }, 160 | { internalType: 'address', name: 'recipient', type: 'address' }, 161 | { 162 | components: [ 163 | { internalType: 'string', name: 'ipMetadataURI', type: 'string' }, 164 | { internalType: 'bytes32', name: 'ipMetadataHash', type: 'bytes32' }, 165 | { internalType: 'string', name: 'nftMetadataURI', type: 'string' }, 166 | { internalType: 'bytes32', name: 'nftMetadataHash', type: 'bytes32' }, 167 | ], 168 | internalType: 'struct WorkflowStructs.IPMetadata', 169 | name: 'ipMetadata', 170 | type: 'tuple', 171 | }, 172 | { 173 | components: [ 174 | { 175 | components: [ 176 | { internalType: 'bool', name: 'transferable', type: 'bool' }, 177 | { internalType: 'address', name: 'royaltyPolicy', type: 'address' }, 178 | { internalType: 'uint256', name: 'defaultMintingFee', type: 'uint256' }, 179 | { internalType: 'uint256', name: 'expiration', type: 'uint256' }, 180 | { internalType: 'bool', name: 'commercialUse', type: 'bool' }, 181 | { internalType: 'bool', name: 'commercialAttribution', type: 'bool' }, 182 | { internalType: 'address', name: 'commercializerChecker', type: 'address' }, 183 | { internalType: 'bytes', name: 'commercializerCheckerData', type: 'bytes' }, 184 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 185 | { internalType: 'uint256', name: 'commercialRevCeiling', type: 'uint256' }, 186 | { internalType: 'bool', name: 'derivativesAllowed', type: 'bool' }, 187 | { internalType: 'bool', name: 'derivativesAttribution', type: 'bool' }, 188 | { internalType: 'bool', name: 'derivativesApproval', type: 'bool' }, 189 | { internalType: 'bool', name: 'derivativesReciprocal', type: 'bool' }, 190 | { internalType: 'uint256', name: 'derivativeRevCeiling', type: 'uint256' }, 191 | { internalType: 'address', name: 'currency', type: 'address' }, 192 | { internalType: 'string', name: 'uri', type: 'string' }, 193 | ], 194 | internalType: 'struct PILTerms', 195 | name: 'terms', 196 | type: 'tuple', 197 | }, 198 | { 199 | components: [ 200 | { internalType: 'bool', name: 'isSet', type: 'bool' }, 201 | { internalType: 'uint256', name: 'mintingFee', type: 'uint256' }, 202 | { internalType: 'address', name: 'licensingHook', type: 'address' }, 203 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 204 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 205 | { internalType: 'bool', name: 'disabled', type: 'bool' }, 206 | { internalType: 'uint32', name: 'expectMinimumGroupRewardShare', type: 'uint32' }, 207 | { internalType: 'address', name: 'expectGroupRewardPool', type: 'address' }, 208 | ], 209 | internalType: 'struct Licensing.LicensingConfig', 210 | name: 'licensingConfig', 211 | type: 'tuple', 212 | }, 213 | ], 214 | internalType: 'struct WorkflowStructs.LicenseTermsData[]', 215 | name: 'licenseTermsData', 216 | type: 'tuple[]', 217 | }, 218 | { internalType: 'bool', name: 'allowDuplicates', type: 'bool' }, 219 | ], 220 | name: 'mintAndRegisterIpAndAttachPILTerms', 221 | outputs: [ 222 | { internalType: 'address', name: 'ipId', type: 'address' }, 223 | { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, 224 | { internalType: 'uint256[]', name: 'licenseTermsIds', type: 'uint256[]' }, 225 | ], 226 | stateMutability: 'nonpayable', 227 | type: 'function', 228 | }, 229 | { 230 | inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }], 231 | name: 'multicall', 232 | outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }], 233 | stateMutability: 'nonpayable', 234 | type: 'function', 235 | }, 236 | { 237 | inputs: [ 238 | { internalType: 'address', name: '', type: 'address' }, 239 | { internalType: 'address', name: '', type: 'address' }, 240 | { internalType: 'uint256', name: '', type: 'uint256' }, 241 | { internalType: 'bytes', name: '', type: 'bytes' }, 242 | ], 243 | name: 'onERC721Received', 244 | outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }], 245 | stateMutability: 'nonpayable', 246 | type: 'function', 247 | }, 248 | { 249 | inputs: [], 250 | name: 'proxiableUUID', 251 | outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 252 | stateMutability: 'view', 253 | type: 'function', 254 | }, 255 | { 256 | inputs: [ 257 | { internalType: 'address', name: 'nftContract', type: 'address' }, 258 | { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, 259 | { 260 | components: [ 261 | { internalType: 'string', name: 'ipMetadataURI', type: 'string' }, 262 | { internalType: 'bytes32', name: 'ipMetadataHash', type: 'bytes32' }, 263 | { internalType: 'string', name: 'nftMetadataURI', type: 'string' }, 264 | { internalType: 'bytes32', name: 'nftMetadataHash', type: 'bytes32' }, 265 | ], 266 | internalType: 'struct WorkflowStructs.IPMetadata', 267 | name: 'ipMetadata', 268 | type: 'tuple', 269 | }, 270 | { 271 | components: [ 272 | { internalType: 'address', name: 'signer', type: 'address' }, 273 | { internalType: 'uint256', name: 'deadline', type: 'uint256' }, 274 | { internalType: 'bytes', name: 'signature', type: 'bytes' }, 275 | ], 276 | internalType: 'struct WorkflowStructs.SignatureData', 277 | name: 'sigMetadataAndDefaultTerms', 278 | type: 'tuple', 279 | }, 280 | ], 281 | name: 'registerIpAndAttachDefaultTerms', 282 | outputs: [{ internalType: 'address', name: 'ipId', type: 'address' }], 283 | stateMutability: 'nonpayable', 284 | type: 'function', 285 | }, 286 | { 287 | inputs: [ 288 | { internalType: 'address', name: 'nftContract', type: 'address' }, 289 | { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, 290 | { 291 | components: [ 292 | { internalType: 'string', name: 'ipMetadataURI', type: 'string' }, 293 | { internalType: 'bytes32', name: 'ipMetadataHash', type: 'bytes32' }, 294 | { internalType: 'string', name: 'nftMetadataURI', type: 'string' }, 295 | { internalType: 'bytes32', name: 'nftMetadataHash', type: 'bytes32' }, 296 | ], 297 | internalType: 'struct WorkflowStructs.IPMetadata', 298 | name: 'ipMetadata', 299 | type: 'tuple', 300 | }, 301 | { 302 | components: [ 303 | { 304 | components: [ 305 | { internalType: 'bool', name: 'transferable', type: 'bool' }, 306 | { internalType: 'address', name: 'royaltyPolicy', type: 'address' }, 307 | { internalType: 'uint256', name: 'defaultMintingFee', type: 'uint256' }, 308 | { internalType: 'uint256', name: 'expiration', type: 'uint256' }, 309 | { internalType: 'bool', name: 'commercialUse', type: 'bool' }, 310 | { internalType: 'bool', name: 'commercialAttribution', type: 'bool' }, 311 | { internalType: 'address', name: 'commercializerChecker', type: 'address' }, 312 | { internalType: 'bytes', name: 'commercializerCheckerData', type: 'bytes' }, 313 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 314 | { internalType: 'uint256', name: 'commercialRevCeiling', type: 'uint256' }, 315 | { internalType: 'bool', name: 'derivativesAllowed', type: 'bool' }, 316 | { internalType: 'bool', name: 'derivativesAttribution', type: 'bool' }, 317 | { internalType: 'bool', name: 'derivativesApproval', type: 'bool' }, 318 | { internalType: 'bool', name: 'derivativesReciprocal', type: 'bool' }, 319 | { internalType: 'uint256', name: 'derivativeRevCeiling', type: 'uint256' }, 320 | { internalType: 'address', name: 'currency', type: 'address' }, 321 | { internalType: 'string', name: 'uri', type: 'string' }, 322 | ], 323 | internalType: 'struct PILTerms', 324 | name: 'terms', 325 | type: 'tuple', 326 | }, 327 | { 328 | components: [ 329 | { internalType: 'bool', name: 'isSet', type: 'bool' }, 330 | { internalType: 'uint256', name: 'mintingFee', type: 'uint256' }, 331 | { internalType: 'address', name: 'licensingHook', type: 'address' }, 332 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 333 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 334 | { internalType: 'bool', name: 'disabled', type: 'bool' }, 335 | { internalType: 'uint32', name: 'expectMinimumGroupRewardShare', type: 'uint32' }, 336 | { internalType: 'address', name: 'expectGroupRewardPool', type: 'address' }, 337 | ], 338 | internalType: 'struct Licensing.LicensingConfig', 339 | name: 'licensingConfig', 340 | type: 'tuple', 341 | }, 342 | ], 343 | internalType: 'struct WorkflowStructs.LicenseTermsData[]', 344 | name: 'licenseTermsData', 345 | type: 'tuple[]', 346 | }, 347 | { 348 | components: [ 349 | { internalType: 'address', name: 'signer', type: 'address' }, 350 | { internalType: 'uint256', name: 'deadline', type: 'uint256' }, 351 | { internalType: 'bytes', name: 'signature', type: 'bytes' }, 352 | ], 353 | internalType: 'struct WorkflowStructs.SignatureData', 354 | name: 'sigMetadataAndAttachAndConfig', 355 | type: 'tuple', 356 | }, 357 | ], 358 | name: 'registerIpAndAttachPILTerms', 359 | outputs: [ 360 | { internalType: 'address', name: 'ipId', type: 'address' }, 361 | { internalType: 'uint256[]', name: 'licenseTermsIds', type: 'uint256[]' }, 362 | ], 363 | stateMutability: 'nonpayable', 364 | type: 'function', 365 | }, 366 | { 367 | inputs: [ 368 | { internalType: 'address', name: 'ipId', type: 'address' }, 369 | { 370 | components: [ 371 | { 372 | components: [ 373 | { internalType: 'bool', name: 'transferable', type: 'bool' }, 374 | { internalType: 'address', name: 'royaltyPolicy', type: 'address' }, 375 | { internalType: 'uint256', name: 'defaultMintingFee', type: 'uint256' }, 376 | { internalType: 'uint256', name: 'expiration', type: 'uint256' }, 377 | { internalType: 'bool', name: 'commercialUse', type: 'bool' }, 378 | { internalType: 'bool', name: 'commercialAttribution', type: 'bool' }, 379 | { internalType: 'address', name: 'commercializerChecker', type: 'address' }, 380 | { internalType: 'bytes', name: 'commercializerCheckerData', type: 'bytes' }, 381 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 382 | { internalType: 'uint256', name: 'commercialRevCeiling', type: 'uint256' }, 383 | { internalType: 'bool', name: 'derivativesAllowed', type: 'bool' }, 384 | { internalType: 'bool', name: 'derivativesAttribution', type: 'bool' }, 385 | { internalType: 'bool', name: 'derivativesApproval', type: 'bool' }, 386 | { internalType: 'bool', name: 'derivativesReciprocal', type: 'bool' }, 387 | { internalType: 'uint256', name: 'derivativeRevCeiling', type: 'uint256' }, 388 | { internalType: 'address', name: 'currency', type: 'address' }, 389 | { internalType: 'string', name: 'uri', type: 'string' }, 390 | ], 391 | internalType: 'struct PILTerms', 392 | name: 'terms', 393 | type: 'tuple', 394 | }, 395 | { 396 | components: [ 397 | { internalType: 'bool', name: 'isSet', type: 'bool' }, 398 | { internalType: 'uint256', name: 'mintingFee', type: 'uint256' }, 399 | { internalType: 'address', name: 'licensingHook', type: 'address' }, 400 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 401 | { internalType: 'uint32', name: 'commercialRevShare', type: 'uint32' }, 402 | { internalType: 'bool', name: 'disabled', type: 'bool' }, 403 | { internalType: 'uint32', name: 'expectMinimumGroupRewardShare', type: 'uint32' }, 404 | { internalType: 'address', name: 'expectGroupRewardPool', type: 'address' }, 405 | ], 406 | internalType: 'struct Licensing.LicensingConfig', 407 | name: 'licensingConfig', 408 | type: 'tuple', 409 | }, 410 | ], 411 | internalType: 'struct WorkflowStructs.LicenseTermsData[]', 412 | name: 'licenseTermsData', 413 | type: 'tuple[]', 414 | }, 415 | { 416 | components: [ 417 | { internalType: 'address', name: 'signer', type: 'address' }, 418 | { internalType: 'uint256', name: 'deadline', type: 'uint256' }, 419 | { internalType: 'bytes', name: 'signature', type: 'bytes' }, 420 | ], 421 | internalType: 'struct WorkflowStructs.SignatureData', 422 | name: 'sigAttachAndConfig', 423 | type: 'tuple', 424 | }, 425 | ], 426 | name: 'registerPILTermsAndAttach', 427 | outputs: [{ internalType: 'uint256[]', name: 'licenseTermsIds', type: 'uint256[]' }], 428 | stateMutability: 'nonpayable', 429 | type: 'function', 430 | }, 431 | { 432 | inputs: [{ internalType: 'address', name: 'newAuthority', type: 'address' }], 433 | name: 'setAuthority', 434 | outputs: [], 435 | stateMutability: 'nonpayable', 436 | type: 'function', 437 | }, 438 | { 439 | inputs: [ 440 | { internalType: 'address', name: 'newImplementation', type: 'address' }, 441 | { internalType: 'bytes', name: 'data', type: 'bytes' }, 442 | ], 443 | name: 'upgradeToAndCall', 444 | outputs: [], 445 | stateMutability: 'payable', 446 | type: 'function', 447 | }, 448 | ] 449 | -------------------------------------------------------------------------------- /utils/abi/totalLicenseTokenLimitHook.ts: -------------------------------------------------------------------------------- 1 | export const totalLicenseTokenLimitHook = [ 2 | { 3 | inputs: [ 4 | { internalType: 'address', name: 'licenseRegistry', type: 'address' }, 5 | { internalType: 'address', name: 'licenseToken', type: 'address' }, 6 | { internalType: 'address', name: 'accessController', type: 'address' }, 7 | { internalType: 'address', name: 'ipAssetRegistry', type: 'address' }, 8 | ], 9 | stateMutability: 'nonpayable', 10 | type: 'constructor', 11 | }, 12 | { inputs: [{ internalType: 'address', name: 'ipAccount', type: 'address' }], name: 'AccessControlled__NotIpAccount', type: 'error' }, 13 | { inputs: [], name: 'AccessControlled__ZeroAddress', type: 'error' }, 14 | { 15 | inputs: [ 16 | { internalType: 'uint256', name: 'totalSupply', type: 'uint256' }, 17 | { internalType: 'uint256', name: 'limit', type: 'uint256' }, 18 | ], 19 | name: 'TotalLicenseTokenLimitHook_LimitLowerThanTotalSupply', 20 | type: 'error', 21 | }, 22 | { 23 | inputs: [ 24 | { internalType: 'uint256', name: 'totalSupply', type: 'uint256' }, 25 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 26 | { internalType: 'uint256', name: 'limit', type: 'uint256' }, 27 | ], 28 | name: 'TotalLicenseTokenLimitHook_TotalLicenseTokenLimitExceeded', 29 | type: 'error', 30 | }, 31 | { inputs: [], name: 'TotalLicenseTokenLimitHook_ZeroLicenseRegistry', type: 'error' }, 32 | { inputs: [], name: 'TotalLicenseTokenLimitHook_ZeroLicenseToken', type: 'error' }, 33 | { 34 | anonymous: false, 35 | inputs: [ 36 | { indexed: true, internalType: 'address', name: 'licensorIpId', type: 'address' }, 37 | { indexed: true, internalType: 'address', name: 'licenseTemplate', type: 'address' }, 38 | { indexed: true, internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 39 | { indexed: false, internalType: 'uint256', name: 'limit', type: 'uint256' }, 40 | ], 41 | name: 'SetTotalLicenseTokenLimit', 42 | type: 'event', 43 | }, 44 | { 45 | inputs: [], 46 | name: 'ACCESS_CONTROLLER', 47 | outputs: [{ internalType: 'contract IAccessController', name: '', type: 'address' }], 48 | stateMutability: 'view', 49 | type: 'function', 50 | }, 51 | { 52 | inputs: [], 53 | name: 'IP_ASSET_REGISTRY', 54 | outputs: [{ internalType: 'contract IIPAssetRegistry', name: '', type: 'address' }], 55 | stateMutability: 'view', 56 | type: 'function', 57 | }, 58 | { 59 | inputs: [], 60 | name: 'LICENSE_REGISTRY', 61 | outputs: [{ internalType: 'contract ILicenseRegistry', name: '', type: 'address' }], 62 | stateMutability: 'view', 63 | type: 'function', 64 | }, 65 | { 66 | inputs: [], 67 | name: 'LICENSE_TOKEN', 68 | outputs: [{ internalType: 'contract ILicenseToken', name: '', type: 'address' }], 69 | stateMutability: 'view', 70 | type: 'function', 71 | }, 72 | { 73 | inputs: [ 74 | { internalType: 'address', name: 'caller', type: 'address' }, 75 | { internalType: 'address', name: 'licensorIpId', type: 'address' }, 76 | { internalType: 'address', name: 'licenseTemplate', type: 'address' }, 77 | { internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 78 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 79 | { internalType: 'address', name: 'receiver', type: 'address' }, 80 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 81 | ], 82 | name: 'beforeMintLicenseTokens', 83 | outputs: [{ internalType: 'uint256', name: 'totalMintingFee', type: 'uint256' }], 84 | stateMutability: 'nonpayable', 85 | type: 'function', 86 | }, 87 | { 88 | inputs: [ 89 | { internalType: 'address', name: 'caller', type: 'address' }, 90 | { internalType: 'address', name: 'childIpId', type: 'address' }, 91 | { internalType: 'address', name: 'parentIpId', type: 'address' }, 92 | { internalType: 'address', name: 'licenseTemplate', type: 'address' }, 93 | { internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 94 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 95 | ], 96 | name: 'beforeRegisterDerivative', 97 | outputs: [{ internalType: 'uint256', name: 'mintingFee', type: 'uint256' }], 98 | stateMutability: 'nonpayable', 99 | type: 'function', 100 | }, 101 | { 102 | inputs: [ 103 | { internalType: 'address', name: 'caller', type: 'address' }, 104 | { internalType: 'address', name: 'licensorIpId', type: 'address' }, 105 | { internalType: 'address', name: 'licenseTemplate', type: 'address' }, 106 | { internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 107 | { internalType: 'uint256', name: 'amount', type: 'uint256' }, 108 | { internalType: 'address', name: 'receiver', type: 'address' }, 109 | { internalType: 'bytes', name: 'hookData', type: 'bytes' }, 110 | ], 111 | name: 'calculateMintingFee', 112 | outputs: [{ internalType: 'uint256', name: 'totalMintingFee', type: 'uint256' }], 113 | stateMutability: 'view', 114 | type: 'function', 115 | }, 116 | { 117 | inputs: [ 118 | { internalType: 'address', name: 'licensorIpId', type: 'address' }, 119 | { internalType: 'address', name: 'licenseTemplate', type: 'address' }, 120 | { internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 121 | ], 122 | name: 'getTotalLicenseTokenLimit', 123 | outputs: [{ internalType: 'uint256', name: 'limit', type: 'uint256' }], 124 | stateMutability: 'view', 125 | type: 'function', 126 | }, 127 | { 128 | inputs: [], 129 | name: 'name', 130 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 131 | stateMutability: 'view', 132 | type: 'function', 133 | }, 134 | { 135 | inputs: [ 136 | { internalType: 'address', name: 'licensorIpId', type: 'address' }, 137 | { internalType: 'address', name: 'licenseTemplate', type: 'address' }, 138 | { internalType: 'uint256', name: 'licenseTermsId', type: 'uint256' }, 139 | { internalType: 'uint256', name: 'limit', type: 'uint256' }, 140 | ], 141 | name: 'setTotalLicenseTokenLimit', 142 | outputs: [], 143 | stateMutability: 'nonpayable', 144 | type: 'function', 145 | }, 146 | { 147 | inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], 148 | name: 'supportsInterface', 149 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 150 | stateMutability: 'view', 151 | type: 'function', 152 | }, 153 | { 154 | inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 155 | name: 'totalLicenseTokenLimit', 156 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 157 | stateMutability: 'view', 158 | type: 'function', 159 | }, 160 | ] 161 | -------------------------------------------------------------------------------- /utils/config.ts: -------------------------------------------------------------------------------- 1 | import { aeneid, mainnet, StoryClient, StoryConfig } from '@story-protocol/core-sdk' 2 | import { Chain, createPublicClient, createWalletClient, http, WalletClient } from 'viem' 3 | import { privateKeyToAccount, Address, Account } from 'viem/accounts' 4 | import dotenv from 'dotenv' 5 | 6 | dotenv.config() 7 | 8 | // Network configuration types 9 | type NetworkType = 'aeneid' | 'mainnet' 10 | 11 | interface NetworkConfig { 12 | rpcProviderUrl: string 13 | blockExplorer: string 14 | protocolExplorer: string 15 | defaultNFTContractAddress: Address | null 16 | defaultSPGNFTContractAddress: Address | null 17 | chain: Chain 18 | } 19 | 20 | // Network configurations 21 | const networkConfigs: Record = { 22 | aeneid: { 23 | rpcProviderUrl: 'https://aeneid.storyrpc.io', 24 | blockExplorer: 'https://aeneid.storyscan.io', 25 | protocolExplorer: 'https://aeneid.explorer.story.foundation', 26 | defaultNFTContractAddress: '0x937bef10ba6fb941ed84b8d249abc76031429a9a' as Address, 27 | defaultSPGNFTContractAddress: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc' as Address, 28 | chain: aeneid, 29 | }, 30 | mainnet: { 31 | rpcProviderUrl: 'https://mainnet.storyrpc.io', 32 | blockExplorer: 'https://storyscan.io', 33 | protocolExplorer: 'https://explorer.story.foundation', 34 | defaultNFTContractAddress: null, 35 | defaultSPGNFTContractAddress: '0x98971c660ac20880b60F86Cc3113eBd979eb3aAE' as Address, 36 | chain: mainnet, 37 | }, 38 | } as const 39 | 40 | const getNetwork = (): NetworkType => { 41 | const network = process.env.STORY_NETWORK as NetworkType 42 | if (network && !(network in networkConfigs)) { 43 | throw new Error(`Invalid network: ${network}. Must be one of: ${Object.keys(networkConfigs).join(', ')}`) 44 | } 45 | return network || 'aeneid' 46 | } 47 | 48 | // Initialize client configuration 49 | export const network = getNetwork() 50 | 51 | export const networkInfo = { 52 | ...networkConfigs[network], 53 | rpcProviderUrl: process.env.RPC_PROVIDER_URL || networkConfigs[network].rpcProviderUrl, 54 | } 55 | 56 | export const account: Account = privateKeyToAccount(`0x${process.env.WALLET_PRIVATE_KEY}` as Address) 57 | 58 | const config: StoryConfig = { 59 | account, 60 | transport: http(networkInfo.rpcProviderUrl), 61 | chainId: network, 62 | } 63 | 64 | export const client = StoryClient.newClient(config) 65 | 66 | const baseConfig = { 67 | chain: networkInfo.chain, 68 | transport: http(networkInfo.rpcProviderUrl), 69 | } as const 70 | export const publicClient = createPublicClient(baseConfig) 71 | export const walletClient = createWalletClient({ 72 | ...baseConfig, 73 | account, 74 | }) as WalletClient 75 | -------------------------------------------------------------------------------- /utils/functions/createSpgNftCollection.ts: -------------------------------------------------------------------------------- 1 | import { zeroAddress } from 'viem' 2 | import { client } from '../config' 3 | 4 | const main = async function () { 5 | // Create a new SPG NFT collection 6 | // 7 | // NOTE: Use this code to create a new SPG NFT collection. You can then use the 8 | // `newCollection.spgNftContract` address as the `spgNftContract` argument in 9 | // functions like `mintAndRegisterIpAssetWithPilTerms` in the 10 | // `simpleMintAndRegisterSpg.ts` file. 11 | // 12 | // You will mostly only have to do this once. Once you get your nft contract address, 13 | // you can use it in SPG functions. 14 | // 15 | const newCollection = await client.nftClient.createNFTCollection({ 16 | name: 'Test NFTs', 17 | symbol: 'TEST', 18 | isPublicMinting: true, 19 | mintOpen: true, 20 | mintFeeRecipient: zeroAddress, 21 | contractURI: '', 22 | txOptions: { waitForTransaction: true }, 23 | }) 24 | 25 | console.log('New collection created:', { 26 | 'SPG NFT Contract Address': newCollection.spgNftContract, 27 | 'Transaction Hash': newCollection.txHash, 28 | }) 29 | } 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /utils/functions/mintNFT.ts: -------------------------------------------------------------------------------- 1 | import { Address } from 'viem' 2 | import { NFTContractAddress } from '../utils' 3 | import { publicClient, walletClient, account } from '../config' 4 | import { defaultNftContractAbi } from '../abi/defaultNftContractAbi' 5 | 6 | export async function mintNFT(to: Address, uri: string): Promise { 7 | console.log('Minting a new NFT...') 8 | 9 | const { request } = await publicClient.simulateContract({ 10 | address: NFTContractAddress, 11 | functionName: 'mintNFT', 12 | args: [to, uri], 13 | abi: defaultNftContractAbi, 14 | }) 15 | const hash = await walletClient.writeContract({ ...request, account: account }) 16 | const { logs } = await publicClient.waitForTransactionReceipt({ 17 | hash, 18 | }) 19 | if (logs[0].topics[3]) { 20 | return parseInt(logs[0].topics[3], 16) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /utils/functions/uploadToIpfs.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import FormData from 'form-data' 3 | 4 | export async function uploadJSONToIPFS(jsonMetadata: any): Promise { 5 | const url = 'https://api.pinata.cloud/pinning/pinJSONToIPFS' 6 | const options = { 7 | method: 'POST', 8 | headers: { 9 | Authorization: `Bearer ${process.env.PINATA_JWT}`, 10 | 'Content-Type': 'application/json', 11 | }, 12 | data: { 13 | pinataOptions: { cidVersion: 0 }, 14 | pinataMetadata: { name: 'ip-metadata.json' }, 15 | pinataContent: jsonMetadata, 16 | }, 17 | } 18 | 19 | try { 20 | const response = await axios(url, options) 21 | return response.data.IpfsHash 22 | } catch (error) { 23 | console.error('Error uploading JSON to IPFS:', error) 24 | throw error 25 | } 26 | } 27 | 28 | export async function uploadTextToIPFS(text: string): Promise { 29 | const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS' 30 | const data = new FormData() 31 | const buffer = Buffer.from(text, 'utf-8') 32 | data.append('file', buffer, { filename: 'dispute-evidence.txt', contentType: 'text/plain' }) 33 | 34 | const options = { 35 | method: 'POST', 36 | headers: { 37 | Authorization: `Bearer ${process.env.PINATA_JWT}`, 38 | ...data.getHeaders(), 39 | }, 40 | data: data, 41 | } 42 | 43 | try { 44 | const response = await axios(url, options) 45 | return response.data.IpfsHash 46 | } catch (error) { 47 | console.error('Error uploading text to IPFS:', error) 48 | throw error 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { LicenseTerms, LicensingConfig, WIP_TOKEN_ADDRESS } from '@story-protocol/core-sdk' 2 | import { Address, parseEther, zeroAddress } from 'viem' 3 | import dotenv from 'dotenv' 4 | import { networkInfo } from './config' 5 | 6 | dotenv.config() 7 | 8 | // Export contract addresses with appropriate defaults based on network 9 | export const NFTContractAddress: Address = 10 | (process.env.NFT_CONTRACT_ADDRESS as Address) || networkInfo.defaultNFTContractAddress || zeroAddress 11 | 12 | export const SPGNFTContractAddress: Address = 13 | (process.env.SPG_NFT_CONTRACT_ADDRESS as Address) || networkInfo.defaultSPGNFTContractAddress || zeroAddress 14 | 15 | // This is a pre-configured PIL Flavor: 16 | // https://docs.story.foundation/concepts/programmable-ip-license/pil-flavors#flavor-%231%3A-non-commercial-social-remixing 17 | export const NonCommercialSocialRemixingTermsId = '1' 18 | export const NonCommercialSocialRemixingTerms: LicenseTerms = { 19 | transferable: true, 20 | royaltyPolicy: zeroAddress, 21 | defaultMintingFee: 0n, 22 | expiration: 0n, 23 | commercialUse: false, 24 | commercialAttribution: false, 25 | commercializerChecker: zeroAddress, 26 | commercializerCheckerData: '0x', 27 | commercialRevShare: 0, 28 | commercialRevCeiling: 0n, 29 | derivativesAllowed: true, 30 | derivativesAttribution: true, 31 | derivativesApproval: false, 32 | derivativesReciprocal: true, 33 | derivativeRevCeiling: 0n, 34 | currency: zeroAddress, 35 | uri: 'https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json', 36 | } 37 | 38 | // Docs: https://docs.story.foundation/developers/deployed-smart-contracts 39 | export const RoyaltyPolicyLAP: Address = '0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E' 40 | export const RoyaltyPolicyLRP: Address = '0x9156e603C949481883B1d3355c6f1132D191fC41' 41 | 42 | // This is a pre-configured PIL Flavor: 43 | // https://docs.story.foundation/concepts/programmable-ip-license/pil-flavors#flavor-%233%3A-commercial-remix 44 | export function createCommercialRemixTerms(terms: { commercialRevShare: number; defaultMintingFee: number }): LicenseTerms { 45 | return { 46 | transferable: true, 47 | royaltyPolicy: RoyaltyPolicyLAP, 48 | defaultMintingFee: parseEther(terms.defaultMintingFee.toString()), 49 | expiration: BigInt(0), 50 | commercialUse: true, 51 | commercialAttribution: true, 52 | commercializerChecker: zeroAddress, 53 | commercializerCheckerData: '0x', 54 | commercialRevShare: terms.commercialRevShare, 55 | commercialRevCeiling: BigInt(0), 56 | derivativesAllowed: true, 57 | derivativesAttribution: true, 58 | derivativesApproval: false, 59 | derivativesReciprocal: true, 60 | derivativeRevCeiling: BigInt(0), 61 | currency: WIP_TOKEN_ADDRESS, 62 | uri: 'https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json', 63 | } 64 | } 65 | 66 | export const defaultLicensingConfig: LicensingConfig = { 67 | mintingFee: 0n, 68 | isSet: false, 69 | disabled: false, 70 | commercialRevShare: 0, 71 | expectGroupRewardPool: zeroAddress, 72 | expectMinimumGroupRewardShare: 0, 73 | licensingHook: zeroAddress, 74 | hookData: '0x', 75 | } 76 | 77 | export function convertRoyaltyPercentToTokens(royaltyPercent: number): number { 78 | // there are 100,000,000 tokens total (100, but 6 decimals) 79 | return royaltyPercent * 1_000_000 80 | } 81 | --------------------------------------------------------------------------------