# CDR Overview Source: https://docs.datafdn.org/cdr/overview Threshold-encrypted, on-chain-gated confidentiality for data registered on the DATA Foundation. CDR's current public release runs on Aeneid testnet. Build and test integrations there, but do not treat it as a production confidentiality environment. ## What is CDR? **Confidential Data Rails (CDR)** is the DATA Foundation's confidentiality layer. It lets you encrypt data so that no single party ever holds the complete decryption key: secrets are encrypted against the validator network's DKG-generated public key and can only be recovered when a threshold number of validators collectively provide partial decryptions. Access is enforced **on-chain** through smart-contract conditions, and the validator-side flows run inside `story-kernel` TEEs (Intel SGX enclaves). The result is data that stays **confidential at rest** while remaining **programmatically unlockable** to exactly the wallets, license holders, or custom conditions you define. ```text theme={null} Data owner -> encrypt locally against the DKG public key (plaintext never leaves the client) -> allocate a vault on-chain with read/write conditions -> authorized reader requests decryption (validated on-chain) -> validators return TEE-confined partial decryptions; reader combines client-side ``` The full design behind Confidential Data Rails: cryptography, validator protocol, and threat model. ## Where CDR Fits Proves the origin, consent, and lineage of data with a provider-normalized schema and public audit views. Keeps the underlying data encrypted, with threshold decryption gated by on-chain access control. Defines who owns the data and the terms under which it can be used. Together they let a provider register data that is **provable** (Trace), **confidential** (CDR), and **governed by clear usage rights** (IP & Licensing). ## What CDR Enables * **Secret sharing**: encrypt and share secrets that only specific wallets can decrypt. * **Encrypted file delivery**: keep large files off-chain while storing the encrypted file key on-chain. * **Data marketplaces**: sell access to encrypted data with on-chain payment enforcement. * **IP-gated content**: tie encrypted data to IP Assets and require license tokens to decrypt. ## How Developers Integrate CDR ships as the `@piplabs/cdr-sdk` TypeScript SDK. The current Aeneid surface centers on two workflows: **data key vaults** for small secrets stored directly on-chain, and **encrypted files** for off-chain content with on-chain key management. Install the SDK, allocate vaults, and run the on-chain secret, encrypted file, and IP-gated flows end to end. Full API reference for every CDR SDK method. # 🪝 Hooks Source: https://docs.datafdn.org/concepts/hooks Add custom logic before minting license tokens or registering derivatives. Hooks allow developers to create custom implementations, restrictions, and functionality upon minting [License Tokens](/concepts/licensing-module/license-token) or registering derivatives. There are two types of hooks: 1. **Licensing Hooks**: allow you to [add custom logic before minting license tokens](/concepts/licensing-module/license-config#logic-that-is-possible-with-license-config) (and registering derivatives). For example, requesting a dynamic price, limiting the amount of license tokens that can be minted, whitelists, etc. Licensing Hooks can be added / modified on a licensing config at any point. 2. **Commercializer Checker Hooks**: similar to Licensing Hooks, however they are directly a part of the license terms and do not change. You also cannot return a custom minting fee. ## Licensing Hooks These are contracts that implement the `ILicensingHook` interface, which extends from `IModule`. Most importantly, a Licensing Hook implements a `beforeMintLicenseTokens` function, which is a function that is called before a License Token is minted to implement [custom logic](/concepts/licensing-module/license-config#logic-that-is-possible-with-license-config) and determine the final `totalMintingFee` of that License Token. View the `ILicensingHook` smart contract [here](https://github.com/thedatafoundation/protocol-core-v1/blob/main/contracts/interfaces/modules/licensing/ILicensingHook.sol#L26). ```solidity ILicensingHook.sol theme={null} /// @notice This function is called when the LicensingModule mints license tokens. /// @dev The hook can be used to implement various checks and determine the minting price. /// The hook should revert if the minting is not allowed. /// @param caller The address of the caller who calling the mintLicenseTokens() function. /// @param licensorIpId The ID of licensor IP from which issue the license tokens. /// @param licenseTemplate The address of the license template. /// @param licenseTermsId The ID of the license terms within the license template, /// which is used to mint license tokens. /// @param amount The amount of license tokens to mint. /// @param receiver The address of the receiver who receive the license tokens. /// @param hookData The data to be used by the licensing hook. /// @return totalMintingFee The total minting fee to be paid when minting amount of license tokens. function beforeMintLicenseTokens( address caller, address licensorIpId, address licenseTemplate, uint256 licenseTermsId, uint256 amount, address receiver, bytes calldata hookData ) external returns (uint256 totalMintingFee); ``` Note that it returns the `totalMintingFee`. You may be wondering, "I can set the minting fee in the License Terms, in the `LicenseConfig`, and return a dynamic price from `beforeMintLicenseTokens`. What will the final minting fee actually be?" Here is the priority: | Minting Fee | Importance | | ------------------------------------------------------------- | ---------------- | | The `totalMintingFee` returned from `beforeMintLicenseTokens` | Highest Priority | | The `mintingFee` set in the `LicenseConfig` | ⬇️ | | The `mintingFee` set in the License Terms | Lowest Priority | Beware of potentially malicious implementations of external license hooks. Please first verify the code of the hook you choose because it may be not reviewed or audited by the DATA Foundation team. ### Available Hooks Below are available hooks deployed on our protocol that you can use. View the deployed addresses for these hooks [here](/developers/deployed-smart-contracts#license-hooks). | Hook | Description | Contract Code | | :------------------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | LockLicenseHook | Stop the minting of license tokens or registering new derivatives. | [View here ↗️](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/hooks/LockLicenseHook.sol) | | TotalLicenseTokenLimitHook | Set a limit on the amount of license tokens that can be minted, updatable at any time. | [View here ↗️](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/hooks/TotalLicenseTokenLimitHook.sol) | ### Implementing the Hooks A working TypeScript SDK code example that shows how to implement a licensing hook. More specifically, how to limit the # of licenses that can be minted. A working Solidity code example that shows how to implement a licensing hook. More specifically, how to limit the # of licenses that can be minted. Licensing Hooks are ultimately a smart contract that implements the `ILicensingHook` interface. You can view the interface [here](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/interfaces/ILicensingHook.sol). We have a few Licensing Hooks deployed already (view the chart above). In order to actually use a Licensing Hook, you must set it in the Licensing Config, which is basically a set of configurations that you set on License Terms when attaching terms to an IP Asset. First you have to create a Licensing Config: ```typescript {6-8} theme={null} import { LicensingConfig } from '@story-protocol/core-sdk'; const licensingConfig: LicensingConfig = { isSet: true, mintingFee: 0n, // address of TotalLicenseTokenLimitHook // from https://docs.datafdn.org/developers/deployed-smart-contracts licensingHook: '0xaBAD364Bfa41230272b08f171E0Ca939bD600478', hookData: zeroAddress, commercialRevShare: 0, disabled: false, expectMinimumGroupRewardShare: 0, expectGroupRewardPool: zeroAddress, } ``` Next, we'll set the Licensing Config on the License Terms. In the following example, we'll show this happening upon registering the IP Asset: This code snippet requires a bit of setup, and it meant for developers who already understand how to setup the TypeScript SDK. If you want to learn more, check out the [working code example](https://github.com/thedatafoundation/typescript-tutorial/blob/main/scripts/licenses/oneTimeUseLicense.ts). This uses the `registerIpAsset` method found [here](/sdk-reference/ipasset#registeripasset). ```typescript {6-7} theme={null} const response = await client.ipAsset.registerIpAsset({ nft: { type: 'mint', spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc', // public spg contract for ease-of-use }, licenseTermsData: [ { terms: { defaultMintingFee: 0, commercialUse: true, ... }, // dummy license terms // set the licensing config here licensingConfig: licensingConfig }, ], ipMetadata: { ipMetadataURI: 'test-uri', ipMetadataHash: toHex('test-metadata-hash', { size: 32 }), nftMetadataHash: toHex('test-nft-metadata-hash', { size: 32 }), nftMetadataURI: 'test-nft-uri', } }) console.log(`Token ID: ${response.tokenId}, IPA ID: ${response.ipId}, License Terms ID: ${response.licenseTermsIds}`); ``` Now that we have set the Licensing Config on our terms, we can call the `setTotalLicenseTokenLimit` function on the hook and set the max # of licenses that can be minted to 1. ```typescript theme={null} const hookResponse = await client.license.setMaxLicenseTokens({ ipId: response.ipId, licenseTermsId: response.licenseTermsIds![0], maxLicenseTokens: 1000, }); console.log(`Max license tokens set at transaction hash ${hookResponse.txHash}`); ``` ### Creating a New Licensing Hook Please follow the below process for creating a new licensing hook and getting it whitelisted into the DATA Foundation's protocol. 1. **Develop your Hook**: You may fork [this template repository](https://github.com/thedatafoundation/hook-dev-template) to bootstrap your development. It includes an example hook with tests against our Aeneid protocol, but using it is optional. See [license-caller-whitelist-hook](https://github.com/jacob-tucker/license-caller-whitelist-hook) as an example that was used to develop the `LicenseCallerWhitelistHook.sol` hook. 2. **Fork Registered Modules Repository**: Fork the [registered-modules repository](https://github.com/thedatafoundation/registered-modules) to your own GitHub account. 3. **Update the Module List**: In your `registered-modules` repository, add your hook's details to the `hook-modules.json` file. Make sure to **deploy & verify on block explorer** your hook on both aeneid and mainnet. Ensure that you adhere to the following JSON structure: ```json theme={null} { "name": "YourModuleName", "aeneid": { "address": "YourModuleAddress", "blockExplorerLink": "YourModuleBlockExplorerLink" }, "mainnet": { "address": "YourModuleAddress", "blockExplorerLink": "YourModuleBlockExplorerLink" } } ``` Replace `YourModuleName`, `YourModuleAddress`, and `YourModuleBlockExplorerLink` with your hook's name, its address, and the link to its block explorer page, respectively. Example: ```json theme={null} { "name": "LicenseCallerWhitelistHook", "aeneid": { "address": "0x37be56d9fb06d885cda3cb010096c94c28b4d658", "blockExplorerLink": "https://aeneid.datanetscan.io/address/0x37be56d9fb06d885cda3cb010096c94c28b4d658?tab=contract" }, "mainnet": { "address": "0x6d9d51a444c8318e8840e75dab7ed81b5a714610", "blockExplorerLink": "https://www.datanetscan.io/address/0x6d9d51a444c8318e8840e75dab7ed81b5a714610?tab=contract" } } ``` 4. **Create a Pull Request (PR)**: Once you have added your hook, create a pull request against this repository. In your PR's description, add the following information (replace the values with your own): ```md theme={null} ## Register my module - Module type: `hook` - Module name: `LicenseCallerWhitelistHook` - Aeneid Module address: `0x37be56d9fb06d885cda3cb010096c94c28b4d658` - Mainnet Module address: `0x6d9d51a444c8318e8840e75dab7ed81b5a714610` - My module is immutable: yes - My module is using an upgradeable proxy: no - My module has been verified on the block explorer (required): yes - Summary of my module: Hook for allowing a licensor to gate which addresses can mint a license. The licensor can add/remove an address at any time. - GitHub repository with source code and tests: https://github.com/jacob-tucker/license-caller-whitelist-hook ``` 5. **Await Verification**: After your PR is submitted, it will be reviewed. Once a security audit has been performed and completed, and the module whitelisted into the protocol, the PR will be merged. At this point your module will be officially registered and recognized as safe for use within the DATA Foundation community. We look forward to seeing your contributions and expanding the DATA Foundation module ecosystem! ## Commercializer Checker Hooks Documentation coming soon. If you have questions in the meantime, ask in the [Builder's Discord](https://discord.gg/databuilders). # ⚙️ IP Account Source: https://docs.datafdn.org/concepts/ip-asset/ip-account A modified ERC-6551 implementation bound to an IP Asset Skip the Read Get a quick 2-minute overview of IP Accounts [here](https://twitter.com/jacobmtucker/status/1787603252198134234). When an [🧩 IP Asset](/concepts/ip-asset/overview) is registered, it is given an associated **IP Account**. An IP Account is a modified ERC-6551 (Token Bound Account) implementation. It is a separate contract bound to the IP Asset for controlling permissions around interactions with the DATA Foundation's modules or storing the IP's associated data. Upon registration, an IP Asset is assigned a unique ID. This ID is the address of the IP Account that is bound to the IP Asset. IP Account Diagram An IP Account mainly does two things: 1. Stores comprehensive IP-related data, including metadata and ownership details of associated assets such as the License Tokens or Royalty Tokens that are created from the IP. 2. Facilitates the utilization of this data by various modules. These modules interact with and contribute to the IP Account, creating and storing data. For example, licensing, revenue/royalty sharing, remixing, disputing an IP, and other modules are made possible due to the IP Account's programmability. If the underlying NFT is transferred, the new owner is also automatically the owner of the associated IP Asset & IP Account. ## `execute` and `executeWithSig` A key feature of IP Account is the generic `execute()` function, which allows calling arbitrary modules within the DATA Foundation via encoded bytes data (thus extensible for future modules). Additionally, there is a `executeWithSig()` function that enables users to sign transactions and have others execute on their behalf for seamless UX. # 📝 IPA Metadata Standard Source: https://docs.datafdn.org/concepts/ip-asset/ipa-metadata-standard An overview of the IP-specific metadata standard We are still figuring out the best way to define an IPA Metadata Standard. For the sake of transparency, the following document is our thoughts so far but is subject to change as we release future versions. Check out the official Ippy IP, which has both NFT & IP metadata. Learn how to actually add the IP metadata discussed here to your IP Asset with an explanation or completed code example. This is the JSON metadata that is associated with an IP Asset, and gets stored inside of an IP Account. You must call `setMetadata(...)` inside of the IP Account in order to set the metadata, and then call `metadata()` to read it. ## Attributes & Structure Below are the important attributes you should provide in your IP metadata. Under the **Required For** column is what the specific field is required for: * 🔍 DATA Foundation Explorer - this field will help display your IP on the DATA Foundation Explorer * 🕵️ Commercial Infringement Check - this field is required if your IP is **commercial** (that is, has `commercialUse = true` license terms attached). We will use these fields to run an infringement check on your IP. * This applies when a `commercialUse = true` license term is attached. * 🤖 AI Agents - used for displaying metadata associated with AI Agents | Property Name | Type | Description | Required For | | ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `title` | `string` | Title of the IP | 🔍 DATA Foundation Explorer | | `description` | `string` | Description of the IP | 🔍 DATA Foundation Explorer | | `createdAt` | `string` | Date/Time that the IP was created (either ISO8601 or unix format). This field can be used to specify historical dates that aren't on-chain. For example, Harry Potter was published on June 26. | 🔍 DATA Foundation Explorer | | `image` | `string` | An image for your IP. **For audio assets, the recommended thumbnail aspect ratio is 1:1. For video assets, it is 16:9.** | 🔍 DATA Foundation Explorer | | `imageHash` | `string` | Hash of your `image` using SHA-256 hashing algorithm. See [here](#hashing-content) for how that is done. | 🔍 DATA Foundation Explorer | | `creators` | `IpCreator[]` | An array of information about the creators. [See the type defined below](#type-definitions) | 🔍 DATA Foundation Explorer | | `mediaUrl` | `string` | Used for infringement checking, points to the actual media (ex. image or audio). **For audio assets, the recommended thumbnail aspect ratio is 1:1. For video assets, it is 16:9.** | 🕵️ Commercial Infringement Check | | `mediaHash` | `string` | Hashed string of the media using SHA-256 hashing algorithm. See [here](#hashing-content) for how that is done. | 🕵️ Commercial Infringement Check | | `mediaType` | `string` | Type of media (audio, video, image), based on [mimeType](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types). See the allowed media types [here](#media-types). | 🕵️ Commercial Infringement Check | | `aiMetadata` | `AIMetadata` | Used for registering & displaying AI Agent Metadata. [See the type defined below](#type-definitions) | 🤖 AI Agents | | N/A | N/A | You can include other values as well. | N/A | ### Type Definitions Here are the type definitions for the complex types used in the metadata: ```typescript IpCreator theme={null} type IpCreator = { name: string; address: Address; contributionPercent: number; // add up to 100 description?: string; image?: string; socialMedia?: IpCreatorSocial[]; role?: string; }; type IpCreatorSocial = { platform: string; url: string; }; ``` ```typescript AIMetadata theme={null} type AIMetadata = { // this can be any character file you want // example: https://github.com/elizaOS/characterfile/blob/main/examples/example.character.json characterFileUrl: string; characterFileHash: string; }; ``` ### Media Types The following media types are allowed for the `mediaType` field: | Media Type | Description | | ----------------- | --------------------- | | `image/jpeg` | JPEG image | | `image/png` | PNG image | | `image/apng` | Animated PNG image | | `image/avif` | AV1 Image File Format | | `image/gif` | GIF image | | `image/svg+xml` | SVG image | | `image/webp` | WebP image | | `audio/wav` | WAV audio | | `audio/mpeg` | MP3 audio | | `audio/flac` | FLAC audio | | `audio/aac` | AAC audio | | `audio/ogg` | OGG audio | | `audio/mp4` | MP4 audio | | `audio/x-aiff` | AIFF audio | | `audio/x-ms-wma` | WMA audio | | `audio/opus` | Opus audio | | `video/mp4` | MP4 video | | `video/webm` | WebM video | | `video/quicktime` | QuickTime video | ### Hashing Content To hash content for the `imageHash` or `mediaHash` fields, you can use the SHA-256 hashing algorithm. Here's an example of how to do this in JavaScript: ```typescript TypeScript theme={null} import { toHex, Hex } from "viem"; // get hash from a file async function getFileHash(file: File): Promise { const arrayBuffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer); return toHex(new Uint8Array(hashBuffer), { size: 32 }); } // get hash from a url async function getHashFromUrl(url: string): Promise { const response = await axios.get(url, { responseType: "arraybuffer" }); const buffer = Buffer.from(response.data); return "0x" + createHash("sha256").update(buffer).digest("hex"); } ``` ```shell Shell theme={null} shasum -a 256 myfile.jpg ``` ### Example Use Cases This is the official Ippy mascot that is registered on mainnet. You can view it on our protocol explorer [here](https://explorer.datafdn.org/ipa/0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42). ```json theme={null} { "title": "Ippy", "description": "Official mascot of the DATA Foundation.", "createdAt": "1728401700", "image": "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8", "imageHash": "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e", "mediaUrl": "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8", "mediaHash": "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e", "mediaType": "image/png", "creators": [ { "name": "The DATA Foundation", "address": "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084", "description": "The World's IP Blockchain", "contributionPercent": 100, "socialMedia": [ { "platform": "Twitter", "url": "https://x.com/DataFDN" }, { "platform": "Telegram", "url": "https://t.me/yourproject" }, { "platform": "Website", "url": "https://datafdn.org" }, { "platform": "Discord", "url": "https://discord.gg/datafdn" }, { "platform": "YouTube", "url": "https://youtube.com/@storyFDN" } ] } ], "tags": ["Ippy", "DATA Foundation", "DATA Foundation Mascot", "Mascot", "Official"], // experimental field "ipType": "Character" // experimental field } ``` This is an example song generated on [Suno](https://suno.com/) and registered on our testnet. View the below example [on our protocol explorer](https://aeneid.explorer.datafdn.org/ipa/0x7d126DB8bdD3bF88d757FC2e99BFE3d77a55509b). ```json theme={null} { "title": "Midnight Marriage", "description": "This is a house-style song generated on suno.", "createdAt": "1740005219", "creators": [ { "name": "Jacob Tucker", "address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112", "contributionPercent": 100 } ], "image": "https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg", "imageHash": "0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc", "mediaUrl": "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3", "mediaHash": "0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee", "mediaType": "audio/mpeg" } ``` The main difference here is you should supply `aiMetadata` with a character file. You can provide any character file you want, or use [this ElizaOS example](https://github.com/elizaOS/characterfile/blob/main/examples/example.character.json) as a template. View the below example [on our protocol explorer](https://aeneid.explorer.datafdn.org/ipa/0x49614De8b2b02C790708243F268Af50979D568d4). ```json theme={null} { "title": "DATA Foundation AI Agent", "description": "This is an example AI Agent registered on the DATA Foundation.", "createdAt": "1740005219", "creators": [ { "name": "Jacob Tucker", "address": "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112", "contributionPercent": 100 } ], "image": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute", "imageHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a", "mediaUrl": "https://ipfs.io/ipfs/bafybeigi3k77t5h5aefwpzvx3uiomuavdvqwn5rb5uhd7i7xcq466wvute", "mediaHash": "0x64ccc40de203f218d16bb90878ecca4338e566ab329bf7be906493ce77b1551a", "mediaType": "image/webp", "aiMetadata": { "characterFileUrl": "https://ipfs.io/ipfs/bafkreic6eu4hlnwx46soib62rgkhhmlieko67dggu6bzk7bvtfusqsknfu", "characterFileHash": "0x5e253875b6d7e7a4e407da899473b168229def8cc6a783957c35996928494d2d" } } ``` ## Optional Properties The following properties are optional but can provide additional context about your IP Asset: We are still figuring out the best way to define an IPA Metadata Standard. The fields below are bound to change or be removed at some point. | Property Name | Type | Description | | :--------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ipType` | `string` | Type of the IP Asset, can be defined arbitrarily by the creator. I.e. "character", "chapter", "location", "items", "music", etc | | `relationships` | `IpRelationship[]` | The detailed relationship info with the IPA's direct parent asset, such as `APPEARS_IN`, `FINETUNED_FROM`, etc. See more examples [here](#relationship-types). | | `watermarkImage` | `string` | A separate image with your watermark already applied. This way apps choosing to use it can render this version of the image (with watermark applied). | | `media` | `IpMedia[]` | An array of supporting media. Media type defined below | | `app` | `DataApp` | This is assigned to verified application from the DATA Foundation directly (on a request basis so far). We will map each App ID to a name | | `tags` | `string[]` | Any tags that can help surface this IPA | | `robotTerms` | `IPRobotTerms` | Allows you to set Do Not Train for a specific agent | | N/A | N/A | You can include other values as well. | ### Type Definitions ```typescript IpRelationship theme={null} type IpRelationship = { parentIpId: Address; type: string; // see "Relationship Types" docs below }; ``` ```typescript IpMedia theme={null} type IpMedia = { name: string; url: string; mimeType: string; }; ``` ```typescript DataApp theme={null} type DataApp = { id: string; name: string; website: string; action?: string; }; ``` ```typescript IPRobotTerms theme={null} type IPRobotTerms = { userAgent: string; allow: string; }; ``` ### Relationship Types The different relationship types that can be used for the `relationships` attribute. #### DATA Foundation Relationships 1. **APPEARS\_IN** - A character APPEARS\_IN a chapter. 2. **BELONGS\_TO** - A chapter BELONGS\_TO a book. 3. **PART\_OF** - A book is PART\_OF a series. 4. **CONTINUES\_FROM** - A chapter CONTINUES\_FROM the previous one. 5. **LEADS\_TO** - An event LEADS\_TO a consequence. 6. **FORESHADOWS** - An event FORESHADOWS future developments. 7. **CONFLICTS\_WITH** - A character CONFLICTS\_WITH another character. 8. **RESULTS\_IN** - A decision RESULTS\_IN a significant change. 9. **DEPENDS\_ON** - A subplot DEPENDS\_ON the main plot. 10. **SETS\_UP** - A prologue SETS\_UP the story. 11. **FOLLOWS\_FROM** - A chapter FOLLOWS\_FROM the previous one. 12. **REVEALS\_THAT** - A twist REVEALS\_THAT something unexpected occurred. 13. **DEVELOPS\_OVER** - A character DEVELOPS\_OVER the course of the story. 14. **INTRODUCES** - A chapter INTRODUCES a new character or element. 15. **RESOLVES\_IN** - A conflict RESOLVES\_IN a particular outcome. 16. **CONNECTS\_TO** - A theme CONNECTS\_TO the main narrative. 17. **RELATES\_TO** - A subplot RELATES\_TO the central theme. 18. **TRANSITIONS\_FROM** - A scene TRANSITIONS\_FROM one setting to another. 19. **INTERACTED\_WITH** - A character INTERACTED\_WITH another character. 20. **LEADS\_INTO** - An event LEADS\_INTO the climax.?\ **PARALLEL - story** happening in parallel or around the same timeframe #### AI Relationships 1. **TRAINED\_ON** - A model is TRAINED\_ON a dataset. 2. **FINETUNED\_FROM** - A model is FINETUNED\_FROM a base model. 3. **GENERATED\_FROM** - An image is GENERATED\_FROM a fine-tuned model. 4. **REQUIRES\_DATA** - A model REQUIRES\_DATA for training. 5. **BASED\_ON** - A remix is BASED\_ON a specific workflow. 6. **INFLUENCES** - Sample data INFLUENCES model output. 7. **CREATES** - A pipeline CREATES a fine-tuned model. 8. **UTILIZES** - A workflow UTILIZES a base model. 9. **DERIVED\_FROM** - A fine-tuned model is DERIVED\_FROM a base model. 10. **PRODUCES** - A model PRODUCES generated images. 11. **MODIFIES** - A remix MODIFIES the base workflow. 12. **REFERENCES** - An AI-generated image REFERENCES original data. 13. **OPTIMIZED\_BY** - A model is OPTIMIZED\_BY specific algorithms. 14. **INHERITS** - A fine-tuned model INHERITS features from the base model. 15. **APPLIES\_TO** - A fine-tuning process APPLIES\_TO a model. 16. **COMBINES** - A remix COMBINES elements from multiple datasets. 17. **GENERATES\_VARIANTS** - A model GENERATES\_VARIANTS of an image. 18. **EXPANDS\_ON** - A fine-tuning process EXPANDS\_ON base capabilities. 19. **CONFIGURES** - A workflow CONFIGURES a model’s parameters. 20. **ADAPTS\_TO** - A fine-tuned model ADAPTS\_TO new data. # IP Modifications & Restrictions Source: https://docs.datafdn.org/concepts/ip-asset/ipa-modifications Learn about the modifications and restrictions for IP Assets # IP Asset Modifications IP Assets can be modified/customized a few ways. For example, by [setting the License Config](/concepts/licensing-module/license-config) which allows you to change a few things as you'll see below, changing its metadata, and more. These things can **always be changed unless there is a certain condition**. | Action | Conditions | Via The... | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Modify License Minting Fee | You can **increase** the minting fee. You **cannot** decrease it. | [License Config](/concepts/licensing-module/license-config) | | Modify Licensing Hook | The hook must be whitelisted in the Module Registry. | [License Config](/concepts/licensing-module/license-config) | | Modify `commercialRevShare` | You can **increase** the rev share percentage. You **cannot** decrease it.

However, you **can** set it to 0 to disable the overwrite. | [License Config](/concepts/licensing-module/license-config) | | Disable/Enable the License | License can be disabled or re-enabled at any time.

*Note that disabling a license disallows future licenses from being minted, but does not affect existing ones.* | [License Config](/concepts/licensing-module/license-config) | | Modify Metadata | Cannot modify if the metadata is **frozen**. This is done by calling `freezeMetadata` in the [CoreMetadataModule.sol](https://github.com/thedatafoundation/protocol-core-v1/blob/main/contracts/modules/metadata/CoreMetadataModule.sol). | [CoreMetadataModule.sol](https://github.com/thedatafoundation/protocol-core-v1/blob/main/contracts/modules/metadata/CoreMetadataModule.sol) | ## License Hook Modifications The IP can be further customized or modified using the [License Hook](/concepts/hooks#licensing-hooks). This is a function that gets set within the License Config that gets called before a [License Token](/concepts/licensing-module/license-token) (or more simply, a "license") is minted. There are various features you can implement with the License Hook, and are **always modifiable**: | Feature | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------- | | Dynamic License Fee | You can dynamically set the price of a license. For example, it can be updated dynamically via bonding curve logic. | | Total # of Licenses | You can abort the function based on a maximum number of license tokens that can be minted. | | Specific Receivers | You can restrict minting of license to a specific receiver. | | More... | Additional licensing hook features can be implemented as required. | # 🧩 IP Asset Source: https://docs.datafdn.org/concepts/ip-asset/overview The foundational programmable IP metadata on the DATA Foundation Skip the Read Get a quick 1-minute overview of IP Assets [here](https://twitter.com/jacobmtucker/status/1785765362744889410). IP Assets are the foundational programmable IP metadata on the DATA Foundation. Each IP Asset is an on-chain ERC-721 NFT (representing an IP). If your IP is off-chain, you would simply mint an ERC-721 NFT to represent that IP first, and then register it as an IP Asset. When an IP Asset is created, an associated [⚙️ IP Account](/concepts/ip-asset/ip-account) is deployed, which is a modified ERC-6551 (Token Bound Account) implementation. It is a separate contract bound to the IP Asset for controlling permissions around interactions with the DATA Foundation's modules or storing the IP's associated data. ## Registering an IP Asset An IP Asset is created by registering an ERC-721 NFT into the DATA Foundation's global IP Asset Registry. If you'd like to jump into code examples/tutorials, please see [How to Register IP on the DATA Foundation](/developers/tutorials/how-to-register-ip). ## NFT vs. IP Metadata On the DATA Foundation, your IP is an NFT that gets registered on the protocol as an IP Asset. However, both NFTs and IP Assets have their own metadata you can set, so what's the difference? | | Standard | What is it? | | :------ | :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NFT** | [Opensea ERC721 Standard](https://docs.opensea.io/docs/metadata-standards) | Things like `name`, `description`, `image`, `attributes`, `animation_url`, etc | | **IP** | [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) | More specific to the DATA Foundation, this includes necessary information about the underlying content for infringement checks, authors of the work, etc | All other metadata, such as the ownership, legal, and economic details of an IP Asset are handled by our protocol directly. For example, the protocol stores data associated with parent-child relationships through the [📜 Licensing Module](/concepts/licensing-module/overview), and the legal constraints/permissions of an IP Asset with the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview). ### Adding NFT & IP Metadata to IP Asset Jump to the code and see a completed code example of adding NFT & IP metadata to an IP Asset Learn how to add metadata to your IP Asset with a step-by-step explanation. In practice, whether you are using the SDK or our smart contract directly, our protocol asks you to provide 4 different parameters: * View the `WorkflowStructs.sol` contract [here](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/lib/WorkflowStructs.sol). ```solidity WorkflowStructs.sol theme={null} /// @notice Struct for metadata for NFT minting and IP registration. /// @dev Leave the nftMetadataURI empty if not minting an NFT. /// @param ipMetadataURI The URI of the metadata for the IP. /// @param ipMetadataHash The hash of the metadata for the IP. /// @param nftMetadataURI The URI of the metadata for the NFT. /// @param nftMetadataHash The hash of the metadata for the IP NFT. struct IPMetadata { string ipMetadataURI; bytes32 ipMetadataHash; string nftMetadataURI; bytes32 nftMetadataHash; } ``` * `ipMetadataURI` - a URI pointing to a JSON object that follows the [📝 IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) * `ipMetadataHash` - hash of the `ipMetadataURI` JSON object * `nftMetadataURI` - a URI pointing to a JSON object that follows the [Opensea ERC721 Standard](https://docs.opensea.io/docs/metadata-standards) * `nftMetadataHash` - hash of the `nftMetadataURI` JSON object # License Config Source: https://docs.datafdn.org/concepts/licensing-module/license-config An optional config that can be attached to a specific license for dynamic minting fees and custom logic. ## License Config View the LicensingConfig struct in the smart contract. Optionally, you can attach a `LicensingConfig` to an IP Asset (for a specific `licenseTermsId` attached to that asset) which contains fields like a `mintingFee` and a `licensingHook`, as shown below. ```solidity theme={null} /// @notice This struct is used by IP owners to define the configuration /// when others are minting license tokens of their IP through the LicensingModule. /// When the `mintLicenseTokens` function of LicensingModule is called, the LicensingModule will read /// this configuration to determine the minting fee and execute the licensing hook if set. /// IP owners can set these configurations for each License or set the configuration for the IP /// so that the configuration applies to all licenses of the IP. /// If both the license and IP have the configuration, then the license configuration takes precedence. /// @param isSet Whether the configuration is set or not. /// @param mintingFee The minting fee to be paid when minting license tokens. /// @param licensingHook The hook contract address for the licensing module, or address(0) if none /// @param hookData The data to be used by the licensing hook. /// @param commercialRevShare The commercial revenue share percentage. /// @param disabled Whether the license is disabled or not. /// @param expectMinimumGroupRewardShare The minimum percentage of the group's reward share /// (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group. /// If the remaining reward share in the group is less than the minimumGroupRewardShare, /// the IP cannot be added to the group. /// @param expectGroupRewardPool The address of the expected group reward pool. /// The IP can only be added to a group with this specified reward pool address, /// or address(0) if the IP does not want to be added to any group. struct LicensingConfig { bool isSet; uint256 mintingFee; address licensingHook; bytes hookData; uint32 commercialRevShare; bool disabled; uint32 expectMinimumGroupRewardShare; address expectGroupRewardPool; } ``` What do some of these mean? 1. `isSet` - if this is false, the whole licensing config is completely ignored. So for example, if the licensing config has `mintingFee == 10` and `disabled == true`, but the `isSet == false`, the `mintingFee` and `disabled` will be completely ignored. 2. `disabled` - if this is true, then no licenses can be minted and no more derivatives can be attached at all for the terms the config is attached to. Fields like the `mintingFee` and `commercialRevShare` overwrite their duplicate in the license terms themselves. **A benefit of this is that derivative IP Assets, which normally cannot change their license terms, are able to overwrite certain fields.** The `licensingHook` is an address to a smart contract that implements the `ILicensingHook` interface, which contains a `beforeMintLicenseTokens` function which will be run before a user mints a License Token. This means you can insert logic to be run upon minting a license. The hook itself is described in a different section. You can see it contains information about the license, who is minting the License Token, and who is receiving it. Learn all about Licensing Hooks [here](/concepts/hooks#licensing-hooks). ### Setting the License Config You can set the License Config by calling the `setLicenseConfig` function in the [LicensingModule.sol contract](https://github.com/thedatafoundation/protocol-core-v1/blob/main/contracts/modules/licensing/LicensingModule.sol). ### Logic That Is Possible With License Config 1. **Max Number of Licenses**: The `licensingHook` (described in the next section) is where you can define logic for the max number of licenses that can be minted. For example, reverting the transaction if the max number of licenses has already been minted. 2. **Disallowing Derivatives**: If you register a derivative of an IP Asset, that derivative cannot change its License Terms as described [here](/concepts/licensing-module/license-terms#inherited-license-terms). You can be wondering: "What if I, as a derivative, want to disallow derivatives of myself, but my License Terms allow derivatives and I cannot change this?" To solve this, you can simply set `disabled` to true. 3. **Minting Fee**: Similar to #2 above... what about the minting fee? Although you cannot change License Terms on a derivative IP Asset (and thus the minting fee inside of it), you can change the minting fee for that derivative by modifying the `mintingFee` in the License Config, or returning a `totalMintingFee` from the `licensingHook` (described in the next section). 4. **Commercial Revenue Share**: Similar to #2 and #3 above, you can modify the `commercialRevShare` in the License Config. 5. **Dynamic Pricing for Minting a License Token**: Set dynamic pricing for minting a License Token from an IP Asset based on how many total have been minted, how many licenses the user is minting, or even who the user is. All of this data is available in the `licensingHook` (described in the next section). ... and more. ### Restrictions See [IP Modifications & Restrictions](/concepts/ip-asset/ipa-modifications) for the various restrictions on setting the License Config. # License Template Source: https://docs.datafdn.org/concepts/licensing-module/license-template A legal framework, written in code ("programmable"), that defines various licensing terms for an IP A License Template is a legal framework, written in code ("programmable"), that defines various licensing terms for an IP. Such as: * "Is commercial use allowed?" - true/false (bool) * "Is the license transferrable?" - true/false (bool) * "If commercial, what % of royalty do I receive?" - number These terms and values differ per License Template. The first (and currently only) example of a License Template was developed by the DATA Foundation team directly, and is called the Programmable IP License (PIL :pill:). Learn about the first implementation of a License Template View the smart contract for the PIL. ## License Template Requirements License Templates are responsible for: * Providing a link to the actual, off-chain, legal contract template, with all the parameters, their possible values, and the correspondent legalese, in `licenseTextUrl`. * For a licensing framework to be compatible with the DATA Foundation, the legal text **must** be clear and parametrized, with each licensing parameter establishing the possible outcomes of each value. * The parameter values in each License Template (called "License Template terms") drive the legal text for each license agreement. * Defining a `struct` with the particular definitions of the parameters in accordance, which must be encoded into the License Terms struct (described below). * Providing registration methods for the License Terms, and getters. * **Verifying** that both the **minter** and the address **linking a derivative are allowed by the License Template terms to perform those actions**. * These conditions could be enforced by the License Template itself or through hooks. They can range from limitations on the derivative creations, token-gating LNFT holders, creative control from licensors, KYC, etc. It's up to the implementation of each License Template. * **Verifying that the License Terms are compatible if a derivative has or will have multiple parents** ## Create Your Own Template You can create your own License Template (like the PIL), but it must be approved by the DATA Foundation team to be fully embedded into the protocol. # License Terms Source: https://docs.datafdn.org/concepts/licensing-module/license-terms A particular combination of values from a License Template that define how others can interact with your IP When registering your IP on the DATA Foundation, you can attach License Terms to the IP. These are real, legally binding terms enforced on-chain by the [📜 Licensing Module](/concepts/licensing-module/overview), and in the worst case, able to be enforced off-chain in court through traditional means. In them are also terms for commercial usage (ex. "50% of revenue must be shared with the parent IP"). View some popular combinations of PIL License Terms, also known as "flavors". More specifically, License Terms are a particular combination of values from a [License Template](/concepts/licensing-module/license-template). Indeed, there can and will exist **multiple** License Terms (variations) for each License Template. You can imagine that a License Template generates many License Term variations. License Terms Diagram Once registered, **License Terms are immutable: they can't be tampered with or altered**, even by the License Template that generated it. Additionally, License Terms have a unique numeric ID within the License Template they stem from. This makes License Terms reusable, meaning if someone creates License Terms with a specific set of values, it only needs to be created once and can be used by anyone else. For example, a particular set of term values of the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license/overview), such as non-commercial usage + derivatives allowed + free minting, defines a unique License Terms with an associated ID. ## License Terms Attached to IP Asset The owner of a root IP Asset can attach License Terms to signal to other users that they can mint License Tokens of those terms to create a derivative of this IP Asset. **Once License Terms are attached to an IP Asset, it is now considered "public" and anyone can mint a License Token using those terms.** License Terms Attached to IP Asset ## Inherited License Terms On the other hand, derivative IP Assets inherit their License Terms from the parent IP Asset. This means that when an IP Asset registers itself as a derivative, it burns the License Token and inherits the associated License Terms. **The owner of this derivative cannot set new License Terms.** You may be wondering: "if I cannot set new License Terms on my derivative, does that also mean I can't change the minting fee, or disallowing more derivatives, on my derivative?" Thankfully, there is a way to get around this! Although you cannot change License Terms on a derivative IP, you can utilize the [License Config to implement special behaviors](/concepts/licensing-module/license-config). ## Expiration License Terms support an `expiration` time. Once License Terms expire, any derivatives that abide by that license will no longer be able to generate revenue or create further derivatives. If an IP Asset is a derivative of multiple parents, it will expire when the soonest expiration time between the two parents is reached. # License Token Source: https://docs.datafdn.org/concepts/licensing-module/license-token An ERC-721 NFT that allows you to register your IP as a derivative of another, based on the License Terms defined in the token View the smart contract for License Tokens. A **License Token** is represented as an **ERC-721 NFT** and contains the specific [License Terms](/concepts/licensing-module/license-terms) it represents. Its associated `licenseTokenId` is global, as there is one License Token contract. Once License Terms are attached to an IP Asset, it becomes public so that anyone can mint a License Token for those terms. A License Token is burned when it is used to register another IP as a derivative of the original IP Asset. A diagram showing what happens when a License Token is minted. ## Private Licenses A diagram showing how private licenses are minted. In order to mint a private License Token, the owner of a root IP Asset can issue License Tokens that have terms **not yet attached to the IP Asset itself**. It is important to also note that derivative IP Assets cannot issue private licenses because it is restricted to only issue licenses of its inherited terms. ## Transferability of the License Token License Tokens might be transferrable or not, depending on the values of the License Terms terms they point to. Once a non-transferable License Token is minted to a recipient, it is locked there forever. ## Registering a Derivative You can register an IP Asset as a derivative of other IP Assets, each with their own license terms agreement. This creates a legally binding agreement between IP Assets. ### ⚠️ Restrictions There are a few restrictions on registering a derivative: * An IP Asset can only register as a derivative one time. If an IP Asset has multiple parents, it must register both at the same time. * Once an IP Asset is a derivative, it cannot link any more parents. * When you link an IP Asset as a derivative, it cannot have license terms attached. It will inherit its terms from its parents. * None of the parent IP Assets or the child IP Asset can be disputed. * The child IP Asset cannot have derivatives already. * If at least one of the license terms is commercial, then they all must be commercial (`commercialUse = true`) *** There are two ways to register a derivative IP Asset. An IP Asset can only register as a derivative one time. If an IP Asset has multiple parents, it must register both at the same time. Once an IP Asset is a derivative, it cannot link any more parents. ### 1. Using an Existing License Token A License Token is burned when it is used to register another IP as a derivative of the original IP Asset. Using an Existing License Token ### 2. Registering a Derivative Directly You can also register a derivative directly, without the need for a License Token. Remember that if License Terms are attached to an IP Asset it is public to mint the License Token anyway, so this is simply a convenient way to go about it, thus skipping the middle step of minting a License Token. Registering a Derivative Directly # 📜 Licensing Module Source: https://docs.datafdn.org/concepts/licensing-module/overview Learn about creating & attaching real legal license to your IP on the DATA Foundation The Licensing Module allows you to create a real legal license from a **License Template** (which is the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license/overview)) and attach it to your IP Asset. This license, and the **License Terms** that define it, restrict how others can use your IP, commercialize it, and remix it. If License Terms are attached to an IP Asset, anyone can mint a **License Token** (an ERC-721 NFT) from it which acts as the license to use that work based on the terms that define it. This token can then be burned to register a derivative work. This then establishes a parent-child relationship between assets. The owner of an IP Asset owns intellectual property rights such as creating derivatives, being commercially exploited, and being reproduced in different platforms. IP Assets can programmatically grant permissions for any users to exercise those rights with some autonomy via [License Tokens](/concepts/licensing-module/license-token) (an ERC-721 NFT), which point to a particular set of conditions, known as [License Terms](/concepts/licensing-module/license-terms). The contracts in blue are built into the protocol. The contracts in white can be developed by the community or 3rd party vendor. ## LicensingModule View the smart contract for the License Module. The `LicensingModule.sol` contract is the main entry point for the licensing system. It is responsible for: * Attaching License Terms to IP Assets * Minting License Tokens * Registering derivatives * Setting License Configs ## Further Readings The following document will walk through all of the major components of the Licensing Module as shown above: * [License Template](/concepts/licensing-module/license-template) * [License Terms](/concepts/licensing-module/license-terms) * [License Token](/concepts/licensing-module/license-token) * [License Config](/concepts/licensing-module/license-config) # Overview Source: https://docs.datafdn.org/concepts/overview A broad overview of the DATA Foundation's "Proof-of-Creativity" protocol. A piece of Intellectual Property is represented as an [🧩 IP Asset](/concepts/ip-asset) and its associated [⚙️ IP Account](/concepts/ip-asset/ip-account), a smart contract designed to serve as the core identity for each IP. We also have the [📜 Licensing Module](/concepts/licensing-module) to add functionality to IP Assets, like attaching license terms that govern how the IP can be used. IP and licensing are the **rights layer** for data on the DATA Foundation. The license terms you define here are what [Confidential Data Rails (CDR)](/developers/cdr-sdk/overview) uses to gate access to confidential data. Let's briefly introduce the layers mentioned in the above diagram: ## [🧩 IP Asset](/concepts/ip-asset) When you want to bring an IP on-chain, you mint an ERC-721 NFT. This NFT represents **ownership** over your IP. Then, you **register** the NFT in our protocol through the IP Asset Registry. This deploys an [⚙️ IP Account](/concepts/ip-asset/ip-account), effectively creating an "IP Asset". The address of that contract is the identifier for the IP Asset (the `ipId`). The underlying NFT can be traded/sold like any other NFT, and the new owner will own the IP Asset and all revenue associated with it. ## [⚙️ IP Account](/concepts/ip-asset/ip-account) IP Accounts are smart contracts that are tied to an IP Asset, and do two main things: 1. Store the associated IP Asset's data, such as the associated licenses and royalties created from the IP 2. Facilitates the utilization of this data by various modules. For example, licensing, revenue/royalty sharing, remixing, and other critical features are made possible due to the IP Account's programmability. The address of the IP Account is the IP Asset's identifier (the `ipId`). ## [📜 Licensing Module](/concepts/licensing-module) The Licensing Module is the core module that extends the functionality of IP Accounts. It lets you attach license terms to an IP Asset and mint **License Tokens** from it. These terms, and the tokens minted under them, are what define who can use an IP and, via [CDR](/developers/cdr-sdk/overview), who can decrypt data gated behind it. ## Registry The protocol's registries function as a primary directory/storage for the global states of the protocol. Unlike IP Accounts, which manage the state of specific IPs, a registry oversees the broader states of the protocol, for example, the IP Asset Registry tracks every registered IP Asset. ## [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license) The PIL is a real, off-chain legal contract that defines certain **License Terms** for how an IP Asset can be legally licensed. For example, how an IP Asset is commercialized, remixed, or attributed, and who is allowed to do that and under what conditions. We have mapped these same terms on-chain so you can easily attach terms to your IP Asset for others to seamlessly and transparently license your IP. # 💊 Programmable IP License (PIL) Source: https://docs.datafdn.org/concepts/programmable-ip-license/overview The DATA Foundation Programmable IP License - A legal framework for IP licensing on-chain The PIL is a legal off-chain document based on US copyright law created by the DATA Foundation team. The parameters outlined in the PIL (ex. "Commercial Use", "Derivatives Allowed", etc) have been mapped on-chain, which means they can be enforced on-chain via our protocol, bridging code and law and unlocking the benefit of transparent, autonomous, and permission-less smart contracts for the world of intellectual property. Check out the actual PIL legal text. It is very human-readable for a legal text! The PIL is the first and currently only example of a [License Template](/concepts/licensing-module/license-template). A License Template is simply a traditional legal document that has been brought on-chain and contains a set of pre-defined terms that people must set, like: * `commercialUse` - can someone use my work commercially? * `mintingFee` - the cost of minting a license to use my work in your own works. * `derivativesAttribution` - does someone have to credit me in their derivative works? In code, these terms form a struct that represent their legal off-chain counterparts. To see all of the terms defined by the PIL and their associated explanations in code, see [PIL Terms](/concepts/programmable-ip-license/pil-terms). To see example configurations ("flavors") of the PIL, see [PIL Flavors (examples)](/concepts/programmable-ip-license/pil-flavors). ## The Background Story If you just want to get started developing with the PIL, you can skip this section. We designed the DATA Foundation's [📜 Licensing Module](/concepts/licensing-module/overview) to power the expansion of emerging forms of creativity, such as authorized remixes and co-creation. Our protocol can support any media format or project, ranging from user-generated social videos & images to Hollywood-grade collaborative storytelling. Intellectual property owners can permit other parties to use, or build on, their work by granting rights in a license, which can be for profit or for the common good. In the media world, these licenses are generally highly tailored contracts, which vary by media formats and the unique needs of licensors - often requiring unique expertise (via lawyers) and significant resources to create. We searched for a form of a "universal license" that could support these emerging activities at scale. Hat tip to [Creative Commons](https://creativecommons.org/mission/), [Arweave](https://mirror.xyz/0x64eA438bd2784F2C52a9095Ec0F6158f847182d9/AjNBmiD4A4Sw-ouV9YtCO6RCq0uXXcGwVJMB5cdfbhE), A16Z / [Can't Be Evil,](https://a16zcrypto.com/posts/article/introducing-nft-licenses/) The [Token-Bound NFT License](https://james.grimmelmann.net/files/articles/token-bound-nft-license.pdf) and music rights organizations, among others. But we simply couldn't find one framework or agreement robust enough - so with our expert legal counsel (with special thanks to Ghaith Mahmood and Heather Liu) we created one ourselves! **Introducing the Programmable IP License (PIL:pill:)**, the first example of a [License Template](/concepts/licensing-module/license-template) on the protocol. ## Feedback We are excited to collect feedback and collaborate with IP owners to unlock the potential of their works - please let us know what you think! We can be reached at `legal@storyprotocol.xyz`. Check out the actual PIL legal text. It is very human-readable for a legal text! # PIL Flavors (examples) Source: https://docs.datafdn.org/concepts/programmable-ip-license/pil-flavors Pre-configured License Terms for ease of use The [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license/overview) is very configurable, but we support popular pre-configured License Terms (also known as "flavors") for ease of use. We expect these to be the most popular options: PIL Flavor Comparison ## Non-Commercial Social Remixing This flavor is already registered as `licenseTermsId = 1` on our protocol. This is because it doesn't take any inputs, so we registered it ahead of time. Let the world build on and play with your creation. This license allows for endless free remixing while tracking all uses of your work while giving you full credit. Similar to: TikTok plus attribution. ### What others can do? | Others can | Others cannot | | ----------------------------------------------------- | ----------------------------------------------------------------------------------- | | ✅ Remix this work (`derivativesAllowed == true`) | ❌ Commercialize the original and derivative works (`commercialUse == false`) | | ✅ Distribute their remix anywhere | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) | | ✅ Get the license for free (`defaultMintingFee == 0`) | ❌ Claim credit for the original work ("Attribution" is true in the off-chain terms) | ### PIL Term Values * **On-chain**: ```solidity Solidity theme={null} PILTerms({ transferable: true, royaltyPolicy: address(0), defaultMintingFee: 0, expiration: 0, commercialUse: false, commercialAttribution: false, commercializerChecker: address(0), commercializerCheckerData: EMPTY_BYTES, commercialRevShare: 0, commercialRevCeiling: 0, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0, currency: address(0), uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json" }); ``` ```typescript TypeScript theme={null} import { zeroAddress } from "viem"; import { LicenseTerms } from "@story-protocol/core-sdk"; const nonCommercialSocialRemix: LicenseTerms = { transferable: true, royaltyPolicy: zeroAddress, defaultMintingFee: 0n, expiration: 0n, commercialUse: false, commercialAttribution: false, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: 0, commercialRevCeiling: 0n, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, currency: zeroAddress, uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/NCSR.json", }; ``` * **Off-chain:** | Parameter | Options / Tags | | --------------------------------- | --------------------------------------------------------------------------- | | Territory | No restrictions | | Channels of Distribution | No Restriction | | Attribution | True | | Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | | Sublicensable | False | | AI Learning Models | False | | Restriction on Cross-Platform Use | False | | Governing Law | California, USA | | Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution | | Additional License Parameters | None | ## Commercial Use Retain control over reuse of your work, while allowing anyone to appropriately use the work in exchange for the economic terms you set. This is similar to Shutterstock with creator-set rules. ### What others can do? | Others can | Others cannot | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | ✅ Commercialize the original work (`commercialUse == true`) | ❌ Remix this work (`derivativesAllowed == false`) | | ✅ Keep all revenue (`commercialRevShare == 0`) | ❌ Claim credit for the original work (`commercialAttribution == true`) | | | ❌ Get the license for free (`defaultMintingFee` is set) | | | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) | ### PIL Term Values * **On-chain**: ```solidity Solidity theme={null} PILTerms({ transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: MINTING_FEE, // ex. 1000000000000000000 (which means it costs 1 $WIP to mint) expiration: 0, commercialUse: true, commercialAttribution: true, commercializerChecker: address(0), commercializerCheckerData: EMPTY_BYTES, commercialRevShare: 0, commercialRevCeiling: 0, derivativesAllowed: false, derivativesAttribution: false, derivativesApproval: false, derivativesReciprocal: false, derivativeRevCeiling: 0, currency: CURRENCY, // ex. $WIP address uri: "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json" }) ``` ```typescript TypeScript theme={null} import { zeroAddress, parseEther } from "viem"; import { LicenseTerms } from "@story-protocol/core-sdk"; const commercialUse: LicenseTerms = { transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: MINTING_FEE, // ex. parseEther("1") (which means it costs 1 $WIP to mint) expiration: 0n, commercialUse: true, commercialAttribution: true, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: 0, commercialRevCeiling: 0n, derivativesAllowed: false, derivativesAttribution: false, derivativesApproval: false, derivativesReciprocal: false, derivativeRevCeiling: 0n, currency: CURRENCY, // ex. $WIP address uri: "https://github.com/piplabs/pil-document/blob/9a1f803fcf8101a8a78f1dcc929e6014e144ab56/off-chain-terms/CommercialUse.json", }; ``` * **Off-chain** | Parameter | Options / Tags | | --------------------------------- | --------------------------------------------------------------------------- | | Territory | No restrictions | | Channels of Distribution | No Restriction | | Attribution | True | | Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | | Sublicensable | False | | AI Learning Models | False | | Restriction on Cross-Platform Use | False | | Governing Law | California, USA | | Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution | | Additional License Parameters | None | ## Commercial Remix Let the world build on and play with your creation... and earn money together from it! This license allows for endless free remixing while tracking all uses of your work while giving you full credit, with each derivative paying a percentage of revenue to its "parent" IP. ### Example Check out the DATA Foundation's official mascot **Ippy**, which we have registered with commercial remix terms on both [Mainnet](https://explorer.datafdn.org/ipa/0xB1D831271A68Db5c18c8F0B69327446f7C8D0A42) and [Aeneid Testnet](https://aeneid.explorer.datafdn.org/ipa/0x641E638e8FCA4d4844F509630B34c9D524d40BE5). ### What others can do? | Others can | Others cannot | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | ✅ Remix this work (`derivativesAllowed == true`) | ❌ Claim credit for the original work (`commercialAttribution == true`) | | ✅ Commercialize the original and derivative works (`commercialUse == true`) | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) | | ✅ Distribute their remix anywhere | ❌ Keep all revenue (`commercialRevShare` is set) | | | ❌ Get the license for free (`defaultMintingFee` is set) | | | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) | ### PIL Term Values * **On-chain**: ```solidity Solidity theme={null} PILTerms({ transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: MINTING_FEE, // ex. 1000000000000000000 (which means it costs 1 $WIP to mint) expiration: 0, commercialUse: true, commercialAttribution: true, commercializerChecker: address(0), commercializerCheckerData: EMPTY_BYTES, commercialRevShare: COMMERCIAL_REV_SHARE, // ex. 50 * 10 ** 6 (which means 50% of derivative revenue) commercialRevCeiling: 0, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0, currency: CURRENCY, // ex. $WIP address uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json" }); ``` ```typescript TypeScript theme={null} import { zeroAddress, parseEther } from "viem"; import { LicenseTerms } from "@story-protocol/core-sdk"; const commercialRemix: LicenseTerms = { transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: MINTING_FEE, // ex. parseEther("1") (which means it costs 1 $WIP to mint) expiration: 0n, commercialUse: true, commercialAttribution: true, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: COMMERCIAL_REV_SHARE, // ex. 50 (which means 50% of derivative revenue) commercialRevCeiling: 0n, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCeiling: 0n, currency: CURRENCY, // ex. $WIP address uri: "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json", }; ``` * **Off-chain** | Parameter | Options / Tags | | --------------------------------- | --------------------------------------------------------------------------- | | Territory | No restrictions | | Channels of Distribution | No Restriction | | Attribution | True | | Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | | Sublicensable | False | | AI Learning Models | False | | Restriction on Cross-Platform Use | False | | Governing Law | California, USA | | Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution | | Additional License Parameters | None | ## Creative Commons Attribution Let the world build on and play with your creation - including making money. ### What others can do? | Others can | Others cannot | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | ✅ Remix this work (`derivativesAllowed == true`) | ❌ Claim credit for the original work (`commercialAttribution == true`) | | ✅ Commercialize the original and derivative works (`commercialUse == true`) | ❌ Claim credit for any derivative works (`derivativesAttribution == true`) | | ✅ Distribute their remix anywhere | ❌ Claim credit for the original work even non-commercially ("Attribution" is true in the off-chain terms) | | ✅ Get the license for free (`defaultMintingFee == 0`) | | | ✅ Keep all revenue (`commercialRevShare == 0`) | | ### PIL Term Values * **On-chain**: ```solidity Solidity theme={null} PILTerms({ transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: 0, expiration: 0, commercialUse: true, commercialAttribution: true, commercializerChecker: address(0), commercializerCheckerData: EMPTY_BYTES, commercialRevShare: 0, commercialRevCeiling: 0, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCelling: 0, currency: CURRENCY, // ex. $WIP address uri: 'https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json' }); ``` ```typescript TypeScript theme={null} import { zeroAddress } from "viem"; import { LicenseTerms } from "@story-protocol/core-sdk"; const creativeCommonsAttribution: LicenseTerms = { transferable: true, royaltyPolicy: ROYALTY_POLICY, // ex. RoyaltyPolicyLAP address defaultMintingFee: 0n, expiration: 0n, commercialUse: true, commercialAttribution: true, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: 0, commercialRevCeiling: 0n, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: false, derivativesReciprocal: true, derivativeRevCelling: 0n, currency: CURRENCY, // ex. $WIP address uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json", }; ``` * **Off-chain** | Parameter | Options / Tags | | --------------------------------- | --------------------------------------------------------------------------- | | Territory | No restrictions | | Channels of Distribution | No Restriction | | Attribution | True | | Content Standards | No-Hate, Suitable-for-All-Ages, No-Drugs-or-Weapons, No-Pornography | | Sublicensable | False | | AI Learning Models | True | | Restriction on Cross-Platform Use | False | | Governing Law | California, USA | | Alternative Dispute Resolution | Tag: Alternative-Dispute-Resolution Ledger-Authoritative-Dispute-Resolution | | Additional License Parameters | None | # Examples Here are some common examples of royalty flow. *More coming soon!* ## Example 1 Example 1 Royalty Flow ### Explanation Someone registers their Azuki on the DATA Foundation. By default, that IP Asset has Non-Commercial Social Remixing Terms, which specify that anyone can create derivatives of that work but cannot commercialize them. So, someone else creates & registers a remix of that work (IPA2) which inherits those same terms. Someone else then does the same to IPA2, creating & registering IPA3. The owner of IPA1 then decides that others can commercialize the work, but they cannot create derivatives to do so, they must pay a 10 \$WIP minting fee, and they must share 10% of all revenue earned. So, someone wants to commercialize IPA1 by putting it on a t-shirt. They pay the 10 \$WIP minting fee to get a License Token, which represents the license to commercialize IPA1. They then put the image on a t-shirt and sell it. 10% of revenue earned by that t-shirt must be sent on-chain to IPA1. ## Example 2 Example 2 Royalty Flow ### Explanation Someone registers their Azuki on the DATA Foundation. By default, that IP Asset has Non-Commercial Social Remixing Terms, which specify that anyone can create derivatives of that work but cannot commercialize them. So, someone else creates & registers a remix of that work (IPA2) which inherits those same terms. Someone else then does the same to IPA2, creating & registering IPA3. The owner of IPA1 then decides that others can create derivatives of their work and commercialize them, but they must pay a 10 \$WIP minting fee and share 10% of all revenue earned. So, someone wants to commercialize IPA1 by putting it on a t-shirt. They pay the 10 \$WIP minting fee to get a License Token and burn it to create their own derivative, which changes the background color to red. They then put the remixed image on a t-shirt and sell it. 10% of revenue earned by that t-shirt must be sent on-chain to IPA1. A third person wants to commercialize the remix by putting it in a TV advertisement, but they want to change the hair color to white. So, they pay a 10 \$WIP minting fee (of which, 1 \$WIP gets sent back to IPA1) to create their own derivative. They then put the remixed image in a TV ad. 10% of TV advertising revenue earned must be sent on-chain to IPA4, of which 10% will be distributed back to IPA1. # PIL Terms Source: https://docs.datafdn.org/concepts/programmable-ip-license/pil-terms Detailed explanation of all terms available in the Programmable IP License If you haven't already, read the Programmable IP License (PIL💊) overview. Since there are so many possible combinations of the PIL, we have created preset "flavors" for you to use while developing. Check out the actual PIL legal text. It is very human-readable for a legal text! # On-Chain Terms Most PIL terms are on-chain. They are implemented in the `IPILicenseTemplate.sol` contract as a `PILTerms` struct [here](https://github.com/thedatafoundation/protocol-core-v1/blob/main/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol). ```solidity IPILicenseTemplate.sol theme={null} /// @notice This struct defines the terms for a Programmable IP License (PIL). /// These terms can be attached to IP Assets. struct PILTerms { bool transferable; address royaltyPolicy; uint256 defaultMintingFee; uint256 expiration; bool commercialUse; bool commercialAttribution; address commercializerChecker; bytes commercializerCheckerData; uint32 commercialRevShare; uint256 commercialRevCeiling; bool derivativesAllowed; bool derivativesAttribution; bool derivativesApproval; bool derivativesReciprocal; uint256 derivativeRevCeiling; address currency; string uri; } ``` ## Descriptions | Parameter | Values | Description | | --------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transferable` | True/False | If false, the License Token cannot be transferred once it is minted to a recipient address. | | `royaltyPolicy` | Address | The address of the royalty policy contract. | | `defaultMintingFee` | # | The fee to be paid when minting a license. | | `expiration` | # | The expiration period of the license. | | `commercialUse` | True/False | You can make money from using the original IP Asset, subject to limitations below. | | `commercialAttribution` | True/False | If true, people must give credit to the original work in their commercial application (eg. merch) | | `commercializerChecker` | Address | Commercializers that are allowed to commercially exploit the original work. If zero address, then no restrictions are enforced. | | `commercializerCheckerData` | Bytes | The data to be passed to the commercializer checker contract. | | `commercialRevShare` | \[0-100,000,000] | Amount of revenue (from any source, original & derivative) that must be shared with the licensor (a value of 10,000,000 == 10% of revenue share). This will collect all revenue from tokens that are whitelisted in the [RoyaltyModule.sol contract](https://github.com/thedatafoundation/protocol-core-v1/blob/e339f0671c9172a6699537285e32aa45d4c1b57b/contracts/modules/royalty/RoyaltyModule.sol#L50). | | `commercialRevCeiling` | # | If `commercialUse` is set to true, this value determines the maximum revenue you can earn from the original work. | | `derivativesAllowed` | True/False | Indicates whether the licensee can create derivatives of his work or not. | | `derivativesAttribution` | True/False | If true, derivatives that are made must give credit to the original work. | | `derivativesApproval` | True/False | If true, the licensor must approve derivatives of the work. | | `derivativesReciprocal` | True/False | If false, you cannot create a derivative of a derivative. Set this to true to allow indefinite remixing. | | `derivativeRevCeiling` | # | If `commercialUse` is set to true, this value determines the maximum revenue you can earn from derivative works. | | `currency` | Address | The ERC20 token to be used to pay the minting fee. The token must be registered on the DATA Foundation. | | `uri` | String | The URI of the license terms, which can be used to fetch [off-chain license terms](/concepts/programmable-ip-license/pil-terms#off-chain-terms-to-be-included-in-uri-field). | # Off-Chain Terms to Be Included in `uri` Field Some PIL terms must be stored off-chain and passed in the `uri` field above. This is because these terms are often more lengthy and/or descriptive, so it would not make sense to store them on-chain. | Parameter | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `territory` | Limit usage of the IP to certain regions and/or countries. By default, the IP can be used globally. | | `channelsOfDistribution` | Restrict usage of the IP to certain media formats and use in certain channels of distribution. By default, the IP can be used across all possible channels of distribution. Examples: "television", "physical consumer products", "video games", etc. | | `attribution` | If the original author should be credited for usage of the IP. By default, you do not need to provide credit to the original author. | | `contentStandards` | Set content standards around use of the IP. By default, no standards apply. Examples: "No-Hate", "Suitable-for-All-Ages", "No-Drugs-or-Weapons", "No-Pornography". | | `sublicensable` | Derivative works can grant the same rights they received under this license to a 3rd party, without approval from the original licensor. By default, derivatives may not do so. | | `aiLearningModels` | Whether or not the IP can be used to develop AI learning models. By default, the IP **cannot** be used for such development. | | `restrictionOnCrossPlatformUse` | Limit licensing and creation of derivative works solely on the app on which the IP is made available. By default, the IP can be used anywhere. | | `governingLaw` | The laws of a certain jurisdiction by which this license abides. By default, this is California, USA. | | `alternativeDisputeResolution` | Please see section 3.1 (s) [here](https://github.com/piplabs/pil-document/blob/main/pil.pdf). | | `PILUri` | The URI to the PIL legal terms. | | `additionalParameters` | There may be other terms the licensor would like to add and they can do so in this tag. | # Batch Function Calls Source: https://docs.datafdn.org/concepts/spg/batch-spg-function-calls Learn how to batch multiple operations into a single transaction for efficiency ## Background Prior to this point, registering multiple IPs or performing other operations such as minting, attaching licensing terms, and registering derivatives requires separate transactions for each operation. This can be inefficient and costly. To streamline the process, you can batch multiple transactions into a single one. Two solutions are now available for this: 1. **Batch SPG function calls:** Use [SPG's built-in `multicall` function](#1-batch-spg-function-calls-via-built-in-multicall-function). 2. **Batch function calls beyond SPG:** Use the [Multicall3 Contract](#2-batch-function-calls-via-multicall3-contract). *** ## 1. Batch SPG Function Calls via Built-in `multicall` Function SPG includes a `multicall` function that allows you to combine multiple read or write operations into a single transaction. ### Function Definition The `multicall` function accepts an array of encoded call data and returns an array of encoded results corresponding to each function call: ```solidity Solidity theme={null} /// @dev Executes a batch of function calls on this contract. function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results); ``` ### Example Usage Suppose you want to mint multiple NFTs, register them as IPs, and link them as derivatives to some parent IPs. To accomplish this, you can use SPG's `multicall` function to batch the calls to the `mintAndRegisterIpAndMakeDerivative` function. Here's how you might do it: ```solidity Solidity theme={null} // an SPG workflow contract: https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/workflows/DerivativeWorkflows.sol contract DerivativeWorkflows { ... function mintAndRegisterIpAndMakeDerivative( address nftContract, MakeDerivative calldata derivData, IPMetadata calldata ipMetadata, address recipient ) external returns (address ipId, uint256 tokenId) { .... } ... } ``` To batch call `mintAndRegisterIpAndMakeDerivative` using the `multicall` function: ```javascript JavaScript theme={null} // batch mint, register, and make derivatives for multiple IPs await DerivativeWorkflows.multicall([ DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative( nftContract1, derivData1, recipient1, ipMetadata1, ).encodeABI(), DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative( nftContract2, derivData2, recipient2, ipMetadata2, ).encodeABI(), DerivativeWorkflows.contract.methods.mintAndRegisterIpAndMakeDerivative( nftContract3, derivData3, recipient3, ipMetadata3, ).encodeABI(), ... // Add more calls as needed ]); ``` *** ## 2. Batch Function Calls via Multicall3 Contract The Multicall3 contract is not fully compatible with SPG functions that involve SPGNFT minting due to access control and context changes during Multicall execution. For such operations, use [SPG's built-in multicall function.](#1-batch-spg-function-calls-via-built-in-multicall-function) The Multicall3 contract allows you to execute multiple calls within a single transaction and aggregate the results. The [`viem` library](https://viem.sh/docs/contract/multicall#multicall) provides native support for Multicall3. ### Aeneid Testnet Multicall3 Deployment Info (Same address across all EVM chains) ```json theme={null} { "contractName": "Multicall3", "chainId": 1516, "contractAddress": "0xcA11bde05977b3631167028862bE2a173976CA11", "url": "https://aeneid.datanetscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11" } ``` ### Main Functions To batch multiple function calls, you can use the following functions: 1. **`aggregate3`**: Batches calls using the `Call3` struct. 2. **`aggregate3Value`**: Similar to `aggregate3`, but also allows attaching a value to each call. ```solidity Solidity theme={null} /// @notice Aggregate calls, ensuring each returns success if required. /// @param calls An array of Call3 structs. /// @return returnData An array of Result structs. function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); /// @notice Aggregate calls with an attached msg value. /// @param calls An array of Call3Value structs. /// @return returnData An array of Result structs. function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData); ``` ### Struct Definitions * **Call3**: Used in `aggregate3`. * **Call3Value**: Used in `aggregate3Value`. ```solidity Solidity theme={null} struct Call3 { address target; // Target contract to call. bool allowFailure; // If false, the multicall will revert if this call fails. bytes callData; // Data to call on the target contract. } struct Call3Value { address target; bool allowFailure; uint256 value; // Value (in wei) to send with the call. bytes callData; // Data to call on the target contract. } ``` ### Return Type * **Result**: Struct returned by both `aggregate3` and `aggregate3Value`. ```solidity Solidity theme={null} struct Result { bool success; // Whether the function call succeeded. bytes returnData; // Data returned from the function call. } ``` For detailed examples in Solidity, TypeScript, and Python, see the [Multicall3 repository](https://github.com/mds1/multicall/tree/main/examples). ### Limitations For a list of limitations when using Multicall3, refer to the [Multicall3 README](https://github.com/mds1/multicall/blob/main/README.md#batch-contract-writes). ### Additional Resources * [Multicall3 Documentation](https://github.com/mds1/multicall/blob/main/README.md) * [Multicall Documentation from Viem](https://viem.sh/docs/contract/multicall#multicall) ### Full Multicall3 Interface ```solidity Solidity theme={null} interface IMulticall3 { struct Call { address target; bytes callData; } struct Call3 { address target; bool allowFailure; bytes callData; } struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; } struct Result { bool success; bytes returnData; } function aggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes[] memory returnData); function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); function aggregate3Value(Call3Value[] calldata calls) external payable returns (Result[] memory returnData); function blockAndAggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); function getBasefee() external view returns (uint256 basefee); function getBlockHash(uint256 blockNumber) external view returns (bytes32 blockHash); function getBlockNumber() external view returns (uint256 blockNumber); function getChainId() external view returns (uint256 chainId); function getCurrentBlockCoinbase() external view returns (address coinbase); function getCurrentBlockDifficulty() external view returns (uint256 difficulty); function getCurrentBlockGasLimit() external view returns (uint256 gaslimit); function getCurrentBlockTimestamp() external view returns (uint256 timestamp); function getEthBalance(address addr) external view returns (uint256 balance); function getLastBlockHash() external view returns (bytes32 blockHash); function tryAggregate(bool requireSuccess, Call[] calldata calls) external payable returns (Result[] memory returnData); function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) external payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData); } ``` # 📦 SPG (Periphery) Source: https://docs.datafdn.org/concepts/spg/overview Learn about the DATA Foundation Gateway that simplifies interactions with the protocol The DATA Foundation Gateway (SPG) is a group of periphery/utility smart contracts, deployed on our protocol that **allows you to combine independent operations** - like registering an [🧩 IP Asset](/concepts/ip-asset/overview) and attaching License Terms to that IP Asset - **into one transaction to make your life easier**. This was primarily developed to make our [SDK](/sdk-reference) easier to use. For example, this `mintAndRegisterIpAndAttachPILTerms` is one of the functions in the SPG (more specifically in the `LicenseAttachmentWorkflows.sol`) that allows you to mint an NFT, register it as an IP Asset, and attach License Terms to it all in one call: ```solidity LicenseAttachmentWorkflows.sol theme={null} function mintAndRegisterIpAndAttachPILTerms( address spgNftContract, address recipient, WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) ``` ## All Supported Workflows As mentioned above, there are many different functions we have created for you that combine multiple functions into one. We have categorized them into different groups. These groups are called "workflows". Click here to view all of the supported workflows. Click here to view the workflow smart contracts. ## Batching Calls Although the SPG contains certain functions like `mintAndRegisterIpAndAttachPILTerms`, `registerIpAndAttachPILTerms`, and a bunch more, it would be tedious for us to continually update the contract to account for every single combination of possible interactions with an IP Asset. Instead, we have allowed for a "Multicall" mechanism where you can batch transactions how you like. For more info, see [Batch Function Calls](/concepts/spg/batch-spg-function-calls). # Runtime Configuration Source: https://docs.datafdn.org/developers/cdr-sdk/advanced-configuration DATA Foundation API endpoint, DKG state, threshold tuning, system addresses, and Aeneid runtime notes for the CDR SDK. This page covers the release-specific and operational details that sit beyond basic setup. Start with [Setup CDR Client](/developers/cdr-sdk/setup) first. ## DKG State and the DATA Foundation API Endpoint The SDK reads state from two backends: | Backend | Configured by | What it reads | | ---------------------------- | -------------- | ------------------------------------------------------------------------------- | | **EVM** | `publicClient` | CDR contract state: vaults, fees, `maxEncryptedDataSize`, operational threshold | | **DATA Foundation API REST** | `apiUrl` | DKG state: active round, global public key, threshold, validators, attestations | The `apiUrl` is a **required** `CDRClient` parameter. It is the base URL of a DATA Foundation API REST endpoint, and the `observer` uses it for every DKG read. ```typescript theme={null} const client = new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl: "http://172.192.41.96:1317", }); ``` For production deployments, point `apiUrl` at your own Story node's REST gateway rather than the shared endpoint. See [DATA Foundation API REST Endpoint](/developers/cdr-sdk/setup#data-foundation-api-rest-endpoint) for the per-network values. The `observer` caches round-keyed DKG snapshots for rounds in the stable Active and Ended stages, with in-flight request deduplication. The active round itself is always re-fetched, since it can transition at any time. ## Threshold Tuning By default the SDK uses the network's own DKG threshold when combining partial decryptions. You can raise the bar with the optional `minThresholdRatio` parameter, a value in `[0, 1]`: ```typescript theme={null} const client = new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl: "http://172.192.41.96:1317", minThresholdRatio: 0.67, }); ``` The effective threshold becomes `max(network.threshold, ceil(participants * minThresholdRatio))`. Values above `1` would require more partials than there are participants, causing `collectPartials` / `accessCDR` to time out forever. The SDK rejects them. ## Contract Addresses The current Aeneid release uses the following core system addresses: | Contract | Address | | -------- | -------------------------------------------- | | DKG | `0xCcCcCC0000000000000000000000000000000004` | | CDR | `0xCcCcCC0000000000000000000000000000000005` | The SDK already knows these addresses. For DATA Foundation license-gated condition contracts, see [How the DATA Foundation License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-data-foundation-license-read-pattern-works). ## Release Posture and Availability * Aeneid is the current public **testnet** release for the SDK. * Confidentiality depends on the DKG threshold and validator / enclave trust assumptions described in the CDR overview. * Availability depends on enough validators responding before timeout. If reads fail to reach threshold, retry or increase `timeoutMs`. * DKG reads depend on the `apiUrl` DATA Foundation API endpoint being correct and reachable. Point it at a node you trust for production use. ## Current Release Notes * DATA Foundation license-gated vaults still require manual condition encoding. * The CDR SDK does not auto-wrap IP to WIP or auto-approve WIP for license minting. * `accessCDR()` can auto-generate the ephemeral keypair and query `threshold` when those params are omitted. * `createVault` / `readVault` / `createFileVault` / `readFileVault` are available as high-level aliases. * `getRegisteredValidators()` only includes validators whose registration is fully ratified (`status = Finalized`). * `timeoutMs: 120_000` is a good starting point for `accessCDR()` and `downloadFile()`. # Encrypt & Decrypt Source: https://docs.datafdn.org/developers/cdr-sdk/encrypt-and-decrypt Learn how to encrypt a secret and decrypt it using CDR threshold decryption. This guide walks through the two main CDR flows: * `uploadCDR` / `accessCDR` for small secrets stored directly on-chain * `uploadFile` / `downloadFile` for larger encrypted files stored off-chain ### Prerequisites * [CDR SDK setup](/developers/cdr-sdk/setup) complete with WASM initialized and client created ## What Runs On-Chain vs Off-Chain | Operation | Sends transaction? | What happens | | ---------------------------- | ---------------------------- | -------------------------------------------------------------------------- | | `observer.getGlobalPubKey()` | No | Pure read of DKG state over the DATA Foundation API REST endpoint | | `uploadCDR()` | Yes, 2 txs | Local TDH2 encryption plus `allocate()` and `write()` | | `uploadFile()` | Yes, 2 txs + storage upload | Local AES encryption, storage upload, then `allocate()` and `write()` | | `accessCDR()` | Yes, 1 tx | `read()` on-chain, then off-chain partial collection and local combination | | `downloadFile()` | Yes, 1 tx + storage download | `accessCDR()` plus encrypted file download and local AES decryption | ## Encrypt a Secret The diagram below shows the on-chain secret flow: allocate a vault, encrypt the secret locally with TDH2, and write the ciphertext to the vault. CDR encryption flow showing vault allocation, local encryption, and writing the ciphertext to the vault The simplest "owner-only" pattern uses your wallet (EOA) address as both the write and read condition. The CDR contract bypasses the condition check when `msg.sender` equals the configured condition address, so only that wallet can write or read the vault. Because the high-level `uploadCDR()` helper validates that condition addresses point at deployed condition contracts, EOA conditions are configured through the low-level `allocate()` call with `skipConditionValidation: true`. ```typescript theme={null} import { initWasm, uuidToLabel } from "@piplabs/cdr-sdk"; import { toHex } from "viem"; await initWasm(); // Assumes `client` and `walletClient` are already created (see Setup) const { uploader, observer } = client; const walletAddress = walletClient.account!.address; // Pure read: fetch the DKG global public key const globalPubKey = await observer.getGlobalPubKey(); // Encode your secret as bytes const secret = "my confidential data"; const dataKey = new TextEncoder().encode(secret); // On-chain transaction: allocate a vault using the wallet address as the // write AND read condition. Only this EOA can write or read. const { uuid, txHash: allocateTx } = await uploader.allocate({ updatable: false, writeConditionAddr: walletAddress, readConditionAddr: walletAddress, writeConditionData: "0x", readConditionData: "0x", skipConditionValidation: true, }); // Local: TDH2-encrypt the secret, bound to this vault's UUID const label = uuidToLabel(uuid); const ciphertext = await uploader.encryptDataKey({ dataKey, globalPubKey, label, }); // On-chain transaction: write encrypted data to the vault const { txHash: writeTx } = await uploader.write({ uuid, accessAuxData: "0x", encryptedData: toHex(ciphertext.raw), }); console.log(`Vault created with UUID: ${uuid}`); console.log(`Allocate tx: ${allocateTx}`); console.log(`Write tx: ${writeTx}`); ``` Any EOA address works as a write or read condition; only that EOA can perform the matching action. To gate just one side, set your wallet address on that side and a condition contract (such as `LicenseReadCondition`) on the other. The high-level `uploadCDR()` helper expects deployed condition contracts on both sides and does not support EOA conditions, so use it for patterns like DATA Foundation license-gated reads (see [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults)) and use the low-level `allocate()` + `write()` flow above for owner-only EOA conditions. The value of the transaction must be exactly the same as the fee. `dataKey` is the historical parameter name. In `encryptDataKey()` it can be any secret bytes, not just a cryptographic key. Vault encrypted data is limited to **1024 bytes** on Aeneid (`maxEncryptedDataSize`). TDH2 adds overhead, so the maximum plaintext is smaller. For larger content, use `uploadFile()` so only a small `{cid, key}` payload is written to the vault. ## Decrypt a Secret Decryption requires submitting a read request on-chain, collecting partial decryptions from validators, and combining them client-side. CDR decryption flow showing ephemeral key generation, access control check, partial decryptions from validators, and client-side combination ```typescript theme={null} const { consumer } = client; // Sends 1 transaction, then collects partials and combines them locally const { dataKey, txHash } = await consumer.accessCDR({ uuid, accessAuxData: "0x", timeoutMs: 120_000, // wait up to 2 minutes for validators }); const secret = new TextDecoder().decode(dataKey); console.log(`Read tx: ${txHash}`); console.log(`Decrypted secret: ${secret}`); ``` `accessCDR()` auto-generates the ephemeral keypair and auto-queries `globalPubKey` when you omit them. The threshold is derived automatically from the partial-decryption bucket's DKG round. The timeout of the request on the server side is 200 blocks, which is approximately 7 minutes. If you're not able to collect enough partials within this timeout, try another read request. ```typescript theme={null} import { secp256k1 } from "@noble/curves/secp256k1"; import { toHex } from "viem"; const { consumer, observer } = client; const globalPubKey = await observer.getGlobalPubKey(); const recipientPrivKey = secp256k1.utils.randomPrivateKey(); const requesterPubKey = toHex( secp256k1.getPublicKey(recipientPrivKey, false), ); const { dataKey, txHash } = await consumer.accessCDR({ uuid, accessAuxData: "0x", requesterPubKey, recipientPrivKey, globalPubKey, timeoutMs: 120_000, }); console.log(`Read tx: ${txHash}`); console.log(new TextDecoder().decode(dataKey)); ``` ## Encrypt and Download a File CDR encryption flow showing vault allocation, local encryption, and writing the encrypted key plus data URL to the vault CDR decryption flow showing ephemeral key generation, access control check, partial decryptions from validators, and client-side combination Use the file workflow when the encrypted payload should live off-chain and only the encrypted file key plus pointer should be stored in the vault. Upload happens once by the data owner. Download happens later by an authorized reader who recovers the vault payload and then decrypts the stored file. The `uploadFile()` helper requires deployed condition contracts on both sides, so the example below uses DATA Foundation's `OwnerWriteCondition` for the write side and `LicenseReadCondition` for the read side. License token holders can decrypt the file (see [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults) for the end-to-end license setup). For an owner-only file flow, replicate the low-level steps shown earlier with your wallet (EOA) address as both conditions. ```typescript theme={null} import { HeliaProvider } from "@piplabs/cdr-sdk"; import { readFile, writeFile } from "node:fs/promises"; import { createHelia } from "helia"; import { unixfs } from "@helia/unixfs"; import { CID } from "multiformats/cid"; import { encodeAbiParameters } from "viem"; const uploaderAddress = walletClient.account!.address; const OWNER_WRITE_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B"; const LICENSE_READ_CONDITION = "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3"; const LICENSE_TOKEN = "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC"; const writeConditionData = encodeAbiParameters( [{ type: "address" }], [uploaderAddress], ); const readConditionData = encodeAbiParameters( [{ type: "address" }, { type: "address" }], [LICENSE_TOKEN, ipId], ); // Pure read const globalPubKey = await client.observer.getGlobalPubKey(); const helia = await createHelia(); const storage = new HeliaProvider({ helia, unixfs: unixfs(helia), CID: (s) => CID.parse(s), }); const sourceFile = await readFile("./example.pdf"); // Off-chain upload + 2 on-chain transactions const { uuid, cid } = await client.uploader.uploadFile({ content: new Uint8Array(sourceFile), storageProvider: storage, globalPubKey, updatable: false, writeConditionAddr: OWNER_WRITE_CONDITION, readConditionAddr: LICENSE_READ_CONDITION, writeConditionData, readConditionData, accessAuxData: "0x", }); // 1 on-chain read transaction + off-chain download + local AES decryption const { content, txHash } = await client.consumer.downloadFile({ uuid, accessAuxData: "0x", storageProvider: storage, timeoutMs: 120_000, }); console.log(`Stored at CID: ${cid}`); console.log(`Read tx: ${txHash}`); await writeFile("./example.decrypted.pdf", Buffer.from(content)); console.log("Decrypted file written to ./example.decrypted.pdf"); ``` `HeliaProvider` is the only storage backend fully tested on Aeneid in the current release, and it requires Node.js 22+. `uploadFile()` and `downloadFile()` work with raw file bytes. In a browser, start from a `File` object and convert it with `new Uint8Array(await file.arrayBuffer())`. ### Storage Providers The encrypted-file workflow supports four storage backends: * `HeliaProvider` for in-process IPFS. This is the best starting point for development and the only backend fully tested on Aeneid so far. * `GatewayProvider` for an external IPFS HTTP API plus a gateway URL. * `StorachaProvider` for Storacha / `web3.storage`. * `SynapseProvider` for Filecoin-backed storage via Synapse. If you use `HeliaProvider`, pass the `CID.parse` function into the constructor as shown above to avoid class mismatches. ## Step-by-Step (Low-Level) If you need more control over the process, you can call each step individually. These snippets continue from the variables in the examples above: `walletClient`, `globalPubKey`, `requesterPubKey`, `recipientPrivKey`, and `dataKey`. ### Encrypt (Low-Level) ```typescript theme={null} import { uuidToLabel } from "@piplabs/cdr-sdk"; import { toHex } from "viem"; const walletAddress = walletClient.account!.address; // On-chain transaction: allocate a vault using the wallet address as the // write AND read condition. Only this EOA can write or read. const { txHash: allocateTx, uuid } = await uploader.allocate({ updatable: false, writeConditionAddr: walletAddress, readConditionAddr: walletAddress, writeConditionData: "0x", readConditionData: "0x", skipConditionValidation: true, }); // Local: derive the label from the UUID const label = uuidToLabel(uuid); // Local: TDH2 encrypt the secret const ciphertext = await uploader.encryptDataKey({ dataKey, globalPubKey, label, }); // On-chain transaction: write encrypted data to the vault const { txHash: writeTx } = await uploader.write({ uuid, accessAuxData: "0x", encryptedData: toHex(ciphertext.raw), }); ``` ### Decrypt (Low-Level) ```typescript theme={null} import { uuidToLabel } from "@piplabs/cdr-sdk"; // On-chain transaction: submit read request const { txHash: readTx } = await consumer.read({ uuid, accessAuxData: "0x", requesterPubKey, }); // Off-chain: poll the DATA Foundation API endpoint for validator partial decryptions. // The required threshold is derived from the bucket's own DKG round. const partials = await consumer.collectPartials({ uuid, requesterPubKey, // the secp256k1 pubkey used in the read request timeoutMs: 120_000, }); // Pure read: fetch the vault ciphertext const label = uuidToLabel(uuid); const vault = await observer.getVault(uuid); // Local: decrypt each partial, then combine them const recoveredDataKey = await consumer.decryptDataKey({ ciphertext: { raw: Uint8Array.from(Buffer.from(vault.encryptedData.slice(2), "hex")), label, }, partials, recipientPrivKey, globalPubKey, label, }); ``` ## Query DKG State You can query DKG state and fees without a wallet or WASM initialization: ```typescript theme={null} import { createPublicClient, http } from "viem"; import { CDRClient } from "@piplabs/cdr-sdk"; const publicClient = createPublicClient({ transport: http("https://aeneid.datarpc.io"), }); const client = new CDRClient({ network: "testnet", publicClient, apiUrl: "http://172.192.41.96:1317", }); const threshold = await client.observer.getOperationalThreshold(); console.log("Operational threshold:", threshold); const [allocateFee, writeFee, readFee] = await Promise.all([ client.observer.getAllocateFee(), client.observer.getWriteFee(), client.observer.getReadFee(), ]); console.log( `Fees: allocate: ${allocateFee}, write: ${writeFee}, read: ${readFee}`, ); // Query a specific vault const vault = await client.observer.getVault(1); console.log("Vault:", vault); ``` ## Understanding Fees Each CDR operation has an on-chain fee: | Operation | Fee Query | Description | | --------- | --------------------------- | -------------------------------- | | Allocate | `observer.getAllocateFee()` | One-time cost to create a vault | | Write | `observer.getWriteFee()` | Cost per write to a vault | | Read | `observer.getReadFee()` | Cost per read/decryption request | Fees are paid in native tokens (wei) and are sent as `msg.value` with each transaction. # IP Asset Vaults Source: https://docs.datafdn.org/developers/cdr-sdk/ip-asset-vaults Learn how to create CDR vaults backed by IP Assets that require license tokens to decrypt. CDR vaults can be gated behind the DATA Foundation's license tokens so that only license holders can decrypt the vault contents. In the current Aeneid release, this is a manual integration: you encode the CDR condition data yourself and mint DATA Foundation license tokens separately. ### Prerequisites * [CDR SDK setup](/developers/cdr-sdk/setup) complete * `@story-protocol/core-sdk` installed if you plan to mint license tokens in code * Familiarity with [IP Assets](/concepts/ip-asset) and [License Tokens](/concepts/licensing-module/license-token) ## Aeneid Contracts | Contract | Address | | -------------------- | -------------------------------------------- | | OwnerWriteCondition | `0x4C9bFC96d7092b590D497A191826C3dA2277c34B` | | LicenseReadCondition | `0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3` | | LicenseToken | `0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC` | ## How the DATA Foundation License Read Pattern Works Every CDR vault has a `writeConditionAddr` and `readConditionAddr`. For a DATA Foundation license-gated vault on Aeneid: * `writeConditionAddr` usually points at `OwnerWriteCondition`, with `writeConditionData = abi.encode(ownerAddress)` * `readConditionAddr` points at `LicenseReadCondition`, with `readConditionData = abi.encode(licenseTokenAddress, ipId)` * `accessAuxData` at read time is `abi.encode(uint256[] licenseTokenIds)` The CDR SDK does not register the IP Asset or mint the license token for you. Create the IP and obtain its `ipId` with DATA Foundation tooling first, then configure the vault. If you have not registered the asset yet, start with [Register IP Asset](/developers/typescript-sdk/register-ip-asset). If you need to attach or inspect license terms before minting, see [Attach Terms](/developers/typescript-sdk/attach-terms). ## Upload a License-Gated Vault ```typescript theme={null} import { encodeAbiParameters } from "viem"; const globalPubKey = await client.observer.getGlobalPubKey(); const dataKey = new TextEncoder().encode("confidential IP content"); const writeCondData = encodeAbiParameters( [{ type: "address" }], [uploaderAddress], ); const readCondData = encodeAbiParameters( [{ type: "address" }, { type: "address" }], [ "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", ipId, ], ); await client.uploader.uploadCDR({ dataKey, globalPubKey, updatable: false, writeConditionAddr: "0x4C9bFC96d7092b590D497A191826C3dA2277c34B", writeConditionData: writeCondData, readConditionAddr: "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3", readConditionData: readCondData, accessAuxData: "0x", }); ``` The same condition setup works with `uploadFile()` if the encrypted content lives off-chain. ## Mint a License Token Before Reading Before a user can read a DATA Foundation license-gated vault, they still need to mint a license token. The DATA Foundation core SDK's `wipClient` handles the WIP wrap and approval steps so the reader can wrap IP, approve the RoyaltyModule, and mint in three short calls. **WIP** is **Wrapped IP**, the ERC-20 wrapped form of the native `IP` token. the DATA Foundation's royalty / license flows use WIP, so the reader first wraps IP, then approves the RoyaltyModule to spend it. ```typescript theme={null} import { parseEther, http } from "viem"; import { StoryClient } from "@story-protocol/core-sdk"; const ROYALTY_MODULE = "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086"; const storyClient = StoryClient.newClient({ transport: http("https://aeneid.datarpc.io"), account: readerAccount, chainId: "aeneid", }); // 1. Wrap 1 IP → 1 WIP so the mint fee can be paid in WIP. await storyClient.wipClient.deposit({ amount: parseEther("1"), }); // 2. Approve the RoyaltyModule to spend WIP for the mint. await storyClient.wipClient.approve({ spender: ROYALTY_MODULE, amount: parseEther("1"), }); // 3. Mint the DATA Foundation license token. const mintResult = await storyClient.license.mintLicenseTokens({ licensorIpId: ipId, licenseTermsId: BigInt(2054), amount: 1, }); const licenseTokenId = mintResult.licenseTokenIds![0]; ``` `licenseTermsId: 2054` is only an example. Replace it with the license terms ID actually attached to your IP Asset. You receive that ID when you register the asset or attach terms. The wrap, approve, and mint calls above are all on-chain transactions. The later `accessCDR()` call adds one more on-chain read request. ## Read With a License Token At read time, pass the caller's license token ID through `accessAuxData`. ```typescript theme={null} import { encodeAbiParameters } from "viem"; const accessAuxData = encodeAbiParameters( [{ type: "uint256[]" }], [[BigInt(licenseTokenId)]], ); // Sends 1 read transaction, then collects partials and combines locally const { dataKey } = await client.consumer.accessCDR({ uuid, accessAuxData, timeoutMs: 120_000, }); const content = new TextDecoder().decode(dataKey); console.log(`Decrypted IP content: ${content}`); ``` If the caller does not hold a valid license token for the vault's IP Asset, the read request reverts on-chain and validators will not produce partial decryptions. ## Custom Condition Contracts License gating is just one pattern. You can deploy your own condition contract for any access control logic by implementing one or both of these interfaces: ```solidity theme={null} interface ICDRWriteCondition { function checkWriteCondition( uint32 uuid, bytes calldata accessAuxData, bytes calldata conditionData, address caller ) external view returns (bool); } interface ICDRReadCondition { function checkReadCondition( uint32 uuid, bytes calldata accessAuxData, bytes calldata conditionData, address caller ) external view returns (bool); } ``` The CDR contract calls these functions before allowing a `write()` or `read()` operation. Return `true` to allow, `false` to deny. Then pass your contract's address as `readConditionAddr` or `writeConditionAddr` when allocating a vault. # CDR SDK Overview Source: https://docs.datafdn.org/developers/cdr-sdk/overview Learn how to integrate Confidential Data Rails (CDR) into your application using the CDR SDK. These docs track the Aeneid release of `@piplabs/cdr-sdk` (`v0.2.1`), available on npm. Drop-in skill for Claude and other agents, plus three end-to-end examples covering the on-chain secret, encrypted file, and IP-gated flows. The fastest way to get a working CDR integration. The full design behind Confidential Data Rails: cryptography, validator protocol, and threat model. ## What is CDR? **Confidential Data Rails (CDR)** is the DATA Foundation's application layer for threshold-encrypted data on DATA Foundation L1. Under the hood, it uses the validator network's DKG-generated public key so you can encrypt secrets such that no single party ever holds the complete decryption key. Data can only be decrypted when a threshold number of validators collectively provide partial decryptions, with access control enforced on-chain via smart contracts. The validator-side DKG and partial decryption flows run inside `story-kernel` TEEs (Intel SGX enclaves). CDR enables powerful use cases like: * **Secret sharing** - encrypt and share secrets that only specific wallets can decrypt * **Encrypted file delivery** - keep large files off-chain while storing the encrypted file key on-chain * **Data marketplaces** - sell access to encrypted data with on-chain payment enforcement * **IP-gated content** - tie encrypted data to IP Assets and require license tokens to decrypt ## Security and Trust Model * **Confidentiality** - Vault payloads stay encrypted unless a threshold number of validators participate in decryption and the read condition passes. * **Metadata visibility** - Vault UUIDs, condition addresses, transactions, and any off-chain storage pointers you disclose are not hidden by CDR. * **Availability** - Reads can fail if enough validators do not respond before timeout. In that case, retry the read request or increase `timeoutMs`. * **Forward secrecy / revocation** - Treat CDR ciphertext as bound to the access rules and validator set in effect when you encrypted it. If your access model changes, rotate or re-encrypt the content at the application layer. * **Release posture** - The current public release runs on Aeneid testnet. Build and test integrations there, but do not treat it as a production confidentiality environment. ## What Ships in the Aeneid Release The current SDK surface is centered around two workflows: * **Data key vaults** via `uploadCDR` / `accessCDR` for small secrets stored directly on-chain * **Encrypted files** via `uploadFile` / `downloadFile` for off-chain content with on-chain key management The Aeneid release also includes: * `observer`, `uploader`, and `consumer` sub-clients * DKG state reads over the DATA Foundation API REST endpoint (`apiUrl`) * Storage providers for Helia, gateway-backed IPFS, Storacha, and Synapse * Validator registration, attestation queries, and SGX attestation verification utilities ## How It Works CDR revolves around **vaults**. Each vault stores encrypted data and has two configurable access control conditions: * **Write Condition** - determines who can store encrypted data in the vault * **Read Condition** - determines who can request decryption of the vault's data When `msg.sender` equals the configured condition address, the CDR contract bypasses the condition check, so setting your own wallet address as the condition makes a vault owner-only (other callers revert, since an EOA does not implement `checkWriteCondition` / `checkReadCondition`). The SDK validates condition addresses by default; when you intentionally use an EOA this way, call `allocate()` with `skipConditionValidation: true`. There are two common ways to use a vault: * **On-chain secret**: store the encrypted bytes directly in the vault * **Off-chain file**: store an encrypted file in a storage backend and keep the encrypted AES key plus content pointer in the vault ### Data Key Vault Flow 1. **Allocate** a vault on-chain with your desired read/write conditions 2. **Fetch** the DKG global public key from the validator network 3. **Encrypt** your data locally using TDH2 threshold encryption 4. **Write** the encrypted ciphertext to the vault on-chain Walkthrough: [Encrypt a Secret](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-a-secret). ### Encrypted File Flow **Upload (data owner):** 1. **Encrypt** the file locally with an AES key 2. **Upload** the encrypted file to a storage backend such as IPFS 3. **Encrypt** the AES key plus CID through CDR and write the resulting vault payload **Download (authorized reader):** 1. **Read** the vault and recover the AES key payload through threshold decryption 2. **Download** the encrypted file from storage 3. **Decrypt** the file client-side with the recovered AES key Walkthrough: [Encrypt and Download a File](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-and-download-a-file). ### Decryption Flow 1. **Generate** an ephemeral keypair for the decryption session 2. **Submit** a read request on-chain (validated against the read condition) 3. **Collect** partial decryptions from validators until you meet threshold 4. **Combine** the partials client-side to recover the original data key Plaintext encryption and final decryption happen **client-side**. Validators only produce TEE-confined partial decryptions, and neither the CDR contract nor validators ever see your plaintext data. ## Access Control Patterns ### Wallet Address (Simple) Set your wallet address as the read/write condition. Only you can encrypt/decrypt. ```typescript theme={null} await uploader.allocate({ updatable: false, writeConditionAddr: userAddress, // only you can write readConditionAddr: userAddress, // only you can read writeConditionData: "0x", readConditionData: "0x", skipConditionValidation: true, }); ``` For an end-to-end example, see [Encrypt a Secret](/developers/cdr-sdk/encrypt-and-decrypt#encrypt-a-secret). This EOA shortcut is most useful with `allocate()`. The high-level `uploadCDR()` / `uploadFile()` helpers validate condition contracts and therefore use the deployed owner-only condition contract in the examples. ### License Token (IP-Gated) Use the deployed `LicenseReadCondition` contract on Aeneid and encode `abi.encode(licenseTokenAddress, ipId)` as `readConditionData`. The vault writer typically uses the deployed `OwnerWriteCondition` contract so only the uploader can write, while readers must present valid DATA Foundation license token IDs in `accessAuxData`. Technical walkthrough: [How the DATA Foundation License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-data-foundation-license-read-pattern-works). ### Custom Condition Contracts Deploy your own condition contract implementing `checkReadCondition` and `checkWriteCondition` for advanced access control like: * **Fixed fee** - pay a one-time fee to unlock read access * **Time-based** - access only during a specific time window * **Marketplace** - listing owner controls writes, purchasers can read ### Condition Helpers The SDK includes helper encoders for common access patterns: ```typescript theme={null} import { conditions } from "@piplabs/cdr-sdk"; conditions.ownerOnly({ address: conditionAddr, owner: "0x..." }); conditions.custom({ address: conditionAddr, conditionData: "0x..." }); conditions.open({ address: conditionAddr }); conditions.tokenGate({ address: conditionAddr, token: "0x...", minBalance: 1n }); conditions.merkle({ address: conditionAddr, root: "0x..." }); ``` On Aeneid, `ownerOnly()` and `custom()` are the practical built-in patterns today. `open()`, `tokenGate()`, and `merkle()` help encode condition data, but you still need to deploy a matching condition contract yourself. `conditions.storyLicense()` is not available yet. For the deployed DATA Foundation license-gated pattern, see [How the DATA Foundation License Read Pattern Works](/developers/cdr-sdk/ip-asset-vaults#how-the-data-foundation-license-read-pattern-works). ## Next Steps Install the SDK from npm and initialize the client for Aeneid. DKG backends, validation RPCs, system addresses, and release notes. Use both the on-chain secret and encrypted-file workflows. Configure license-gated reads with DATA Foundation license tokens on Aeneid. Full API reference for every CDR SDK method. Install the CDR skill for your AI agent and explore three end-to-end examples. # Setup CDR Client Source: https://docs.datafdn.org/developers/cdr-sdk/setup Learn how to install and configure the CDR SDK. These docs track the Aeneid release of `@piplabs/cdr-sdk` (`v0.2.1`). ### Prerequisites * Node.js 18+ and npm 8+ * Node.js 22+ if you plan to use `HeliaProvider` * A funded wallet on Aeneid testnet * [viem](https://www.npmjs.com/package/viem) (v2.21+) for blockchain interactions ## Install ```bash npm theme={null} npm install @piplabs/cdr-sdk viem ``` ```bash pnpm theme={null} pnpm add @piplabs/cdr-sdk viem ``` ```bash yarn theme={null} yarn add @piplabs/cdr-sdk viem ``` `viem` (v2.21+) is a required peer dependency. Storage providers are optional and pull their own peer dependencies. Install them only for the backend you use: `helia`, `multiformats`, and `@helia/unixfs` for `HeliaProvider`, `@storacha/client` for `StorachaProvider`, or `@filoz/synapse-sdk` for `SynapseProvider`. If you plan to mint DATA Foundation license tokens in an IP-gated flow, also install `@story-protocol/core-sdk`. ## Initialize WASM The CDR SDK uses a WebAssembly module for threshold cryptography. You must initialize it once before performing any encryption or decryption operations. ```typescript theme={null} import { initWasm } from "@piplabs/cdr-sdk"; // Call once at application startup await initWasm(); ``` In a React application, initialize WASM in a provider component or top-level effect so it's ready before any CDR operations are attempted. ## Browser and Bundler Guidance * **Vite / webpack** - Import the SDK from normal ESM application code and call `initWasm()` before the first encryption or decryption. If your SSR build tries to evaluate the SDK server-side, move the import behind a client-only boundary. * **Next.js / SSR** - Keep browser wallet flows in `"use client"` components. For route handlers or scripts that use CDR cryptography, run them in the Node runtime instead of Edge. * **Edge runtime** - The current release is not documented for Edge runtimes. Prefer the browser or Node.js runtime on Aeneid. * **TypeScript** - Use modern ESM resolution. `moduleResolution: "Bundler"` is a good default for browser apps; `moduleResolution: "NodeNext"` fits pure Node ESM projects. ```typescript theme={null} // Next.js route handlers / server actions export const runtime = "nodejs"; ``` ## DATA Foundation API REST Endpoint Every `CDRClient` requires an `apiUrl`: the base URL of a DATA Foundation API REST endpoint. The SDK reads all DKG state (active round, global public key, threshold, participant count, registered validators, and validator attestations) over this REST API. Contract state such as vaults and fees is still read over the EVM `publicClient`. | Network | DATA Foundation API REST URL | Notes | | ------- | ---------------------------- | ------------------------------------------- | | Aeneid | `http://172.192.41.96:1317` | Plain HTTP. May change between deployments. | For production deployments you can point `apiUrl` at your own Story node's REST gateway instead of the shared endpoint. Configure it through an environment variable so it is easy to swap. ## Create the CDR Client The `CDRClient` provides three sub-clients: * **`observer`** - Read-only queries (fees, vault data, DKG state). Always available. * **`uploader`** - Encryption and vault allocation. Requires a `walletClient`. * **`consumer`** - Decryption and read requests. Requires a `walletClient`. ### In React (Wallet Connector) In a React app, you typically get the wallet from a connector like Privy, RainbowKit, or wagmi. Create a read-only `CDRClient` up front, and build a write-capable client on demand from the wallet's provider. ```typescript hooks/use-cdr-client.ts theme={null} import { useMemo } from "react"; import { createPublicClient, createWalletClient, custom, http } from "viem"; import { CDRClient } from "@piplabs/cdr-sdk"; // Example using Privy, adapt for your wallet connector import { usePrivy, useWallets } from "@privy-io/react-auth"; export function useCDRClient() { const { authenticated } = usePrivy(); const { wallets } = useWallets(); const wallet = wallets[0]; // Read-only client, always available const publicClient = useMemo( () => createPublicClient({ transport: http(process.env.NEXT_PUBLIC_RPC_URL) }), [], ); const apiUrl = process.env.NEXT_PUBLIC_DATAFDN_API_URL!; const client = useMemo( () => new CDRClient({ network: "testnet", publicClient, apiUrl }), [publicClient, apiUrl], ); // Write client, created on demand from the wallet's provider const getWriteClient = async () => { if (!wallet) throw new Error("No wallet connected"); const provider = await wallet.getEthereumProvider(); const walletClient = createWalletClient({ transport: custom(provider), account: wallet.address as `0x${string}`, }); return new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl, }); }; return { client, publicClient, getWriteClient, address: wallet?.address }; } ``` Then in your components: ```typescript theme={null} const { client, getWriteClient } = useCDRClient(); // Read-only operations work immediately const vault = await client.observer.getVault(42); // Write operations: get a write client first const writeClient = await getWriteClient(); await writeClient.uploader.write({ uuid, accessAuxData: "0x", encryptedData }); ``` ### With Private Key (Backend / Scripts) For server-side code, scripts, or CLI tools, you can use a private key directly: ```typescript theme={null} import { createPublicClient, createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { CDRClient } from "@piplabs/cdr-sdk"; const account = privateKeyToAccount(`0x${process.env.WALLET_PRIVATE_KEY}`); const publicClient = createPublicClient({ transport: http(process.env.RPC_PROVIDER_URL), }); const walletClient = createWalletClient({ account, transport: http(process.env.RPC_PROVIDER_URL), }); const client = new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl: process.env.DATAFDN_API_URL!, }); ``` ### Read-Only (No Wallet) If you only need to query vault data or DKG state, you can omit the `walletClient`: ```typescript theme={null} const client = new CDRClient({ network: "testnet", publicClient, apiUrl: process.env.DATAFDN_API_URL!, }); // observer methods work without a wallet const vault = await client.observer.getVault(123); const allocateFee = await client.observer.getAllocateFee(); ``` Attempting to use `client.uploader` or `client.consumer` without a `walletClient` will throw a `WalletClientRequiredError`. ## Network Configuration ### Supported Network | Network | `network` param | Default RPC URL | DATA Foundation API REST URL | Description | | ------- | --------------- | --------------------------- | ---------------------------- | ------------------------- | | Aeneid | `"testnet"` | `https://aeneid.datarpc.io` | `http://172.192.41.96:1317` | Current supported release | ```typescript Testnet theme={null} const publicClient = createPublicClient({ transport: http("https://aeneid.datarpc.io"), }); const client = new CDRClient({ network: "testnet", publicClient, apiUrl: "http://172.192.41.96:1317", }); ``` ### Custom RPC URL You can point the SDK to any Aeneid-compatible RPC endpoint by changing the `http()` transport URL. This is useful for third-party RPC providers with higher rate limits. The `apiUrl` is configured independently; point it at the shared DATA Foundation API endpoint or your own Story node's REST gateway. ```typescript theme={null} const publicClient = createPublicClient({ transport: http("https://your-aeneid-rpc.example.com"), }); const walletClient = createWalletClient({ account, transport: http("https://your-aeneid-rpc.example.com"), }); // Use "testnet" const client = new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl: "http://172.192.41.96:1317", }); ``` ### Using Environment Variables A common pattern is to configure the network via environment variables: ```typescript config.ts theme={null} const RPC_URL = process.env.RPC_URL ?? "https://aeneid.datarpc.io"; const DATAFDN_API_URL = process.env.DATAFDN_API_URL ?? "http://172.192.41.96:1317"; const NETWORK = (process.env.NETWORK ?? "testnet") as "testnet"; const publicClient = createPublicClient({ transport: http(RPC_URL) }); const client = new CDRClient({ network: NETWORK, publicClient, apiUrl: DATAFDN_API_URL, }); ``` ```bash .env theme={null} # Testnet (default) RPC_URL=https://aeneid.datarpc.io DATAFDN_API_URL=http://172.192.41.96:1317 NETWORK=testnet # Alternate Aeneid RPC RPC_URL=https://your-aeneid-rpc.example.com DATAFDN_API_URL=http://your-data-node:1317 NETWORK=testnet ``` ## Quick Start: End-to-End Secret Example The script below creates an owner-only vault, writes a small secret, then reads it back with the same wallet. It is fully runnable once `WALLET_PRIVATE_KEY` is set. ```typescript quickstart-cdr.ts theme={null} import { CDRClient, initWasm, uuidToLabel } from "@piplabs/cdr-sdk"; import { createPublicClient, createWalletClient, http, toHex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const RPC_URL = process.env.RPC_URL ?? "https://aeneid.datarpc.io"; const DATAFDN_API_URL = process.env.DATAFDN_API_URL ?? "http://172.192.41.96:1317"; const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY as `0x${string}` | undefined; if (!PRIVATE_KEY) { throw new Error("Set WALLET_PRIVATE_KEY before running this script."); } const account = privateKeyToAccount(PRIVATE_KEY); const publicClient = createPublicClient({ transport: http(RPC_URL) }); const walletClient = createWalletClient({ account, transport: http(RPC_URL), }); await initWasm(); const client = new CDRClient({ network: "testnet", publicClient, walletClient, apiUrl: DATAFDN_API_URL, }); // Use the wallet (EOA) address as both write and read condition. Only this // EOA can encrypt to or decrypt from the vault. const { uuid, txHash: allocateTx } = await client.uploader.allocate({ updatable: false, writeConditionAddr: account.address, readConditionAddr: account.address, writeConditionData: "0x", readConditionData: "0x", skipConditionValidation: true, }); const globalPubKey = await client.observer.getGlobalPubKey(); const ciphertext = await client.uploader.encryptDataKey({ dataKey: new TextEncoder().encode("hello from CDR"), globalPubKey, label: uuidToLabel(uuid), }); const { txHash: writeTx } = await client.uploader.write({ uuid, accessAuxData: "0x", encryptedData: toHex(ciphertext.raw), }); console.log("Vault UUID:", uuid); console.log("Allocate tx:", allocateTx); console.log("Write tx:", writeTx); const { dataKey, txHash } = await client.consumer.accessCDR({ uuid, accessAuxData: "0x", timeoutMs: 120_000, }); console.log("Read tx:", txHash); console.log("Recovered secret:", new TextDecoder().decode(dataKey)); ``` This example sends three transactions total: `allocate()`, `write()`, and `read()`. For larger payloads, switch to `uploadFile()` / `downloadFile()` with deployed condition contracts (such as the DATA Foundation license-gated pattern in [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults)). Any EOA address works as a write or read condition; only that EOA can perform the matching action. The high-level `uploadCDR()` / `uploadFile()` helpers validate that condition addresses point at deployed contracts, so EOA conditions go through the low-level `allocate()` call with `skipConditionValidation: true`. ## Next Steps * For network-side runtime behavior, see [Runtime Configuration](/developers/cdr-sdk/advanced-configuration). * For the main integration flows, continue to [Encrypt & Decrypt](/developers/cdr-sdk/encrypt-and-decrypt). ## Error Handling The SDK throws typed errors you can catch and handle: | Error Class | Code | When | | ------------------------------- | ---------------------------- | -------------------------------------------------------------------------- | | `CDRError` | varies | Base class for all SDK-specific errors | | `WalletClientRequiredError` | `WALLET_CLIENT_REQUIRED` | Accessing `uploader` or `consumer` without a `walletClient` | | `InvalidParamsError` | `INVALID_PARAMS` | Invalid parameter combinations, such as only passing one keypair parameter | | `InvalidConditionContractError` | `INVALID_CONDITION_CONTRACT` | Condition address does not implement the required interface | | `LabelMismatchError` | `LABEL_MISMATCH` | Ciphertext label does not match the vault UUID | | `ContentSizeExceededError` | `CONTENT_SIZE_EXCEEDED` | Encrypted data exceeds `maxEncryptedDataSize` | | `EmptyVaultError` | `EMPTY_VAULT` | Reading a vault that has never been written to | | `PartialCollectionTimeoutError` | `PARTIAL_COLLECTION_TIMEOUT` | `collectPartials` or `accessCDR` times out waiting for validator responses | | `CidIntegrityError` | `CID_INTEGRITY` | Downloaded encrypted file does not match the vault CID | On-chain transaction reverts (for example, a failed condition check) surface as the underlying `viem` contract errors, not a CDR-specific error class. All errors extend `CDRError`, which has a `code` property for programmatic handling: ```typescript theme={null} import { CDRError, PartialCollectionTimeoutError } from "@piplabs/cdr-sdk"; try { const { dataKey } = await client.consumer.accessCDR({ ... }); } catch (err) { if (err instanceof PartialCollectionTimeoutError) { console.error("Not enough validators responded in time. Try increasing timeoutMs."); } else if (err instanceof CDRError) { console.error(`CDR error [${err.code}]: ${err.message}`); } } ``` # Deployed Smart Contracts Source: https://docs.datafdn.org/developers/deployed-smart-contracts A list of all deployed protocol addresses ## Core Protocol Contracts * View contracts on our GitHub [here](https://github.com/thedatafoundation/protocol-core-v1/tree/main) ```json Aeneid Testnet theme={null} { "AccessController": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", "ArbitrationPolicyUMA": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", "CoreMetadataModule": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", "CoreMetadataViewModule": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20", "DisputeModule": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", "EvenSplitGroupPool": "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89", "GroupNFT": "0x4709798FeA84C84ae2475fF0c25344115eE1529f", "GroupingModule": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", "IPAccountImplBeacon": "0x9825cc7A398D9C3dDD66232A8Ec76d5b05422581", "IPAccountImplBeaconProxy": "0x00b800138e4D82D1eea48b414d2a2A8Aee9A33b1", "IPAccountImpl": "0xdeC03e0c63f800efD7C9d04A16e01E80cF57Bf79", "IPAssetRegistry": "0x77319B4031e6eF1250907aa00018B8B1c67a244b", "IPGraphACL": "0x1640A22a8A086747cD377b73954545e2Dfcc9Cad", "IpRoyaltyVaultBeacon": "0x6928ba25Aa5c410dd855dFE7e95713d83e402AA6", "IpRoyaltyVaultImpl": "0xbd0f3c59B6f0035f55C58893fA0b1Ac4aDEa50Dc", "LicenseRegistry": "0x529a750E02d8E2f15649c13D69a465286a780e24", "LicenseToken": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", "LicensingModule": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", "ModuleRegistry": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", "PILicenseTemplate": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", "ProtocolAccessManager": "0xFdece7b8a2f55ceC33b53fd28936B4B1e3153d53", "ProtocolPauseAdmin": "0xdd661f55128A80437A0c0BDA6E13F214A3B2EB24", "RoyaltyModule": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", "RoyaltyPolicyLAP": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", "RoyaltyPolicyLRP": "0x9156e603C949481883B1d3355c6f1132D191fC41" } ``` ```json Mainnet theme={null} { "AccessController": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", "ArbitrationPolicyUMA": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", "CoreMetadataModule": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", "CoreMetadataViewModule": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20", "DisputeModule": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", "EvenSplitGroupPool": "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89", "GroupNFT": "0x4709798FeA84C84ae2475fF0c25344115eE1529f", "GroupingModule": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", "IPAccountImplBeacon": "0x9825cc7A398D9C3dDD66232A8Ec76d5b05422581", "IPAccountImplBeaconProxy": "0x00b800138e4D82D1eea48b414d2a2A8Aee9A33b1", "IPAccountImpl": "0x7343646585443F1c3F64E4F08b708788527e1C77", "IPAssetRegistry": "0x77319B4031e6eF1250907aa00018B8B1c67a244b", "IPGraphACL": "0x1640A22a8A086747cD377b73954545e2Dfcc9Cad", "IpRoyaltyVaultBeacon": "0x6928ba25Aa5c410dd855dFE7e95713d83e402AA6", "IpRoyaltyVaultImpl": "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298", "LicenseRegistry": "0x529a750E02d8E2f15649c13D69a465286a780e24", "LicenseToken": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", "LicensingModule": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", "ModuleRegistry": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", "PILicenseTemplate": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", "ProtocolAccessManager": "0xFdece7b8a2f55ceC33b53fd28936B4B1e3153d53", "ProtocolPauseAdmin": "0xdd661f55128A80437A0c0BDA6E13F214A3B2EB24", "RoyaltyModule": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", "RoyaltyPolicyLAP": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", "RoyaltyPolicyLRP": "0x9156e603C949481883B1d3355c6f1132D191fC41" } ``` ## Periphery Contracts * View contracts on our GitHub [here](https://github.com/thedatafoundation/protocol-periphery-v1) ```json Aeneid Testnet theme={null} { "DerivativeWorkflows": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", "GroupingWorkflows": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", "LicenseAttachmentWorkflows": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", "OwnableERC20Beacon": "0xB83639aF55F03108091020b7c75a46e2eaAb4FfA", "OwnableERC20Template": "0xf8D299af9CBEd49f50D7844DDD1371157251d0A7", "RegistrationWorkflows": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", "RoyaltyTokenDistributionWorkflows": "0xa38f42B8d33809917f23997B8423054aAB97322C", "RoyaltyWorkflows": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", "SPGNFTBeacon": "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218", "SPGNFTImpl": "0x5266215a00c31AaA2f2BB7b951Ea0028Ea8b4e37", "TokenizerModule": "0xAC937CeEf893986A026f701580144D9289adAC4C" } ``` ```json Mainnet theme={null} { "DerivativeWorkflows": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", "GroupingWorkflows": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", "LicenseAttachmentWorkflows": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", "OwnableERC20Beacon": "0x9a81C447C0b4C47d41d94177AEea3511965d3Bc9", "OwnableERC20Template": "0xE6505ffc5A7C19B68cEc2311Cc35BC02d8f7e0B1", "RegistrationWorkflows": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", "RoyaltyTokenDistributionWorkflows": "0xa38f42B8d33809917f23997B8423054aAB97322C", "RoyaltyWorkflows": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", "SPGNFTBeacon": "0xD2926B9ecaE85fF59B6FB0ff02f568a680c01218", "SPGNFTImpl": "0x6Cfa03Bc64B1a76206d0Ea10baDed31D520449F5", "TokenizerModule": "0xAC937CeEf893986A026f701580144D9289adAC4C" } ``` ## License Hooks * View contracts on our GitHub [here](https://github.com/thedatafoundation/protocol-periphery-v1/tree/main/contracts/hooks) ```json Aeneid Testnet theme={null} { "LockLicenseHook": "0x54C52990dA304643E7412a3e13d8E8923cD5bfF2", "TotalLicenseTokenLimitHook": "0xaBAD364Bfa41230272b08f171E0Ca939bD600478" } ``` ```json Mainnet theme={null} { "LockLicenseHook": "0x5D874d4813c4A8A9FB2AB55F30cED9720AEC0222", "TotalLicenseTokenLimitHook": "0xB72C9812114a0Fc74D49e01385bd266A75960Cda" } ``` ## Whitelisted Revenue Tokens The below list contains the whitelisted revenue tokens that can be used in the Royalty Module. | Token | Contract Address | Explorer | Mint | | :----- | :------------------------------------------- | :----------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | | WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://aeneid.datanetscan.io/address/0x1514000000000000000000000000000000000000) | N/A | | MERC20 | `0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E` | [View here ↗️](https://aeneid.datanetscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) | [Mint ↗️](https://aeneid.datanetscan.io/address/0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E?tab=write_contract#0x40c10f19) | | Token | Contract Address | Explorer | Mint | | :---- | :------------------------------------------- | :-------------------------------------------------------------------------------------------- | :--- | | WIP | `0x1514000000000000000000000000000000000000` | [View here ↗️](https://www.datanetscan.io/address/0x1514000000000000000000000000000000000000) | N/A | ## Misc * **Multicall3**: 0xcA11bde05977b3631167028862bE2a173976CA11 * **Default License Terms ID** (Non-Commercial Social Remixing): 1 * **Bridged USDC (Stargate)**: 0xF1815bd50389c46847f0Bda824eC8da914045D14 We only support the above USDC on DATA Network. ## Ecosystem Official Contracts The below is a list of official ecosystem contracts. ### DATA Foundation ENS ```json Aeneid Testnet theme={null} { "SidRegistry": "0x5dC881dDA4e4a8d312be3544AD13118D1a04Cb17", "PublicResolver": "0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD" } ``` ```json Mainnet theme={null} { "SidRegistry": "0x5dC881dDA4e4a8d312be3544AD13118D1a04Cb17", "PublicResolver": "0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD" } ``` # Global Wallet Source: https://docs.datafdn.org/developers/global-wallet/overview Use DATA Foundation's Global Wallet to enable Dynamic social login in your app. The DATA Foundation Global Wallet is a Dynamic-powered wallet experience that you can enable by adding a single import to your app. ## Integrate Install the package in your app (see the [npm page](https://www.npmjs.com/package/@story-protocol/global-wallet)): ```bash npm theme={null} npm install @story-protocol/global-wallet ``` Add this import at the top of your root client component: ```tsx theme={null} import "@story-protocol/global-wallet/story"; ``` ## What Users Get After the import is in place, “DATA Foundation Global Wallet” is automatically added to the wallet connection UI wallet list. Users can then log in via socials using Dynamic. DATA Foundation Global Wallet connecting screen DATA Foundation Global Wallet login screen ## Examples See the reference implementations below. Make sure to read the [README](https://github.com/piplabs/story-global-wallet/blob/main/README.md) to get them up and running. * [Dynamic wallet + Next.js](https://github.com/piplabs/story-global-wallet/tree/main/examples/dynamic-nextjs) * [RainbowKit + Vite](https://github.com/piplabs/story-global-wallet/tree/main/examples/rainbowkit-vite) # Dev Overview Source: https://docs.datafdn.org/developers/overview For developers who want to build on our protocol. If you're a developer, here is everything you need: Can't find something? Ask the writer of our docs for help in our [Builder Discord](https://discord.gg/databuilders). View all testnet block & transaction data on the DATA Foundation. View testnet transaction data specifically related to IP interactions like registering, licensing, etc. Start building on the DATA Foundation quickly. ## Confidential Data Rails (CDR) CDR is the primary way to encrypt, store, and gate access to confidential data on the DATA Foundation. * [CDR SDK Guide](/developers/cdr-sdk/overview) - a step-by-step walkthrough of the `@piplabs/cdr-sdk`, from setup to encrypting data and gating it on IP Asset licenses ***(includes working code)*** * [CDR SDK Reference](/sdk-reference/cdr/overview) - detailed **explanations and examples** for every function in the CDR SDK ## Trace [Trace](/trace/overview) is the data-provenance and audit layer. Providers integrate over a simple REST API. See the [Trace Integration Guide](/trace/integration). ## IP & Licensing SDK The rights layer that CDR builds on: * [SDK Reference](/sdk-reference) - view the entire SDK reference with detailed **explanations and examples** for each function ***(includes working code so you can jump right to coding)*** * [TypeScript SDK Guide](/developers/typescript-sdk/overview) - a detailed, step-by-step walkthrough of how to register IP, attach terms, and mint licenses ***(includes working code so you can jump right to coding)*** * [Tutorials](/developers/tutorials/how-to-register-ip) - more specific topics like "How do I register music on the DATA Foundation?" and "How do I implement wallet-less onboarding with the SDK?" ***(includes working code so you can jump right to coding)*** ## Smart Contracts Check out the following resources to learn the protocol: * [Smart Contract Guide](/developers/smart-contracts-guide/overview) - a walkthrough of how to set up and implement the most popular uses of the protocol ***(includes working code so you can jump right to coding)*** * [Deployed Smart Contracts](/developers/deployed-smart-contracts) - all the deployed protocol addresses Do not use `RANDAO` for pseudo-randomness, instead use onchain VRF (Pyth or Gelato). Currently, `RANDAO` value is set as the parent block hash and thus is not random for X-1 block. ## API View our [API Reference](/api-reference). # React Guide Source: https://docs.datafdn.org/developers/react-guide/overview Learn how to integrate the TypeScript SDK to work with React-based apps. The best way to get started is to get your hands dirty and start building. A working code example that shows setting up & calling TypeScript SDK functions in Next.js/React. View the whole SDK reference, which shows examples and types for every function in our SDK. In the following series of tutorials, you will learn how to setup the TypeScript SDK in React. # Dynamic Setup Source: https://docs.datafdn.org/developers/react-guide/setup/dynamic-setup Learn how to setup Dynamic Wallet in your DATA Foundation DApp. **Optional: Official Dynamic Docs** Check out the official Wagmi + Dynamic installation docs [here](https://docs.dynamic.xyz/react-sdk/using-wagmi). ## Install the Dependencies ```bash npm theme={null} npm install --save @story-protocol/core-sdk viem wagmi @dynamic-labs/sdk-react-core @dynamic-labs/wagmi-connector @dynamic-labs/ethereum @tanstack/react-query ``` ```bash pnpm theme={null} pnpm install @story-protocol/core-sdk viem ``` ```bash yarn theme={null} yarn add @story-protocol/core-sdk viem ``` ## Setup Before diving into the example, make sure you have two things setup: 1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file. * You can use the public default one (`https://aeneid.datarpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs). 2. Make sure to have `NEXT_PUBLIC_DYNAMIC_ENV_ID` set up in your `.env` file. Do this by logging into [Dynamic](https://app.dynamic.xyz/) and creating a project. ```jsx Web3Providers.tsx theme={null} "use client"; import { createConfig, WagmiProvider } from "wagmi"; import { http } from 'viem'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core"; import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector"; import { EthereumWalletConnectors } from "@dynamic-labs/ethereum"; import { PropsWithChildren } from "react"; import { aeneid } from "@story-protocol/core-sdk"; // setup wagmi const config = createConfig({ chains: [aeneid], multiInjectedProviderDiscovery: false, transports: { [aeneid.id]: http(), }, }); const queryClient = new QueryClient(); export default function Web3Providers({ children }: PropsWithChildren) { return ( // setup dynamic {children} ); } ``` ```jsx layout.tsx theme={null} import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { PropsWithChildren } from "react"; import Web3Providers from "./Web3Providers"; import { DynamicWidget } from "@dynamic-labs/sdk-react-core"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Example", description: "This is an Example DApp", }; export default function RootLayout({ children }: PropsWithChildren) { return ( {children} ); } ``` ```jsx TestComponent.tsx theme={null} import { custom, toHex } from 'viem'; import { useWalletClient } from "wagmi"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; // example of how you would now use the fully setup sdk export default function TestComponent() { const { data: wallet } = useWalletClient(); async function setupStoryClient(): Promise { const config: StoryConfig = { wallet: wallet, transport: custom(wallet!.transport), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const client = await setupStoryClient(); const response = await client.ipAsset.registerIpAsset({ nft: { type: 'minted', nftContract: '0x01...', tokenId: '1', } ipMetadata: { ipMetadataURI: "test-metadata-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "test-nft-metadata-uri", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), } }); console.log( `Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}` ); } return ( {/* */} ) } ``` # React Setup Source: https://docs.datafdn.org/developers/react-guide/setup/overview Learn how to setup the TypeScript SDK in React. We do not have a specific React SDK, however **we can use the TypeScript SDK in React** all the same, to delay signing & sending transactions to a JSON-RPC account like Metamask. We recommend using [wagmi](https://wagmi.sh/) as a Web3 provider and then installing a wallet provider like Dynamic or RainbowKit. We provide examples for all of the following: * [Dynamic Setup](/developers/react-guide/setup/dynamic-setup) * [RainbowKit Setup](/developers/react-guide/setup/rainbowkit-setup) * [Reown (WalletConnect) Setup](/developers/react-guide/setup/reown-setup) * [Tomo Setup](/developers/react-guide/setup/tomo-setup) # RainbowKit Setup Source: https://docs.datafdn.org/developers/react-guide/setup/rainbowkit-setup Learn how to setup RainbowKit Wallet in your DATA Foundation DApp. **Optional: Official RainbowKit Docs** Check out the official Wagmi + RainbowKit installation docs [here](https://www.rainbowkit.com/docs/installation). ## Install the Dependencies ```bash npm theme={null} npm install --save @story-protocol/core-sdk @rainbow-me/rainbowkit wagmi viem @tanstack/react-query ``` ```bash pnpm theme={null} pnpm install @story-protocol/core-sdk viem ``` ```bash yarn theme={null} yarn add @story-protocol/core-sdk viem ``` ## Setup Before diving into the example, make sure you have two things setup: 1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file. * You can use the public default one (`https://aeneid.datarpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs). 2. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project. ```jsx Web3Providers.tsx theme={null} "use client"; import "@rainbow-me/rainbowkit/styles.css"; import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { WagmiProvider } from "wagmi"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { PropsWithChildren } from "react"; import { aeneid } from "@story-protocol/core-sdk"; const config = getDefaultConfig({ appName: "Test DATA Foundation App", projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string, chains: [aeneid], ssr: true, // If your dApp uses server side rendering (SSR) }); const queryClient = new QueryClient(); export default function Web3Providers({ children }: PropsWithChildren) { return ( {children} ); } ``` ```jsx layout.tsx theme={null} import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { PropsWithChildren } from "react"; import Web3Providers from "./Web3Providers"; import { ConnectButton } from "@rainbow-me/rainbowkit"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Example", description: "This is an Example DApp", }; export default function RootLayout({ children }: PropsWithChildren) { return ( {children} ); } ``` ```jsx TestComponent.tsx theme={null} import { custom, toHex } from 'viem'; import { useWalletClient } from "wagmi"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; // example of how you would now use the fully setup sdk export default function TestComponent() { const { data: wallet } = useWalletClient(); async function setupStoryClient(): Promise { const config: StoryConfig = { wallet: wallet, transport: custom(wallet!.transport), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const client = await setupStoryClient(); const response = await client.ipAsset.registerIpAsset({ nft: { type: 'minted', nftContract: '0x01...', tokenId: '1', } ipMetadata: { ipMetadataURI: "test-metadata-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "test-nft-metadata-uri", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), } }); console.log( `Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}` ); } return ( {/* */} ) } ``` # Reown (WalletConnect) Setup Source: https://docs.datafdn.org/developers/react-guide/setup/reown-setup Learn how to setup Reown (WalletConnect) in your DATA Foundation DApp. **Optional: Official WalletConnect Docs** Check out the official Wagmi + Reown installation docs [here](https://docs.walletconnect.com/appkit/next/core/installation). ## Install the Dependencies ```bash npm theme={null} npm install --save @story-protocol/core-sdk @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` ```bash pnpm theme={null} pnpm install @story-protocol/core-sdk viem ``` ```bash yarn theme={null} yarn add @story-protocol/core-sdk viem ``` ## Setup Before diving into the example, make sure you have two things setup: 1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file. * You can use the public default one (`https://aeneid.datarpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs). 2. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project. ```jsx config/index.tsx theme={null} import { cookieStorage, createStorage, http } from "@wagmi/core"; import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; import { mainnet, arbitrum } from "@reown/appkit/networks"; import { aeneid } from "@story-protocol/core-sdk"; // Get projectId from https://cloud.reown.com export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID; if (!projectId) { throw new Error("Project ID is not defined"); } export const networks = [aeneid]; //Set up the Wagmi Adapter (Config) export const wagmiAdapter = new WagmiAdapter({ storage: createStorage({ storage: cookieStorage, }), ssr: true, projectId, networks, }); export const config = wagmiAdapter.wagmiConfig; ``` ```jsx context/index.tsx theme={null} 'use client' import { wagmiAdapter, projectId } from '@/config' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createAppKit } from '@reown/appkit/react' import { mainnet, arbitrum } from '@reown/appkit/networks' import React, { type ReactNode } from 'react' import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi' // Set up queryClient const queryClient = new QueryClient() if (!projectId) { throw new Error('Project ID is not defined') } // Set up metadata const metadata = { name: 'appkit-example', description: 'AppKit Example', url: 'https://appkitexampleapp.com', // origin must match your domain & subdomain icons: ['https://avatars.githubusercontent.com/u/179229932'] } // Create the modal const modal = createAppKit({ adapters: [wagmiAdapter], projectId, networks: [mainnet, arbitrum], defaultNetwork: mainnet, metadata: metadata, features: { analytics: true // Optional - defaults to your Cloud configuration } }) function ContextProvider({ children, cookies }: { children: ReactNode; cookies: string | null }) { const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig as Config, cookies) return ( {children} ) } export default ContextProvider ``` ```jsx app/layout.tsx theme={null} import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: ['latin'] }) import { headers } from 'next/headers' // added import ContextProvider from '@/context' export const metadata: Metadata = { title: 'AppKit Example App', description: 'Powered by Reown' } export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { const headersObj = await headers(); const cookies = headersObj.get('cookie') return ( {children} ) } ``` ```jsx TestComponent.tsx theme={null} import { custom, toHex } from 'viem'; import { useWalletClient } from "wagmi"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; // example of how you would now use the fully setup sdk export default function TestComponent() { const { data: wallet } = useWalletClient(); async function setupStoryClient(): Promise { const config: StoryConfig = { wallet: wallet, transport: custom(wallet!.transport), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const client = await setupStoryClient(); const response = await client.ipAsset.registerIpAsset({ nft: { type: 'minted', nftContract: '0x01...', tokenId: '1', } ipMetadata: { ipMetadataURI: "test-metadata-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "test-nft-metadata-uri", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), } }); console.log( `Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}` ); } return ( {/* */} ) } ``` # Tomo Setup Source: https://docs.datafdn.org/developers/react-guide/setup/tomo-setup Learn how to setup TomoEVMKit in your DATA Foundation DApp. **Optional: Official TomoEVMKit Docs** Check out the official Wagmi + TomoEVMKit installation docs [here](https://docs.tomo.inc/tomo-sdk/tomoevmkit/quick-start). ## Install the Dependencies ```bash npm theme={null} npm install --save @story-protocol/core-sdk @tomo-inc/tomo-evm-kit wagmi viem @tanstack/react-query ``` ```bash pnpm theme={null} pnpm install @story-protocol/core-sdk viem @tomo-inc/tomo-evm-kit wagmi @tanstack/react-query ``` ```bash yarn theme={null} yarn add @story-protocol/core-sdk viem @tomo-inc/tomo-evm-kit wagmi @tanstack/react-query ``` ## Setup Before diving into the example, make sure you have two things setup: 1. Make sure to have `NEXT_PUBLIC_RPC_PROVIDER_URL` set up in your `.env` file. * You can use the public default one (`https://aeneid.datarpc.io`) or any other RPC [here](/network/network-info/aeneid#rpcs). 2. Make sure to have `NEXT_PUBLIC_TOMO_CLIENT_ID` set up in your `.env` file. Do this by logging into the [Tomo Dashboard](https://dashboard.tomo.inc/) and creating a project. 3. Make sure to have `NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` set up in your `.env` file. Do this by logging into [Reown (prev. WalletConnect)](https://reown.com/) and creating a project. ```jsx Web3Providers.tsx theme={null} "use client"; import '@tomo-inc/tomo-evm-kit/styles.css'; import { getDefaultConfig, TomoEVMKitProvider } from "@tomo-inc/tomo-evm-kit"; import { WagmiProvider } from "wagmi"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { PropsWithChildren } from "react"; import { aeneid } from "@story-protocol/core-sdk"; const config = getDefaultConfig({ appName: "Test DATA Foundation App", clientId: process.env.NEXT_PUBLIC_TOMO_CLIENT_ID as string, projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string, chains: [aeneid], ssr: true, // If your dApp uses server side rendering (SSR) }); const queryClient = new QueryClient(); export default function Web3Providers({ children }: PropsWithChildren) { return ( {children} ); } ``` ```jsx layout.tsx theme={null} import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { PropsWithChildren } from "react"; import Web3Providers from "./Web3Providers"; import { useConnectModal } from "@tomo-inc/tomo-evm-kit"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Example", description: "This is an Example DApp", }; export default function RootLayout({ children }: PropsWithChildren) { const { openConnectModal } = useConnectModal(); return ( {children} ); } ``` ```jsx TestComponent.tsx theme={null} import { custom, toHex } from 'viem'; import { useWalletClient } from "wagmi"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; // example of how you would now use the fully setup sdk export default function TestComponent() { const { data: wallet } = useWalletClient(); async function setupStoryClient(): Promise { const config: StoryConfig = { wallet: wallet, transport: custom(wallet!.transport), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const client = await setupStoryClient(); const response = await client.ipAsset.registerIpAsset({ nft: { type: 'minted', nftContract: '0x01...', tokenId: '1', } ipMetadata: { ipMetadataURI: "test-metadata-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "test-nft-metadata-uri", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), } }); console.log( `Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}` ); } return ( {/* */} ) } ``` # Using the SDK in React Source: https://docs.datafdn.org/developers/react-guide/using-the-sdk-in-react Learn how to use the SDK in React once you have it set up. Once you have the SDK set up in React, you can use it just as we describe in the [TypeScript SDK Guide](/developers/typescript-sdk/overview). A working code example that shows setting up & calling TypeScript SDK functions in Next.js/React. View the whole SDK reference, which shows examples and types for every function in our SDK. ## Prerequisites 1. Complete the [SDK setup in React](/developers/react-guide/setup/overview) ## Example Here is an example of calling an SDK function in React, which will look the same for any function you use: ```jsx TestComponent.tsx theme={null} import { custom, toHex } from 'viem'; import { useWalletClient } from "wagmi"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; // example of how you would now use the fully setup sdk export default function TestComponent() { const { data: wallet } = useWalletClient(); async function setupStoryClient(): Promise { const config: StoryConfig = { wallet: wallet, transport: custom(wallet!.transport), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const client = await setupStoryClient(); const response = await client.ipAsset.registerIpAsset({ nft: { type: 'mint', spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc', }, ipMetadata: { ipMetadataURI: "test-metadata-uri", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "test-nft-metadata-uri", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), } }); console.log( `Root IPA created at tx hash ${response.txHash}, IPA ID: ${response.ipId}` ); } return ( {/* */} ) } ``` # Releases Source: https://docs.datafdn.org/developers/releases Links to all Story releases # Attach Terms to an IPA Source: https://docs.datafdn.org/developers/smart-contracts-guide/attach-terms Learn how to attach License Terms to an IP Asset in Solidity. Follow the completed code all the way through. This section demonstrates how to attach [License Terms](/concepts/licensing-module/license-terms) to an [IP Asset](/concepts/ip-asset/overview). By attaching terms, users can publicly mint [License Tokens](/concepts/licensing-module/license-token) (the on-chain "license") with those terms from the IP. ## Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup) 2. Create License Terms and have a `licenseTermsId`. You can do that by following the [previous page](/developers/smart-contracts-guide/register-terms). ## Attach License Terms Now that we have created terms and have the associated `licenseTermsId`, we can attach them to an existing IP Asset. Let's create a test file under `test/2_AttachTerms.t.sol` to see it work and verify the results: **Contract Addresses** We have filled in the addresses from the DATA Foundation contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts) ```solidity test/2_AttachTerms.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; // for testing purposes only import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol"; import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { SimpleNFT } from "../src/mocks/SimpleNFT.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/2_AttachTerms.t.sol contract AttachTermsTest is Test { address internal alice = address(0xa11ce); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - IPAssetRegistry IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b); // Protocol Core - LicenseRegistry ILicenseRegistry internal LICENSE_REGISTRY = ILicenseRegistry(0x529a750E02d8E2f15649c13D69a465286a780e24); // Protocol Core - LicensingModule ILicensingModule internal LICENSING_MODULE = ILicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f); // Protocol Core - PILicenseTemplate IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316); // Protocol Core - RoyaltyPolicyLAP address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E; // Revenue Token - MERC20 address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E; SimpleNFT public SIMPLE_NFT; uint256 public tokenId; address public ipId; uint256 public licenseTermsId; function setUp() public { // this is only for testing purposes // due to our IPGraph precompile not being // deployed on the fork vm.etch(address(0x0101), address(new MockIPGraph()).code); SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM"); tokenId = SIMPLE_NFT.mint(alice); ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId); // Register random Commercial Remix terms so we can attach them later licenseTermsId = PIL_TEMPLATE.registerLicenseTerms( PILFlavors.commercialRemix({ mintingFee: 0, commercialRevShare: 10 * 10 ** 6, // 10% royaltyPolicy: ROYALTY_POLICY_LAP, currencyToken: MERC20 }) ); } /// @notice Attaches license terms to an IP Asset. /// @dev Only the owner of an IP Asset can attach license terms to it. /// So in this case, alice has to be the caller of the function because /// she owns the NFT associated with the IP Asset. function test_attachLicenseTerms() public { vm.prank(alice); LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId); assertTrue(LICENSE_REGISTRY.hasIpAttachedLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId)); assertEq(LICENSE_REGISTRY.getAttachedLicenseTermsCount(ipId), 1); (address licenseTemplate, uint256 attachedLicenseTermsId) = LICENSE_REGISTRY.getAttachedLicenseTerms({ ipId: ipId, index: 0 }); assertEq(licenseTemplate, address(PIL_TEMPLATE)); assertEq(attachedLicenseTermsId, licenseTermsId); } } ``` ## Test Your Code! Run `forge build`. If everything is successful, the command should successfully compile. Now run the test by executing the following command: ```bash theme={null} forge test --fork-url https://aeneid.datarpc.io/ --match-path test/2_AttachTerms.t.sol ``` ## Mint a License Congratulations, you attached terms to an IPA! Follow the completed code all the way through. Now that we have attached License Terms to our IP, the next step is minting a License Token, which we'll go over on the next page. # Using an Example Source: https://docs.datafdn.org/developers/smart-contracts-guide/example Combine all of our tutorials together in a practical example. See the completed code. Check out a video walkthrough of this tutorial! # Writing the Smart Contract Now that we have walked through each of the individual steps, let's try to write, deploy, and verify our own smart contract. ## Register IPA, Register License Terms, and Attach to IPA In this first section, we will combine a few of the tutorials into one. We will create a function named `mintAndRegisterAndCreateTermsAndAttach` that allows you to mint & register a new IP Asset, register new License Terms, and attach those terms to an IP Asset. It will also accept a `receiver` field to be the owner of the new IP Asset. ### Prerequisites * Complete [Register an IP Asset](/developers/smart-contracts-guide/register-ip-asset) * Complete [Register License Terms](/developers/smart-contracts-guide/register-terms) * Complete [Attach Terms to an IPA](/developers/smart-contracts-guide/attach-terms) ### Writing Our Contract Create a new file under `./src/Example.sol` and paste the following: **Contract Addresses** In order to get the contract addresses to pass in the constructor, go to [Deployed Smart Contracts](/developers/deployed-smart-contracts). ```solidity src/Example.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; import { SimpleNFT } from "./mocks/SimpleNFT.sol"; import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; /// @notice An example contract that demonstrates how to mint an NFT, register it as an IP Asset, /// attach license terms to it, mint a license token from it, and register it as a derivative of the parent. contract Example is ERC721Holder { IIPAssetRegistry public immutable IP_ASSET_REGISTRY; ILicensingModule public immutable LICENSING_MODULE; IPILicenseTemplate public immutable PIL_TEMPLATE; address public immutable ROYALTY_POLICY_LAP; address public immutable WIP; SimpleNFT public immutable SIMPLE_NFT; constructor( address ipAssetRegistry, address licensingModule, address pilTemplate, address royaltyPolicyLAP, address wip ) { IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry); LICENSING_MODULE = ILicensingModule(licensingModule); PIL_TEMPLATE = IPILicenseTemplate(pilTemplate); ROYALTY_POLICY_LAP = royaltyPolicyLAP; WIP = wip; // Create a new Simple NFT collection SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM"); } /// @notice Mint an NFT, register it as an IP Asset, and attach License Terms to it. /// @param receiver The address that will receive the NFT/IPA. /// @return tokenId The token ID of the NFT representing ownership of the IPA. /// @return ipId The address of the IP Account. /// @return licenseTermsId The ID of the license terms. function mintAndRegisterAndCreateTermsAndAttach( address receiver ) external returns (uint256 tokenId, address ipId, uint256 licenseTermsId) { // We mint to this contract so that it has permissions // to attach license terms to the IP Asset. // We will later transfer it to the intended `receiver` tokenId = SIMPLE_NFT.mint(address(this)); ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId); // register license terms so we can attach them later licenseTermsId = PIL_TEMPLATE.registerLicenseTerms( PILFlavors.commercialRemix({ mintingFee: 0, commercialRevShare: 10 * 10 ** 6, // 10% royaltyPolicy: ROYALTY_POLICY_LAP, currencyToken: WIP }) ); // attach the license terms to the IP Asset LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId); // transfer the NFT to the receiver so it owns the IPA SIMPLE_NFT.transferFrom(address(this), receiver, tokenId); } } ``` ## Mint a License Token and Register as Derivative In this next section, we will combine a few of the later tutorials into one. We will create a function named `mintLicenseTokenAndRegisterDerivative` that allows a potentially different user to register their own "child" (derivative) IP Asset, mint a License Token from the "parent" (root) IP Asset, and register their child IPA as a derivative of the parent IPA. It will accept a few parameters: 1. `parentIpId`: the `ipId` of the parent IPA 2. `licenseTermsId`: the id of the License Terms you want to mint a License Token for 3. `receiver`: the owner of the child IPA ### Prerequisites * Complete [Mint a License Token](/developers/smart-contracts-guide/mint-license) ### Writing Our Contract In your `Example.sol` contract, add the following function at the bottom: ```solidity src/Example.sol theme={null} /// @notice Mint and register a new child IPA, mint a License Token /// from the parent, and register it as a derivative of the parent. /// @param parentIpId The ipId of the parent IPA. /// @param licenseTermsId The ID of the license terms you will /// mint a license token from. /// @param receiver The address that will receive the NFT/IPA. /// @return childTokenId The token ID of the NFT representing ownership of the child IPA. /// @return childIpId The address of the child IPA. function mintLicenseTokenAndRegisterDerivative( address parentIpId, uint256 licenseTermsId, address receiver ) external returns (uint256 childTokenId, address childIpId) { // We mint to this contract so that it has permissions // to register itself as a derivative of another // IP Asset. // We will later transfer it to the intended `receiver` childTokenId = SIMPLE_NFT.mint(address(this)); childIpId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), childTokenId); // mint a license token from the parent uint256 licenseTokenId = LICENSING_MODULE.mintLicenseTokens({ licensorIpId: parentIpId, licenseTemplate: address(PIL_TEMPLATE), licenseTermsId: licenseTermsId, amount: 1, // mint the license token to this contract so it can // use it to register as a derivative of the parent receiver: address(this), royaltyContext: "", // for PIL, royaltyContext is empty string maxMintingFee: 0, maxRevenueShare: 0 }); uint256[] memory licenseTokenIds = new uint256[](1); licenseTokenIds[0] = licenseTokenId; // register the new child IPA as a derivative // of the parent LICENSING_MODULE.registerDerivativeWithLicenseTokens({ childIpId: childIpId, licenseTokenIds: licenseTokenIds, royaltyContext: "", // empty for PIL maxRts: 0 }); // transfer the NFT to the receiver so it owns the child IPA SIMPLE_NFT.transferFrom(address(this), receiver, childTokenId); } ``` # Testing Our Contract Create another new file under `test/Example.t.sol` and paste the following: ```solidity test/Example.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; // for testing purposes only import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol"; import { Example } from "../src/Example.sol"; import { SimpleNFT } from "../src/mocks/SimpleNFT.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/Example.t.sol contract ExampleTest is Test { address internal alice = address(0xa11ce); address internal bob = address(0xb0b); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - IPAssetRegistry address internal ipAssetRegistry = 0x77319B4031e6eF1250907aa00018B8B1c67a244b; // Protocol Core - LicenseRegistry address internal licenseRegistry = 0x529a750E02d8E2f15649c13D69a465286a780e24; // Protocol Core - LicensingModule address internal licensingModule = 0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f; // Protocol Core - PILicenseTemplate address internal pilTemplate = 0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316; // Protocol Core - RoyaltyPolicyLAP address internal royaltyPolicyLAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E; // Revenue Token - WIP address internal wip = 0x1514000000000000000000000000000000000000; SimpleNFT public SIMPLE_NFT; Example public EXAMPLE; function setUp() public { // this is only for testing purposes // due to our IPGraph precompile not being // deployed on the fork vm.etch(address(0x0101), address(new MockIPGraph()).code); EXAMPLE = new Example(ipAssetRegistry, licensingModule, pilTemplate, royaltyPolicyLAP, wip); SIMPLE_NFT = SimpleNFT(EXAMPLE.SIMPLE_NFT()); } function test_mintAndRegisterAndCreateTermsAndAttach() public { ILicenseRegistry LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); IIPAssetRegistry IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry); uint256 expectedTokenId = SIMPLE_NFT.nextTokenId(); address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SIMPLE_NFT), expectedTokenId); (uint256 tokenId, address ipId, uint256 licenseTermsId) = EXAMPLE.mintAndRegisterAndCreateTermsAndAttach(alice); assertEq(tokenId, expectedTokenId); assertEq(ipId, expectedIpId); assertEq(SIMPLE_NFT.ownerOf(tokenId), alice); assertTrue(LICENSE_REGISTRY.hasIpAttachedLicenseTerms(ipId, pilTemplate, licenseTermsId)); assertEq(LICENSE_REGISTRY.getAttachedLicenseTermsCount(ipId), 1); (address licenseTemplate, uint256 attachedLicenseTermsId) = LICENSE_REGISTRY.getAttachedLicenseTerms({ ipId: ipId, index: 0 }); assertEq(licenseTemplate, pilTemplate); assertEq(attachedLicenseTermsId, licenseTermsId); } function test_mintLicenseTokenAndRegisterDerivative() public { ILicenseRegistry LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); IIPAssetRegistry IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry); (uint256 parentTokenId, address parentIpId, uint256 licenseTermsId) = EXAMPLE .mintAndRegisterAndCreateTermsAndAttach(alice); (uint256 childTokenId, address childIpId) = EXAMPLE.mintLicenseTokenAndRegisterDerivative( parentIpId, licenseTermsId, bob ); assertTrue(LICENSE_REGISTRY.hasDerivativeIps(parentIpId)); assertTrue(LICENSE_REGISTRY.isParentIp(parentIpId, childIpId)); assertTrue(LICENSE_REGISTRY.isDerivativeIp(childIpId)); assertEq(LICENSE_REGISTRY.getDerivativeIpCount(parentIpId), 1); assertEq(LICENSE_REGISTRY.getParentIpCount(childIpId), 1); assertEq(LICENSE_REGISTRY.getParentIp({ childIpId: childIpId, index: 0 }), parentIpId); assertEq(LICENSE_REGISTRY.getDerivativeIp({ parentIpId: parentIpId, index: 0 }), childIpId); } } ``` Run `forge build`. If everything is successful, the command should successfully compile. To test this out, simply run the following command: ```bash theme={null} forge test --fork-url https://aeneid.datarpc.io/ --match-path test/Example.t.sol ``` # Deploy & Verify the Example Contract The `--constructor-args` come from [Deployed Smart Contracts](/developers/deployed-smart-contracts). ```bash theme={null} forge create \ --rpc-url https://aeneid.datarpc.io/ \ --private-key $PRIVATE_KEY \ ./src/Example.sol:Example \ --legacy \ --verify \ --verifier blockscout \ --verifier-url https://aeneid.datanetscan.io/api/ \ --constructor-args 0x77319B4031e6eF1250907aa00018B8B1c67a244b 0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f 0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E ``` If everything worked correctly, you should see something like `Deployed to: 0xfb0923D531C1ca54AB9ee10CB8364b23d0C7F47d` in the console. Paste that address into [the explorer](https://aeneid.datanetscan.io/) and see your verified contract! # Great job! :) See the completed code. Check out a video walkthrough of this tutorial! # Mint a License Token Source: https://docs.datafdn.org/developers/smart-contracts-guide/mint-license Learn how to mint a License Token from an IPA in Solidity. Follow the completed code all the way through. This section demonstrates how to mint a [License Token](/concepts/licensing-module/license-token) from an [IP Asset](/concepts/ip-asset/overview). You can only mint a License Token from an IP Asset if the IP Asset has [License Terms](/concepts/licensing-module/license-terms) attached to it. A License Token is minted as an ERC-721. There are two reasons you'd mint a License Token: 1. To hold the license and be able to use the underlying IP Asset as the license described (for ex. "Can use commercially as long as you provide proper attribution and share 5% of your revenue) 2. Use the license token to link another IP Asset as a derivative of it. *Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.* ## Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup) 2. An IP Asset has License Terms attached to it. You can learn how to do that [here](/developers/smart-contracts-guide/attach-terms) ## Mint License Let's say that IP Asset (`ipId = 0x01`) has License Terms (`licenseTermdId = 10`) attached to it. We want to mint 2 License Tokens with those terms to a specific wallet address (`0x02`). **Paid Licenses** Be mindful that some IP Assets may have license terms attached that require the user minting the license to pay a `mintingFee`. Let's create a test file under `test/3_LicenseToken.t.sol` to see it work and verify the results: **Contract Addresses** We have filled in the addresses from the DATA Foundation contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts) ```solidity test/3_LicenseToken.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; // for testing purposes only import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; import { ILicenseToken } from "@storyprotocol/core/interfaces/ILicenseToken.sol"; import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol"; import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { SimpleNFT } from "../src/mocks/SimpleNFT.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/3_LicenseToken.t.sol contract LicenseTokenTest is Test { address internal alice = address(0xa11ce); address internal bob = address(0xb0b); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - IPAssetRegistry IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b); // Protocol Core - LicensingModule ILicensingModule internal LICENSING_MODULE = ILicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f); // Protocol Core - PILicenseTemplate IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316); // Protocol Core - RoyaltyPolicyLAP address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E; // Protocol Core - LicenseToken ILicenseToken internal LICENSE_TOKEN = ILicenseToken(0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC); // Revenue Token - MERC20 address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E; SimpleNFT public SIMPLE_NFT; uint256 public tokenId; address public ipId; uint256 public licenseTermsId; function setUp() public { // this is only for testing purposes // due to our IPGraph precompile not being // deployed on the fork vm.etch(address(0x0101), address(new MockIPGraph()).code); SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM"); tokenId = SIMPLE_NFT.mint(alice); ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId); licenseTermsId = PIL_TEMPLATE.registerLicenseTerms( PILFlavors.commercialRemix({ mintingFee: 0, commercialRevShare: 10 * 10 ** 6, // 10% royaltyPolicy: ROYALTY_POLICY_LAP, currencyToken: MERC20 }) ); vm.prank(alice); LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId); } /// @notice Mints license tokens for an IP Asset. /// Anyone can mint a license token. function test_mintLicenseToken() public { uint256 startLicenseTokenId = LICENSING_MODULE.mintLicenseTokens({ licensorIpId: ipId, licenseTemplate: address(PIL_TEMPLATE), licenseTermsId: licenseTermsId, amount: 2, receiver: bob, royaltyContext: "", // for PIL, royaltyContext is empty string maxMintingFee: 0, maxRevenueShare: 0 }); assertEq(LICENSE_TOKEN.ownerOf(startLicenseTokenId), bob); assertEq(LICENSE_TOKEN.ownerOf(startLicenseTokenId + 1), bob); } } ``` ## Test Your Code! Run `forge build`. If everything is successful, the command should successfully compile. Now run the test by executing the following command: ```bash theme={null} forge test --fork-url https://aeneid.datarpc.io/ --match-path test/3_LicenseToken.t.sol ``` ## Register a Derivative Follow the completed code all the way through. Now that we have minted a License Token, we can hold it or use it to link an IP Asset as a derivative. We will go over that on the next page. # Smart Contract Guide Source: https://docs.datafdn.org/developers/smart-contracts-guide/overview For smart contract developers who wish to build on top of the DATA Foundation directly. In this section, we will briefly go over the protocol contracts and then guide you through how to start building on top of the protocol. If you haven't yet familiarized yourself with the overall architecture, we recommend first going over the [Architecture Overview](/concepts/overview) section. ## Smart Contract Tutorial Skip the tutorial and view the completed code. Follow the README instructions to run the tests, or go to the `/test` folder to view all of the example contracts. **If you want to set things up from scratch**, then continue with the following tutorials, starting with the [Setup Your Own Project](/developers/smart-contracts-guide/setup) step. ## Our Smart Contracts As of the current version, our Proof-of-Creativity Protocol is compatible with all EVM chains and is written as a set of Smart Contracts in Solidity. There are two repositories that you may interact with as a developer: * [DATA Foundation Core](https://github.com/thedatafoundation/protocol-core-v1) - This repository contains the core protocol logic, consisting of a thin IP registry (the IP Asset Registry), a set of modules defining logic around [Licensing](/concepts/licensing-module/overview), metadata, and a module manager for administering module and user access control. * [DATA Foundation Periphery](https://github.com/thedatafoundation/protocol-periphery-v1)- Whereas the core contracts deal with the underlying protocol logic, the periphery contracts deal with protocol extensions that greatly increase UX and simplify IPA management. This is mostly handled through the [SPG](/concepts/spg/overview). ## Deploy & Verify Contracts on the DATA Foundation The approach to deploy & verify contracts comes from the [Blockscout official documentation](https://docs.blockscout.com/developer-support/verifying-a-smart-contract/foundry-verification). Verify a contract with Blockscout right after deployment (make sure you add "/api/" to the end of the Blockscout homepage explorer URL): ```shell theme={null} forge create \ --rpc-url \ --private-key $PRIVATE_KEY \ : \ --verify \ --verifier blockscout \ --verifier-url /api/ ``` Or if using foundry scripts: ```shell theme={null} forge script \ --rpc-url \ --private-key $PRIVATE_KEY \ --broadcast \ --verify \ --verifier blockscout \ --verifier-url /api/ ``` Do not use RANDAO for pseudo-randomness, instead use onchain VRF (Pyth or Gelato). Currently, RANDAO value is set as the parent block hash and thus is not random for X-1 block. # Register an IP Asset Source: https://docs.datafdn.org/developers/smart-contracts-guide/register-ip-asset Learn how to Register an NFT as an IP Asset in Solidity. Follow the completed code all the way through. Let's say you have some off-chain IP (ex. a book, a character, a drawing, etc). In order to register that IP on the DATA Foundation, you first need to mint an NFT. This NFT is the **ownership** over the IP. Then you **register** that NFT on the DATA Foundation, turning it into an [IP Asset](/concepts/ip-asset/overview). The below tutorial will walk you through how to do this. ## Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup) ## Before We Start There are two scenarios: 1. You already have a **custom** ERC-721 NFT contract and can mint from it 2. You want to create an [SPG (Periphery)](/concepts/spg/overview) NFT contract to do minting for you ## Scenario #1: You Already Have a Custom ERC-721 NFT Contract and Can Mint From It If you already have an NFT minted, or you want to register IP using a custom-built ERC-721 contract, this is the section for you. As you can see below, the registration process is relatively straightforward. We use `SimpleNFT` as an example, but you can replace it with your own ERC-721 contract. All you have to do is call `register` on the IP Asset Registry with: * `chainid` - you can simply use `block.chainid` * `tokenContract` - the address of your NFT collection * `tokenId` - your NFT's ID Let's create a test file under `test/0_IPARegistrar.t.sol` to see it work and verify the results: **Contract Addresses** We have filled in the addresses from the DATA Foundation contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts) You can view the `SimpleNFT` contract we're using to test [here](https://github.com/thedatafoundation/story-protocol-boilerplate/blob/main/src/mocks/SimpleNFT.sol). You can view the `SimpleNFT` contract we're using to test [here](https://github.com/thedatafoundation/story-protocol-boilerplate/blob/main/src/mocks/SimpleNFT.sol). ```solidity test/0_IPARegistrar.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; // your own ERC-721 NFT contract import { SimpleNFT } from "../src/mocks/SimpleNFT.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/0_IPARegistrar.t.sol contract IPARegistrarTest is Test { address internal alice = address(0xa11ce); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - IPAssetRegistry IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b); SimpleNFT public SIMPLE_NFT; function setUp() public { // Create a new Simple NFT collection SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM"); } /// @notice Mint an NFT and then register it as an IP Asset. function test_register() public { uint256 expectedTokenId = SIMPLE_NFT.nextTokenId(); address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SIMPLE_NFT), expectedTokenId); uint256 tokenId = SIMPLE_NFT.mint(alice); address ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId); assertEq(tokenId, expectedTokenId); assertEq(ipId, expectedIpId); assertEq(SIMPLE_NFT.ownerOf(tokenId), alice); } } ``` ## Scenario #2: You Want to Create an SPG NFT Contract to Do Minting for You If you don't have your own custom NFT contract, this is the section for you. To achieve this, we will be using the [SPG](/concepts/spg/overview), which is a utility contract that allows us to combine multiple transactions into one. In this case, we'll be using the SPG's `mintAndRegisterIp` function which combines both minting an NFT and registering it as an IP Asset. In order to use `mintAndRegisterIp`, we first have to create a new `SPGNFT` collection. We can do this simply by calling `createCollection` on the `StoryProtocolGateway` contract. Or, if you want to create your own `SPGNFT` for some reason, you can implement the [ISPGNFT](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) contract interface. Follow the example below to see example parameters you can use to initialize a new SPGNFT. Once you have your own SPGNFT, all you have to do is call `mintAndRegisterIp` with: * `spgNftContract` - the address of your SPGNFT contract * `recipient` - the address of who will receive the NFT and thus be the owner of the newly registered IP. *Note: remember that registering IP on the DATA Foundation is permissionless, so you can register an IP for someone else (by paying for the transaction) yet they can still be the owner of that IP Asset.* * `ipMetadata` - the metadata associated with your NFT & IP. See [this](/concepts/ip-asset/overview#nft-vs-ip-metadata) section to better understand setting NFT & IP metadata. 1. Run `touch test/0_IPARegistrar.t.sol` to create a test file under `test/0_IPARegistrar.t.sol`. Then, paste in the following code: **Contract Addresses** We have filled in the addresses from the DATA Foundation contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts) ```solidity test/0_IPARegistrar.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol"; import { ISPGNFT } from "@storyprotocol/periphery/interfaces/ISPGNFT.sol"; import { IRegistrationWorkflows } from "@storyprotocol/periphery/interfaces/workflows/IRegistrationWorkflows.sol"; import { WorkflowStructs } from "@storyprotocol/periphery/lib/WorkflowStructs.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/0_IPARegistrar.t.sol contract IPARegistrarTest is Test { address internal alice = address(0xa11ce); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - IPAssetRegistry IIPAssetRegistry internal IP_ASSET_REGISTRY = IIPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b); // Protocol Periphery - RegistrationWorkflows IRegistrationWorkflows internal REGISTRATION_WORKFLOWS = IRegistrationWorkflows(0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424); ISPGNFT public SPG_NFT; function setUp() public { // Create a new NFT collection via SPG SPG_NFT = ISPGNFT( REGISTRATION_WORKFLOWS.createCollection( ISPGNFT.InitParams({ name: "Test Collection", symbol: "TEST", baseURI: "", contractURI: "", maxSupply: 100, mintFee: 0, mintFeeToken: address(0), mintFeeRecipient: address(this), owner: address(this), mintOpen: true, isPublicMinting: false }) ) ); } /// @notice Mint an NFT and register it in the same call via the DATA Foundation Gateway. /// @dev Requires the collection address that is passed into the `mintAndRegisterIp` function /// to be created via SPG (createCollection), as done above. Or, a contract that /// implements the `ISPGNFT` interface. function test_mintAndRegisterIp() public { uint256 expectedTokenId = SPG_NFT.totalSupply() + 1; address expectedIpId = IP_ASSET_REGISTRY.ipId(block.chainid, address(SPG_NFT), expectedTokenId); // Note: The caller of this function must be the owner of the SPG NFT Collection. // In this case, the owner of the SPG NFT Collection is the contract itself // because it deployed it in the `setup` function. // We can make `alice` the recipient of the NFT though, which makes her the // owner of not only the NFT, but therefore the IP Asset. (address ipId, uint256 tokenId) = REGISTRATION_WORKFLOWS.mintAndRegisterIp( address(SPG_NFT), alice, WorkflowStructs.IPMetadata({ ipMetadataURI: "https://ipfs.io/ipfs/QmZHfQdFA2cb3ASdmeGS5K6rZjz65osUddYMURDx21bT73", ipMetadataHash: keccak256( abi.encodePacked( "{'title':'My IP Asset','description':'This is a test IP asset','createdAt':'','creators':[]}" ) ), nftMetadataURI: "https://ipfs.io/ipfs/QmRL5PcK66J1mbtTZSw1nwVqrGxt98onStx6LgeHTDbEey", nftMetadataHash: keccak256( abi.encodePacked( "{'name':'Test NFT','description':'This is a test NFT','image':'https://picsum.photos/200'}" ) ) }), true ); assertEq(ipId, expectedIpId); assertEq(tokenId, expectedTokenId); assertEq(SPG_NFT.ownerOf(tokenId), alice); } } ``` ## Run the Test and Verify the Results 2. Run `forge build`. If everything is successful, the command should successfully compile. 3. Now run the test by executing the following command: ```bash theme={null} forge test --fork-url https://aeneid.datarpc.io/ --match-path test/0_IPARegistrar.t.sol ``` ## Add License Terms to IP Congratulations, you registered an IP! Follow the completed code all the way through. Now that your IP is registered, you can create and attach [License Terms](/concepts/licensing-module/license-terms) to it. This will allow others to mint a license and use your IP, restricted by the terms. We will go over this on the next page. # Register License Terms Source: https://docs.datafdn.org/developers/smart-contracts-guide/register-terms Learn how to create new License Terms in Solidity. Follow the completed code all the way through. [License Terms](/concepts/licensing-module/license-terms) are a configurable set of values that define restrictions on licenses minted from your IP that have those terms. For example, "If you mint this license, you must share 50% of your revenue with me." You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms). ## Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [Setup Your Own Project](/developers/smart-contracts-guide/setup) ## Before We Start It's important to know that if **License Terms already exist for the identical set of parameters you intend to create, it is unnecessary to create it again**. License Terms are protocol-wide, so you can use existing License Terms by its `licenseTermsId`. ## Register License Terms You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms). Let's create a test file under `test/1_LicenseTerms.t.sol` to see it work and verify the results: **Contract Addresses** We have filled in the addresses from the DATA Foundation contracts for you. However you can also find the addresses for them here: [Deployed Smart Contracts](/developers/deployed-smart-contracts) ```solidity test/1_LicenseTerms.t.sol theme={null} // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; // Run this test: // forge test --fork-url https://aeneid.datarpc.io/ --match-path test/1_LicenseTerms.t.sol contract LicenseTermsTest is Test { address internal alice = address(0xa11ce); // For addresses, see https://docs.datafdn.org/developers/deployed-smart-contracts // Protocol Core - PILicenseTemplate IPILicenseTemplate internal PIL_TEMPLATE = IPILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316); // Protocol Core - RoyaltyPolicyLAP address internal ROYALTY_POLICY_LAP = 0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E; // Revenue Token - MERC20 address internal MERC20 = 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E; function setUp() public {} /// @notice Registers new PIL Terms. Anyone can register PIL Terms. function test_registerPILTerms() public { PILTerms memory pilTerms = PILTerms({ transferable: true, royaltyPolicy: ROYALTY_POLICY_LAP, defaultMintingFee: 0, expiration: 0, commercialUse: true, commercialAttribution: true, commercializerChecker: address(0), commercializerCheckerData: "", commercialRevShare: 0, commercialRevCeiling: 0, derivativesAllowed: true, derivativesAttribution: true, derivativesApproval: true, derivativesReciprocal: true, derivativeRevCeiling: 0, currency: MERC20, uri: "" }); uint256 licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(pilTerms); uint256 selectedLicenseTermsId = PIL_TEMPLATE.getLicenseTermsId(pilTerms); assertEq(licenseTermsId, selectedLicenseTermsId); } } ``` ### PIL Flavors As you see above, you have to choose between a lot of terms. We have convenience functions to help you register new terms. We have created [PIL Flavors](/concepts/programmable-ip-license/pil-flavors), which are pre-configured popular combinations of License Terms to help you decide what terms to use. You can view those PIL Flavors and then register terms using the following convenience functions: Free remixing with attribution. No commercialization. Pay to use the license with attribution, but don't have to share revenue. Pay to use the license with attribution and pay % of revenue earned. Free remixing and commercial use with attribution. For example: ```solidity Solidity theme={null} import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; PILTerms memory pilTerms = PILFlavors.commercialRemix({ mintingFee: 0, commercialRevShare: 5 * 10 ** 6, // 5% rev share royaltyPolicy: ROYALTY_POLICY_LAP, currencyToken: MERC20 }); ``` ## Test Your Code! Run `forge build`. If everything is successful, the command should successfully compile. Now run the test by executing the following command: ```bash theme={null} forge test --fork-url https://aeneid.datarpc.io/ --match-path test/1_LicenseTerms.t.sol ``` ## Attach Terms to Your IP Congratulations, you created new license terms! Follow the completed code all the way through. Now that you have registered new license terms, we can attach them to an IP Asset. This will allow others to mint a license and use your IP, restricted by the terms. We will go over this on the next page. # Setup Source: https://docs.datafdn.org/developers/smart-contracts-guide/setup Set up your development environment for DATA Foundation smart contracts. In this guide, we will show you how to setup the DATA Foundation smart contract development environment in just a few minutes. ## Prerequisites * [Install Foundry](https://book.getfoundry.sh/getting-started/installation) * [Install yarn](https://classic.yarnpkg.com/lang/en/docs/install/) ## Creating a Project 1. Run `foundryup` to automatically install the latest stable version of the precompiled binaries: forge, cast, anvil, and chisel 2. Run the following command in a new directory: `forge init`. This will create a `foundry.toml` and example project files in the project root. By default, forge init will also initialize a new git repository. 3. Initialize a new yarn project: `yarn init`. (⚠️ Note: Only Yarn is compatible with the packages used in this project. Using `npm` or `pnpm` may result in dependency conflicts.) 4. Open up your root-level `foundry.toml` file (located in the top directory of your project) and replace it with this: ```toml theme={null} [profile.default] out = 'out' libs = ['node_modules', 'lib'] cache_path = 'forge-cache' gas_reports = ["*"] optimizer = true optimizer_runs = 20000 test = 'test' solc = '0.8.26' fs_permissions = [{ access = 'read', path = './out' }, { access = 'read-write', path = './deploy-out' }] evm_version = 'cancun' remappings = [ '@openzeppelin/=node_modules/@openzeppelin/', '@storyprotocol/core/=node_modules/@story-protocol/protocol-core/contracts/', '@storyprotocol/periphery/=node_modules/@story-protocol/protocol-periphery/contracts/', 'erc6551/=node_modules/erc6551/', 'forge-std/=node_modules/forge-std/src/', 'ds-test/=node_modules/ds-test/src/', '@storyprotocol/test/=node_modules/@story-protocol/protocol-core/test/foundry/', '@solady/=node_modules/solady/' ] ``` 5. Remove the example contract files: `rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol` ## Installing Dependencies Now, we are ready to start installing our dependencies. To incorporate the DATA Foundation core and periphery modules, run the following to have them added to your `package.json`. We will also install `openzeppelin` and `erc6551` as a dependency for the contract and test. ```bash theme={null} # note: you can run them one-by-one, or all at once yarn add @story-protocol/protocol-core@https://github.com/thedatafoundation/protocol-core-v1 yarn add @story-protocol/protocol-periphery@https://github.com/thedatafoundation/protocol-periphery-v1 yarn add @openzeppelin/contracts yarn add @openzeppelin/contracts-upgradeable yarn add erc6551 yarn add solady ``` Additionally, for working with Foundry's test kit, we also recommend adding the following `devDependencies`: ```bash theme={null} yarn add -D https://github.com/dapphub/ds-test yarn add -D github:foundry-rs/forge-std#v1.7.6 ``` Now we are ready to build a simple test registration contract! # Easy $DATA Onboarding Source: https://docs.datafdn.org/developers/tutorials/easy-ip-onboarding An example of how to integrate purchasing $DATA with Apple Pay, Venmo, Debit Card, Bank, and more with Halliday. View the completed code for this tutorial. This tutorial will show you how to integrate purchasing \$DATA with Apple Pay, Venmo, Debit Card, Bank, and more into your DApp with Halliday. **This tutorial is a React/Next.js tutorial**. It is also based on the [Halliday Docs](https://docs.halliday.xyz/pages/index-page). Here is what the end result will look like: Halliday Payment End Result ## Instructions In order to use Halliday, you will need to get an API key. Right now the process is to email [partnerships@halliday.xyz](mailto:partnerships@halliday.xyz). But this may change, so I recommend checking the [Halliday Docs](https://docs.halliday.xyz/pages/payments-hello-world) for the most up to date information. Next, install the Halliday Payments SDK in the root folder of your project. ```bash npm theme={null} npm install @halliday-sdk/payments ``` ```bash yarn theme={null} yarn add @halliday-sdk/payments ``` In your `.env` file, add your Halliday API key. ```env theme={null} NEXT_PUBLIC_HALLIDAY_PUBLIC_API_KEY=your-api-key ``` Lastly, integrate Halliday Payments into your existing application with a few lines of code. In this example, we make it so that the Halliday popup is embedded in a div with the id `halliday-embed`. Such that when the user loads the page, it is already there. However you can change this so that it pops up when the user clicks a button. Check out the [Halliday Docs](https://docs.halliday.xyz/pages/payments-widget) for more information. ```tsx theme={null} "use client"; import { openHallidayPayments } from "@halliday-sdk/payments"; import { useEffect } from "react"; export default function Home() { useEffect(() => { openHallidayPayments({ apiKey: process.env.NEXT_PUBLIC_HALLIDAY_PUBLIC_API_KEY as string, // $DATA on DATA Foundation outputs: ["story:0x"], // $USDC.e on DATA Foundation // outputs: ["story:0xf1815bd50389c46847f0bda824ec8da914045d14"], sandbox: false, windowType: "EMBED", targetElementId: "halliday-embed", }); }, []); return (
); } ```
That's it! You can now use Halliday to purchase \$DATA on DATA Foundation. View the completed code for this tutorial. # How to Register IP on the DATA Foundation Source: https://docs.datafdn.org/developers/tutorials/how-to-register-ip Learn how to register an NFT as IP with proper metadata on the DATA Foundation. Learn how to register an IP using the SDK. Learn how to register an IP using the Smart Contracts. # How to Register Music on the DATA Foundation Source: https://docs.datafdn.org/developers/tutorials/how-to-register-music Learn how to properly register music on the DATA Foundation as an IP Asset using the Typescript SDK. In this tutorial, you will learn how to properly register music as IP on the DATA Foundation using the TypeScript SDK. At the end, you will be able to listen to your song directly on our explorer. View an example result after following this tutorial. "Peaches" by Justin Bieber is one of the first RWAs coming to the DATA Foundation. Check out the announcement! ## 1. Create a Song Before we register music on the DATA Foundation, you'll obviously need some music! If you already have music, make sure you have a link to the music file directly. For example, `https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3`. If you don't already have this, you can upload your music file to IPFS: If you want to create a test song, go to [Suno](https://suno.com), which is an awesome platform for AI-generated music. We can get a test song by: 1. Inputting a prompt to create a song 2. Click on the final result, which should take you to a URL like `https://suno.com/song/dcd3076f-3aa5-400b-ba5d-87d30f27c311` 3. Copy the the `SONG_ID` in the URL (`dcd3076f-3aa5-400b-ba5d-87d30f27c311`) 4. Copy the following URL: `https://cdn1.suno.ai/${SONG_ID}.mp3`, making sure to replace `SONG_ID` with your own. This is the URL we'll use in step 2. ## 2. Complete the "How to Register IP" Tutorial Most of what we need to do is already covered in [Register an IP Asset](/developers/typescript-sdk/register-ip-asset). Complete that tutorial first, and then come back here. ## 3. Change Metadata The only difference is how you set your metadata. Here is an example: * `image.*` is used to display a cover image when your song is registered * `media.*` is used for the audio file. Note that the fields passed into `media.*` may be used for infringement checking. ```typescript main.ts theme={null} const ipMetadata = { title: "Midnight Marriage", description: "This is a house-style song generated on suno.", createdAt: "1740005219", creators: [ { name: "Jacob Tucker", address: "0xA2f9Cf1E40D7b03aB81e34BC50f0A8c67B4e9112", contributionPercent: 100, }, ], image: "https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg", imageHash: "0xc404730cdcdf7e5e54e8f16bc6687f97c6578a296f4a21b452d8a6ecabd61bcc", mediaUrl: "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3", mediaHash: "0xb52a44f53b2485ba772bd4857a443e1fb942cf5dda73c870e2d2238ecd607aee", mediaType: "audio/mpeg", }; ``` After you've done that, you can set your NFT metadata like so: * `image` for the cover image * `animation_url` is used for the audio file * `attributes` for any extra attributes you want to include ```typescript main.ts theme={null} const nftMetadata = { name: "Midnight Marriage", description: "This is a house-style song generated on suno. This NFT represents ownership of the IP Asset.", image: "https://cdn2.suno.ai/image_large_8bcba6bc-3f60-4921-b148-f32a59086a4c.jpeg", animation_url: "https://cdn1.suno.ai/dcd3076f-3aa5-400b-ba5d-87d30f27c311.mp3", attributes: [ { key: "Suno Artist", value: "amazedneurofunk956", }, { key: "Artist ID", value: "4123743b-8ba6-4028-a965-75b79a3ad424", }, { key: "Source", value: "Suno.com", }, ], }; ``` ## 4. Done! When you run the script, you will register an IP Asset and it will look something like [this](https://aeneid.explorer.datafdn.org/ipa/0x70920EaC7F9748Ac5A71C82310f1ac1C7eD11f02) on our explorer. You can see the explorer recognizes the metadata format, and you can play the song directly on the page! Explore more tutorials in our documentation # Email Login & Sponsored Transactions with Privy Source: https://docs.datafdn.org/developers/tutorials/privy-tutorial Learn how to implement email logins and sponsored transactions with Privy & Pimlico. View the completed code for this tutorial. You are reading this tutorial because you probably want to do one or both of these things: 1. Enable users who don't have a wallet to login with email to your app ("Embedded Wallets") 2. Sponsor transactions for your users so they don't have to pay gas ("Smart Wallets") Here is how Privy describes both of these things: > Embedded wallets are self-custodial wallets provisioned by Privy itself for a wallet experience that is directly embedded in your application. Embedded wallets do not require a separate wallet client, like a browser extension or a mobile app, and can be accessed directly from your product. These are primarily designed for users of your app who may not already have an external wallet, or don't want to connect their external wallet. > > Smart wallets are programmable, onchain accounts that incorporate the features of account abstraction. With just a few lines of code, you can create smart wallets for your users to sponsor gas payments, send batched transactions, and more. We will be implementing both using [Privy](https://www.privy.io/) + [Pimlico](https://www.pimlico.io/). ### ⚠️ Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Create a new project on [Privy's Dashboard](https://dashboard.privy.io) 2. Copy your **"App ID"** under **"App settings > API keys"**. In your local project, make a `.env` file and add your App ID: ```Text .env theme={null} NEXT_PUBLIC_PRIVY_APP_ID= ``` 3. On your project dashboard, enable Smart Wallets under "**Wallet Configuration > Smart wallets**" and select "**Kernel (ZeroDev)**" as shown below: Privy Dashboard 4. Once you enable Smart wallets, right underneath make sure to put a "Custom chain" with the following values: 1. Name: `Aeneid Testnet` 2. ID number: `1315` 3. RPC URL: `https://aeneid.datarpc.io` 4. For the Bundler URL and Paymaster URL, go to [Pimlico's Dashboard](https://dashboard.pimlico.io) and create a new app. Then click on "API Keys", create a new API Key, click "RPC URLs" as shown below, and then select "Aeneid Testnet" as the network: This is for testing. In a real scenario, you would have to set up proper sponsorship policies and billing info on Pimlico to automatically sponsor the transactions on behalf of your app. We don't have to do this on testnet. Pimlico Dashboard 5. Install the dependencies: ```Text Terminal theme={null} npm install @story-protocol/core-sdk permissionless viem @privy-io/react-auth ``` ## 1. Set up Embedded Wallets Follow Privy's official tutorial for setup instead of reading this step. This part of the Privy documentation [here](https://docs.privy.io/basics/react/advanced/automatic-wallet-creation#automatic-wallet-creation) describes setting up Embedded Wallets automatically, which is a fancy way of saying it supports email login, such that when a user logs in with email it creates a wallet for them. In the below example, we simply create an embedded wallet for every user, but you may want more customization by reading their tutorial. You must wrap any component that will be using embedded/smart wallets with the `PrivyProvider` and `SmartWalletsProvider`. In a `providers.tsx` (or whatever you want to call it) file, add the following code: ```jsx providers.tsx theme={null} "use client"; import { PrivyProvider } from "@privy-io/react-auth"; import { SmartWalletsProvider } from "@privy-io/react-auth/smart-wallets"; import { aeneid } from "@story-protocol/core-sdk"; export default function Providers({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Then you can simply add it to your`layout.tsx` like so: ```jsx layout.tsx theme={null} import Providers from "@/providers/providers"; /* other code here... */ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode, }>) { return ( {children} ); } ``` ## 2. Login & Logout You can add email login to your app like so: ```jsx page.tsx theme={null} import { usePrivy } from "@privy-io/react-auth"; export default function Home() { const { login, logout, user } = usePrivy(); useEffect(() => { if (user) { const smartWallet = user.linkedAccounts.find( (account) => account.type === "smart_wallet" ); // Logs the smart wallet's address console.log(smartWallet.address); // Logs the smart wallet type (e.g. 'safe', 'kernel', 'light_account', 'biconomy', 'thirdweb', 'coinbase_smart_wallet') console.log(smartWallet.type); } }, [user]); return (
); } ``` ## 3. Sign a Message With Privy Follow Privy's official tutorial for signing messages instead of reading this step. We can use the generated smart wallet to sign messages: ```jsx page.tsx theme={null} import { useSmartWallets } from "@privy-io/react-auth/smart-wallets"; export default function Home() { const { client: smartWalletClient } = useSmartWallets(); /* previous code here */ async function sign() { const uiOptions = { title: "Example Sign", description: "This is an example for a user to sign.", buttonText: "Sign", }; const request = { message: "IP is cool", }; const signature = await smartWalletClient?.signMessage(request, { uiOptions, }); } return (
{/* previous code here */}
); } ``` ## 4. Send an Arbitrary Transaction Follow Privy's official tutorial for sending transactions instead of reading this step. We can also use the generated smart wallet to sponsor transactions for our users: ```jsx page.tsx theme={null} import { useSmartWallets } from "@privy-io/react-auth/smart-wallets"; import { encodeFunctionData } from "viem"; import { defaultNftContractAbi } from "./defaultNftContractAbi"; export default function Home() { const { client: smartWalletClient } = useSmartWallets(); /* previous code here */ async function mintNFT() { const uiOptions = { title: "Mint NFT", description: "This is an example transaction that mints an NFT.", buttonText: "Mint", }; const transactionRequest = { to: "0x937bef10ba6fb941ed84b8d249abc76031429a9a", // example nft contract data: encodeFunctionData({ abi: defaultNftContractAbi, // abi from another file functionName: "mintNFT", args: ["0x6B86B39F03558A8a4E9252d73F2bDeBfBedf5b68", "test-uri"], }), } as const; const txHash = await smartWalletClient?.sendTransaction( transactionRequest, { uiOptions } ); console.log(`View Tx: https://aeneid.datanetscan.io/tx/${txHash}`); } return (
{/* previous code here */}
) } ``` ```Text defaultNftContractAbi.ts theme={null} export const defaultNftContractAbi = [ { inputs: [], stateMutability: "nonpayable", type: "constructor", }, { inputs: [ { internalType: "address", name: "recipient", type: "address", }, { internalType: "string", name: "tokenURI", type: "string", }, ], name: "mintNFT", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "tokenURI", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "tokenId", type: "uint256", }, ], name: "ownerOf", outputs: [ { internalType: "address", name: "", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "symbol", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "name", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "totalSupply", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, ]; ```
## 5. Send a Transaction From DATA Foundation SDK We can also use the generated smart wallet to send transactions from the [🛠️ TypeScript SDK](/developers/typescript-sdk). Some of the functions have an option to return the `encodedTxData`, which we can use to pass into Privy's smart wallet. You can see which functions support this in the [SDK Reference](/sdk-reference). ```jsx page.tsx theme={null} import { useSmartWallets } from "@privy-io/react-auth/smart-wallets"; import { EncodedTxData, StoryClient, StoryConfig, } from "@story-protocol/core-sdk"; import { http } from "viem"; export default function Home() { const { client: smartWalletClient } = useSmartWallets(); /* previous code here */ async function setupStoryClient() { const config: StoryConfig = { account: smartWalletClient!.account, transport: http("https://aeneid.datarpc.io"), chainId: "aeneid", }; const client = StoryClient.newClient(config); return client; } async function registerIp() { const storyClient = await setupStoryClient(); const response = await storyClient.ipAsset.registerIpAsset({ nft: { type: "mint", spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc", }, }); const uiOptions = { title: "Register IP", description: "This is an example transaction that registers an IP.", buttonText: "Register", }; const txHash = await smartWalletClient?.sendTransaction( response.encodedTxData as EncodedTxData, { uiOptions } ); console.log(`View Tx: https://aeneid.datanetscan.io/tx/${txHash}`); } return (
{/* previous code here */}
); } ``` ## 6. Done! View the completed code for this tutorial. Explore more tutorials in our documentation # Attach Terms to an IPA Source: https://docs.datafdn.org/developers/typescript-sdk/attach-terms Learn how to Attach License Terms to an IP Asset in TypeScript. This section demonstrates how to attach [License Terms](/concepts/licensing-module/license-terms) to an [IP Asset](/concepts/ip-asset). By attaching terms, users can publicly mint [License Tokens](/concepts/licensing-module/license-token) (the on-chain "license") with those terms from the IP. ### Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup) ## 1. Before We Start We should mention that you do not need an existing IP Asset to attach terms to it. As we saw in the previous section, you can register an IP Asset and attach terms to it in the same transaction. ## 2. Register License Terms In order to attach terms to an IP Asset, let's first create them! [License Terms](/concepts/licensing-module/license-terms) are a configurable set of values that define restrictions on licenses minted from your IP that have those terms. For example, "If you mint this license, you must share 50% of your revenue with me." You can view the full set of terms in [PIL Terms](/concepts/programmable-ip-license/pil-terms). If License Terms already exist on our protocol for the identical set of parameters you intend to create, it is unnecessary to create it again and the function will simply return the existing `licenseTermsId` and an undefined `txHash`. License Terms are protocol-wide, so you can use existing License Terms by its `licenseTermsId`. Below is a code example showing how to create new terms: Associated Docs: [license.registerPILTerms](/sdk-reference/license#registerpilterms) ```typescript main.ts theme={null} import { LicenseTerms } from "@story-protocol/core-sdk"; import { zeroAddress } from "viem"; // you should already have a client set up (prerequisite) import { client } from "./utils"; async function main() { const licenseTerms: LicenseTerms = { defaultMintingFee: 0n, // must be a whitelisted revenue token from https://docs.datafdn.org/developers/deployed-smart-contracts // in this case, we use $WIP currency: "0x1514000000000000000000000000000000000000", // RoyaltyPolicyLAP address from https://docs.datafdn.org/developers/deployed-smart-contracts royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", transferable: false, expiration: 0n, commercialUse: false, commercialAttribution: false, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: 0, commercialRevCeiling: 0n, derivativesAllowed: false, derivativesAttribution: false, derivativesApproval: false, derivativesReciprocal: false, derivativeRevCeiling: 0n, uri: "", }; const response = await client.license.registerPILTerms({ ...licenseTerms, }); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); } main(); ``` ### 2a. PIL Flavors As you see above, you have to choose between a lot of terms. We have convenience functions to help you register new terms. We have created [PIL Flavors](/concepts/programmable-ip-license/pil-flavors), which are pre-configured popular combinations of License Terms to help you decide what terms to use. You can view those PIL Flavors and then register terms using the following convenience functions: Free remixing with attribution. No commercialization. Pay to use the license with attribution, but don't have to share revenue. Pay to use the license with attribution and pay % of revenue earned. Free remixing and commercial use with attribution. You can easily register a flavor of terms like so: ```typescript main.ts theme={null} import { PILFlavor, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk"; import { parseEther } from "viem"; // you should already have a client set up (prerequisite) import { client } from "./utils"; async function main() { const response = await client.license.registerPILTerms( PILFlavor.commercialRemix({ commercialRevShare: 5, defaultMintingFee: parseEther("1"), // 1 $DATA currency: WIP_TOKEN_ADDRESS, }) ); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); } main(); ``` ## 3. Attach License Terms Now that we have created terms and have the associated `licenseTermsId`, we can attach them to an existing IP Asset like so: Associated Docs: [license.attachLicenseTerms](/sdk-reference/license#attachlicenseterms) ```typescript main.ts theme={null} import { LicenseTerms } from "@story-protocol/core-sdk"; import { zeroAddress } from "viem"; // you should already have a client set up (prerequisite) import { client } from "./utils"; async function main() { // previous code here ... const response = await client.license.attachLicenseTerms({ // insert your newly created license terms id here licenseTermsId: LICENSE_TERMS_ID, // insert the ipId you want to attach terms to here ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", }); if (response.success) { console.log( `Attached License Terms to IPA at transaction hash ${response.txHash}.` ); } else { console.log(`License Terms already attached to this IPA.`); } } main(); ``` ## 3. Mint a License Now that we have attached License Terms to our IP, the next step is minting a License Token, which we'll go over on the next page. # Mint a License Token Source: https://docs.datafdn.org/developers/typescript-sdk/mint-license Learn how to mint a License Token from an IP Asset in TypeScript. This section demonstrates how to mint a [License Token](/concepts/licensing-module/license-token) from an [IP Asset](/concepts/ip-asset). You can only mint a License Token from an IP Asset if the IP Asset has [License Terms](/concepts/licensing-module/license-terms) attached to it. A License Token is minted as an ERC-721. There are two reasons you'd mint a License Token: 1. To hold the license and be able to use the underlying IP Asset as the license described (for ex. "Can use commercially as long as you provide proper attribution and share 5% of your revenue) 2. Use the license token to link another IP Asset as a derivative of it. *Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.* ### Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup) 2. An IP Asset that has License Terms added. Learn how to add License Terms to an IPA [here](/developers/typescript-sdk/attach-terms). ## 1. Mint License Let's say that IP Asset (`ipId = 0x01`) has License Terms (`licenseTermdId = 10`) attached to it. We want to mint 2 License Tokens with those terms to a specific wallet address (`0x02`). Be mindful that some IP Assets may have license terms attached that require the user minting the license to pay a `defaultMintingFee`. You can see an example of that in the [TypeScript Tutorial](https://github.com/thedatafoundation/typescript-tutorial/blob/main/scripts/derivative/registerDerivativeCommercial.ts). Note that a license token can only be minted if the `licenseTermsId` are already attached to the IP Asset, making it a publicly available license. The IP owner can, however, mint a [private license](/concepts/licensing-module/license-token#private-licenses) by minting a license token with a `licenseTermsId` that is not attached to the IP Asset. Associated Docs: [license.mintLicenseTokens](/sdk-reference/license#mintlicensetokens) ```typescript main.ts theme={null} // you should already have a client set up (prerequisite) import { client } from "./client"; async function main() { const response = await client.license.mintLicenseTokens({ licenseTermsId: "10", licensorIpId: "0x641E638e8FCA4d4844F509630B34c9D524d40BE5", receiver: "0x641E638e8FCA4d4844F509630B34c9D524d40BE5", // optional. if not provided, it will go to the tx sender amount: 2, maxMintingFee: BigInt(0), // disabled maxRevenueShare: 100, // default }); console.log( `License Token minted at transaction hash ${response.txHash}, License IDs: ${response.licenseTokenIds}` ); } main(); ``` ### 1a. Setting Restrictions on Minting License Token This is a note for owners of an IP Asset who want to set restrictions on who or how their license tokens are minted. You can: * Set a max number of licenses that can be minted * Charge dynamic fees based on who / how many are minted * Whitelisted certain wallets to mint the tokens ... and more. Learn more by checking out the [License Config](/concepts/licensing-module/license-config) section of our documentation. ## 2. Register a Derivative Now that we have minted a License Token, we can hold it or use it to link an IP Asset as a derivative. We will go over that on the next page. *Note though that, as you'll see later, some SDK functions don't require you to explicitly mint a license token first in order to register a derivative, and will actually handle it for you behind the scenes.* ### 2a. Why would I ever use a License Token if it's not needed? There are a few times when **you would need** a License Token to register a derivative: * The License Token contains private license terms, so you would only be able to register as a derivative if you had the License Token that was manually minted by the owner. More on that [here](/concepts/licensing-module/license-token#private-licenses). * The License Token (which is an NFT) costs a `mintingFee` to mint, and you were able to buy it on a marketplace for a cheaper price. Then it makes more sense to simply register with the License Token then have to pay the more expensive `defaultMintingFee`. # Overview Source: https://docs.datafdn.org/developers/typescript-sdk/overview For TypeScript developers who want to build with the DATA Foundation. The best way to get started is to get your hands dirty and start building. Extremely easy & straightforward working code examples for all of the following tutorials. View the whole SDK reference, which shows examples and types for every function in our SDK. In the following series of tutorials, you will learn how to build IP applications with the DATA Foundation SDK along with the concepts we mentioned in the [Architecture Overview](/concepts/overview). # Register an IP Asset Source: https://docs.datafdn.org/developers/typescript-sdk/register-ip-asset Learn how to Register an NFT as an IP Asset in TypeScript. Follow the completed code all the way through. Let's say you have some off-chain IP (ex. a book, a character, a drawing, etc). In order to register that IP on the DATA Foundation, you first need to mint an NFT. This NFT is the **ownership** over the IP. Then you **register** that NFT on the DATA Foundation, turning it into an [IP Asset](/concepts/ip-asset). The below tutorial will walk you through how to do this. ### Prerequisites There are a few steps you have to complete before you can start the tutorial. 1. Complete the [TypeScript SDK Setup](/developers/typescript-sdk/setup) 2. \[OPTIONAL] Go to [Pinata](https://pinata.cloud/) and create a new API key. Add the JWT to your `.env` file: ```text .env theme={null} PINATA_JWT= ``` 3. \[OPTIONAL] Install the `pinata-web3` dependency: ```bash Terminal theme={null} npm install pinata-web3 ``` ## 1. Set up Your IP Metadata We can set metadata on our NFT & IP, *but you don't have to*. To do this, view the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) and construct your metadata for both your NFT & IP. ```typescript main.ts theme={null} // you should already have a client set up (prerequisite) import { client } from "./utils"; async function main() { const ipMetadata = { title: "Ippy", description: "Official mascot of the DATA Foundation.", image: "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8", imageHash: "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e", mediaUrl: "https://ipfs.io/ipfs/QmSamy4zqP91X42k6wS7kLJQVzuYJuW2EN94couPaq82A8", mediaHash: "0x21937ba9d821cb0306c7f1a1a2cc5a257509f228ea6abccc9af1a67dd754af6e", mediaType: "image/png", creators: [ { name: "The DATA Foundation", address: "0x67ee74EE04A0E6d14Ca6C27428B27F3EFd5CD084", description: "The World's IP Blockchain", contributionPercent: 100, socialMedia: [ { platform: "Twitter", url: "https://x.com/DataFDN", }, { platform: "Website", url: "https://datafdn.org", }, ], }, ], }; } main(); ``` ## 2. Set up Your NFT Metadata The NFT Metadata follows the [ERC-721 Metadata Standard](https://eips.ethereum.org/EIPS/eip-721). ```typescript main.ts theme={null} import { IpMetadata } from "@story-protocol/core-sdk"; import { client } from "./utils"; async function main() { // previous code here ... const nftMetadata = { name: "Ownership NFT", description: "This is an NFT representing owernship of our IP Asset.", image: "https://picsum.photos/200", }; } main(); ``` ## 3. Upload Your IP and NFT Metadata to IPFS In a separate `uploadToIpfs` file, create a function to upload your IP & NFT Metadata objects to IPFS: ```typescript uploadToIpfs.ts theme={null} import { PinataSDK } from "pinata-web3"; const pinata = new PinataSDK({ pinataJwt: process.env.PINATA_JWT, }); export async function uploadJSONToIPFS(jsonMetadata: any): Promise { const { IpfsHash } = await pinata.upload.json(jsonMetadata); return IpfsHash; } ``` You can then use that function to upload your metadata, as shown below: ```typescript main.ts theme={null} import { IpMetadata } from "@story-protocol/core-sdk"; import { client } from "./utils"; import { uploadJSONToIPFS } from "./uploadToIpfs"; import { createHash } from "crypto"; async function main() { // previous code here ... const ipIpfsHash = await uploadJSONToIPFS(ipMetadata); const ipHash = createHash("sha256") .update(JSON.stringify(ipMetadata)) .digest("hex"); const nftIpfsHash = await uploadJSONToIPFS(nftMetadata); const nftHash = createHash("sha256") .update(JSON.stringify(nftMetadata)) .digest("hex"); } main(); ``` ## 4. Register an NFT as an IP Asset Remember that in order to register a new IP, we first have to mint an NFT, which will represent the underlying ownership of the IP. This NFT then gets "registered" and becomes an [IP Asset](/concepts/ip-asset). Luckily, we can use the `registerIpAsset` function to mint an NFT and register it as an IP Asset in the same transaction. This function needs an SPG NFT Contract to mint from. ### 4a. What SPG NFT contract address should I use? For simplicity, you can use a public collection we have created for you on Aeneid testnet: `0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc`. On Mainnet, or even when testing a real scenario on Aeneid, you should **create your own** contract as described in the "Using a custom ERC-721 contract" section below. Using a public collection we provide for you is fine, but when you do this for real, you should make your own NFT Collection for your IPs. You can do this in 2 ways: 1. Deploy a contract that implements the [ISPGNFT](https://github.com/thedatafoundation/protocol-periphery-v1/blob/main/contracts/interfaces/ISPGNFT.sol) interface, or use the SDK's [createNFTCollection](/sdk-reference/nftclient#createnftcollection) function (shown below) to do it for you. This will give you your own SPG NFT Collection that only you can mint from. ```typescript createSpgNftCollection.ts theme={null} import { zeroAddress } from "viem"; import { client } from "./utils"; async function createSpgNftCollection() { const newCollection = await client.nftClient.createNFTCollection({ name: "Test NFTs", symbol: "TEST", isPublicMinting: false, mintOpen: true, mintFeeRecipient: zeroAddress, contractURI: "", }); console.log("New collection created:", { "SPG NFT Contract Address": newCollection.spgNftContract, "Transaction Hash": newCollection.txHash, }); } createSpgNftCollection(); ``` 2. Create a custom ERC-721 NFT collection on your own. See a working code example [here](https://github.com/thedatafoundation/typescript-tutorial/blob/main/scripts/registration/registerCustom.ts). This is helpful if you **already have a custom NFT contract that has your own custom logic, or if your IPs themselves are NFTs.** Here is the code to register an IP: Associated Docs: [ipAsset.registerIpAsset](/sdk-reference/ipasset#registeripasset) ```typescript main.ts theme={null} import { IpMetadata } from "@story-protocol/core-sdk"; import { client } from "./utils"; import { uploadJSONToIPFS } from "./uploadToIpfs"; import { createHash } from "crypto"; import { Address } from "viem"; async function main() { // previous code here ... const response = await client.ipAsset.registerIpAsset({ nft: { type: "mint", spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc", }, ipMetadata: { ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`, ipMetadataHash: `0x${ipHash}`, nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`, nftMetadataHash: `0x${nftHash}`, }, }); console.log( `Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}` ); console.log( `View on the explorer: https://aeneid.explorer.datafdn.org/ipa/${response.ipId}` ); } main(); ``` ## 5. Add License Terms to IP During the registration process, you can attach [License Terms](/concepts/licensing-module/license-terms) to the IP. This will allow others to mint a license and use your IP, restricted by the terms. ```typescript main.ts theme={null} import { IpMetadata, PILFlavor, WIP_TOKEN_ADDRESS, } from "@story-protocol/core-sdk"; import { client } from "./utils"; import { uploadJSONToIPFS } from "./uploadToIpfs"; import { createHash } from "crypto"; import { Address, parseEther } from "viem"; async function main() { // previous code here ... const response = await client.ipAsset.registerIpAsset({ nft: { type: "mint", spgNftContract: "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc", }, // [!code ++:9] licenseTermsData: [ { terms: PILFlavor.commercialRemix({ commercialRevShare: 5, defaultMintingFee: parseEther("1"), // 1 $DATA currency: WIP_TOKEN_ADDRESS, }), }, ], ipMetadata: { ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`, ipMetadataHash: `0x${ipHash}`, nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`, nftMetadataHash: `0x${nftHash}`, }, }); console.log( `Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}` ); console.log( `View on the explorer: https://aeneid.explorer.datafdn.org/ipa/${response.ipId}` ); } main(); ``` ## 6. View Completed Code Congratulations, you registered an IP and attached license terms to it! Follow the completed code all the way through. # Setup Client Source: https://docs.datafdn.org/developers/typescript-sdk/setup Learn how to setup the TypeScript SDK. ### Prerequisites We require node version 18 or later version and npm version 8 to be installed in your environment. To install node and npm, we recommend you go to the [Node.js official website](https://nodejs.org) and download the latest LTS (Long Term Support) version. ### Install the Dependencies Install the [DATA Foundation SDK](https://www.npmjs.com/package/@story-protocol/core-sdk) node package, as well as [viem](https://www.npmjs.com/package/viem). ```bash npm theme={null} npm install --save @story-protocol/core-sdk viem ``` ```bash pnpm theme={null} pnpm install @story-protocol/core-sdk viem ``` ```bash yarn theme={null} yarn add @story-protocol/core-sdk viem ``` ## Initiate SDK Client Next we can initiate the SDK Client. There are two ways to do this: 1. Using a private key (preferable for some backend admin) 2. JSON-RPC account like Metamask where users sign their own transactions ### Set Up Private Key Account Check out the TypeScript Tutorial for a working example of how to set up the DATA Foundation SDK Client. Before continuing with the code below: 1. Make sure to have `WALLET_PRIVATE_KEY` set up in your `.env` file. * Don’t forget to fund the wallet with some testnet tokens from a [Faucet](/network/network-info/aeneid#faucet) 2. Make sure to have `RPC_PROVIDER_URL` set up in your `.env` file. * You can use the public default one (`https://aeneid.datarpc.io`) or check out the other RPCs [here](/network/network-info/aeneid#rpcs). ```typescript utils.ts theme={null} import { http } from "viem"; import { Account, privateKeyToAccount, Address } from "viem/accounts"; import { StoryClient, StoryConfig } from "@story-protocol/core-sdk"; const privateKey: Address = `0x${process.env.WALLET_PRIVATE_KEY}`; const account: Account = privateKeyToAccount(privateKey); const config: StoryConfig = { account: account, // the account object from above transport: http(process.env.RPC_PROVIDER_URL), chainId: "aeneid", }; export const client = StoryClient.newClient(config); ``` ### Setup for React (ex. Metamask) The [React Setup Guide](/developers/react-guide/setup/overview) shows how we can also use the TypeScript SDK to delay signing & sending transactions to a JSON-RPC account like Metamask. # FAQ Source: https://docs.datafdn.org/faq Get answers to the most common questions about the DATA Foundation as a whole. \$DATA Your preference obviously, since you can use any EVM-based wallet. But we recommend [MetaMask](https://metamask.io/) for [OKX](https://www.okx.com/web3). You can add DATA Foundation's L1 below: Connect your wallet to DATA Network. Connect your wallet to the Aeneid testnet. We don't have a native stablecoin right now. You can use bridge USDC.e powered by [Stargate](https://stargate.finance/bridge). Use [Stargate](https://stargate.finance/bridge), [deBridge](https://app.debridge.finance/?inputChain=1\&outputChain=1514\&inputCurrency=\&outputCurrency=0xf1815bd50389c46847f0bda824ec8da914045d14\&dlnMode=simple\&address=\&amount=1), [Orbiter Finance](https://www.orbiter.finance/en?tgt_chain=1514\&src_chain=1\&src_token=ETH) The DATA Foundation isn't replacing the legal system, it's providing on-chain rails to make the legal system more efficient for creative IP. We have worked with world class legal teams to craft a real, off-chain legal contract called the [Programmable IP License (PIL💊)](/concepts/programmable-ip-license) that has simple terms allowing creators to state who can remix, monetize, and create derivatives of their IP and at what cost. We've then built business logic on-chain (in the form of smart contracts) to automate & enforce those terms. This creates a tight mapping between the legal world and our on-chain terms. You would mint an NFT that represents your off-chain asset, and register that NFT on the DATA Foundation. View the [disclaimers](/foundation/disclaimer). # Introduction Source: https://docs.datafdn.org/introduction The DATA Foundation: prove where data came from, keep it confidential, and define the rights over it. The DATA Foundation ## The DATA Foundation The DATA Foundation is the home for data, built on a purpose-built layer 1 blockchain. It's where data is given an origin, a level of confidentiality, and a clear set of rights, all programmable through a simple API. Every piece of data on the DATA Foundation can be: * **Proven**: a verifiable, portable record of where it came from and under what terms it was contributed * **Confidential**: encrypted so only the parties you authorize can ever decrypt it * **Governed**: owned, licensed, and monetized under transparent on-chain terms Data could be an image, a song, a dataset, an RWA, AI training data, or anything in-between. ## The Three Layers Verifiable, provider-normalized provenance: content hashes, contributor consent, and KYC signals, with public audit views over the whole dataset. Confidential Data Rails: threshold-encrypt data and gate decryption with on-chain access control: no single party ever holds the full key. Register data as IP and attach on-chain license terms that define who can use it and how, the rights layer the other two build on. Together they let you put data on the DATA Foundation that is **provable** (Trace), **confidential** (CDR), and **governed by clear usage rights** (IP & Licensing). ## Start Building Add the network, grab the code examples, and ship your first integration. Encrypt, store, and gate access to confidential data with `@piplabs/cdr-sdk`. Register data provenance over a simple REST API, no SDK required. Every function in the CDR, Protocol, and Python SDKs, with working examples. ## How It Fits Together The DATA Network is a purpose-built layer 1 that combines the best of the EVM and the Cosmos SDK. It is 100% EVM-compatible, with execution-layer optimizations for traversing complex data structures (like IP graphs) in seconds at marginal cost, and a CometBFT-based consensus layer for fast finality and cheap transactions. On top of the network, the three layers compose: * **Trace** records where data came from and the terms it was contributed under. * **CDR** keeps that data encrypted and gates decryption on-chain, often on the very licenses defined below. * **IP & Licensing** registers data as an [🧩 IP Asset](/concepts/ip-asset) and attaches transparent terms through the [📜 Licensing Module](/concepts/licensing-module). Although enforced on-chain, these terms map to an off-chain legal contract, the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license). Data is the most valuable asset of the AI era, yet the systems we use to share it weren't built for it. Creators lose control and credit the moment their work goes online; AI is trained on copyrighted and unconsented data; and there's no portable, verifiable record of where any of it came from. The DATA Foundation fixes this. Data registered on it carries its provenance with it (Trace), can stay confidential until access is earned (CDR), and is governed by transparent, programmable rights (IP & Licensing), so contributors are credited and fairly rewarded when their data is used, including by AI. ## Quick FAQs \$DATA Your preference obviously, since you can use any EVM-based wallet. But we recommend [MetaMask](https://metamask.io/) or [OKX](https://www.okx.com/web3). You can add DATA Foundation's L1 below: Connect your wallet to DATA Network. Connect your wallet to the Aeneid testnet. You can use bridged USDC.e powered by [Stargate](https://stargate.finance/bridge). Use [Stargate](https://stargate.finance/bridge), [deBridge](https://app.debridge.finance/?inputChain=1\&outputChain=1514\&inputCurrency=\&outputCurrency=0xf1815bd50389c46847f0bda824ec8da914045d14\&dlnMode=simple\&address=\&amount=1), or [Orbiter Finance](https://www.orbiter.finance/en?tgt_chain=1514\&src_chain=1\&src_token=ETH). # Aeneid testnet Source: https://docs.datafdn.org/network/connect/aeneid Information and resources for the Aeneid testnet # Resources **Network Name**: Aeneid testnet **Chain ID**: 1315 **Currency**: \$DATA **RPC URL**: `https://aeneid.datarpc.io` **Explorer**: [https://aeneid.datanetscan.io](https://aeneid.datanetscan.io/) ## RPCs | RPC Name | RPC URL | Official | | :-------------- | :-------------------------- | :------: | | DATA Foundation | `https://aeneid.datarpc.io` | ✅ | ## Explorers | Explorer | URL | Official | | :------------------------------------------------------------------------------------------------------------------------ | :------------------------------------ | :------: | | [Blockscout Explorer ↗️](https://aeneid.datanetscan.io) | `https://aeneid.datanetscan.io` | ✅ | | [IP Explorer ↗️](https://aeneid.explorer.datafdn.org) (only for IP-related actions like licensing, minting licenses, etc) | `https://aeneid.explorer.datafdn.org` | ✅ | | [Stakeme Explorer ↗️](https://aeneid.storyscan.app/) | `https://aeneid.storyscan.app/` | | ## Faucet | Faucet | Amount | | :------------------------------------------------------- | :----- | | [Official Faucet ↗️](https://aeneid.faucet.datafdn.org/) | 10 IP | ## Staking Dashboard | Dashboard URL | Official | | :--------------------------------------------------------------- | :------: | | [DATA Foundation Dashboard](https://aeneid.staking.datafdn.org/) | ✅ | ## Contract Deployment Addresses * [Proof of Creativity](/developers/deployed-smart-contracts) # Mainnet Source: https://docs.datafdn.org/network/connect/mainnet Information and Resources for the DATA Network # Resources **Network Name**: DATA Network **Chain ID**: 1514 **Currency**: \$DATA **RPC URL**: `https://mainnet.datarpc.io` **Explorer**: [https://datanetscan.io](https://datanetscan.io) ## RPCs | RPC Name | RPC URL | Official | | :-------------- | :--------------------------- | :------: | | DATA Foundation | `https://mainnet.datarpc.io` | ✅ | ## Block Explorers | Explorer | URL | Official | | :----------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------- | :------: | | [BlockScout Explorer ↗️](https://datanetscan.io) | `https://datanetscan.io` | ✅ | | [IP Explorer ↗️](https://explorer.datafdn.org) (only for IP-related actions like licensing, minting licenses, etc) | `https://explorer.datafdn.org` | ✅ | | [Stakeme Explorer ↗️](https://storyscan.app/) | `https://storyscan.app/` | | | [Nodes.Guru ↗️](https://story.explorers.guru/) | `https://story.explorers.guru/` | | | [CroutonDigital ↗️](https://explorer.crouton.digital/mainnets/story/overview) | `https://explorer.crouton.digital/mainnets/story/overview` | | | [DeSpread ↗️](https://vp.despreadlabs.io/explorer/mainnet/story) | `https://vp.despreadlabs.io/explorer/mainnet/story` | | | [Noders ↗️](https://ipstoryhub.org/explorer) | `https://ipstoryhub.org/explorer` | | ## Staking & Validator Dashboard | Dashboard URL | Official | | :----------------------------------------------------------------------------------------- | :------: | | [DATA Foundation Dashboard](https://staking.datafdn.org/) | ✅ | | [Node.Guru](https://story.explorers.guru/) | | | [Krews](https://story-dashboard.krews.xyz/story/validators) | | | [IT Rocket](https://itrocket.net/services/mainnet/story/analytics/validators-performance/) | | ## Contract Deployment Addresses * [Proof of Creativity](/developers/deployed-smart-contracts) # Consensus Layer (CL) Source: https://docs.datafdn.org/network/learn/node-software/consensus_layer The **Consensus Layer (CL)** is built on the Cosmos SDK and CometBFT. The Cosmos SDK provides a modular framework for building blockchain applications, enabling seamless integration of new modules and features while allowing the network to be easily extended and customized. `story` client introduces upgrades and additional Cosmos SDK modules to support Engine API integration and novel staking mechanisms. CometBFT, a high-performance, scalable, and secure consensus engine, has been extensively tested within the Cosmos ecosystem. CometBFT and Cosmos SDK communicate through ABCI++ interface(link to ABCI++ spec). Checkout [story](https://github.com/piplabs/story) repo to review the codes. # Overview Source: https://docs.datafdn.org/network/learn/node-software/cosmos-modules/cosmos-module-overview List of all production-grade modules used on the DATA Network # List of Modules Here is a list of all production-grade modules that can be used on the DATA Network, along with their respective documentation: * [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) - Handles Cosmos-side logics on each EVM state transition via the [Engine API](/network/node-architecture/engine-api). * [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) - Handles staking and network emission logics with queues. * [mint](/network/node-architecture/cosmos-modules/mint-module) ## Cosmos SDK (Modified) The DATA Network uses the following Cosmos SDK modules with some modifications: * [staking](/network/node-architecture/cosmos-modules/staking-module) * [distribution](https://docs.cosmos.network/main/build/modules/distribution) ## Cosmos SDK (Unmodified) The DATA Network uses the following Cosmos SDK modules without non-trivial modifications: * [auth](https://docs.cosmos.network/main/build/modules/auth) * [bank](https://docs.cosmos.network/main/build/modules/bank) * [consensusparams](https://docs.cosmos.network/main/build/modules/consensus) * [gov](https://docs.cosmos.network/main/build/modules/gov) * [slashing](https://docs.cosmos.network/main/build/modules/slashing) * [upgrade](https://docs.cosmos.network/main/build/modules/upgrade) # EVM engine module Source: https://docs.datafdn.org/network/learn/node-software/cosmos-modules/evmengine-module Module that facilitates communication between consensus and execution layers ## Abstract This document specifies the internal `x/evmengine` module of the DATA Network. As the DATA Network separates the consensus and execution client, like Ethereum, the consensus client (CL) and execution client (EL) needs to communicate to sync to the network, propose proper EVM blocks, and execute EVM-triggered EL actions in CL. The module exists to facilitate all communications between CL and EL using the [Engine API](/network/node-architecture/engine-api), from staking and upgrades to driving block production and consensus in CL and EL. ## Contents 1. **[State](#state)** 2. **[Prepare Proposal](#prepare-proposal)** 3. **[Process Proposal](#process-proposal)** 4. **[Post Finalize](#post-finalize)** 5. **[Messages](#messages)** 6. **[UBI](#ubi)** 7. **[Upgrades](#upgrades)** ## State ### Build Delay Type: `time.Duration` Build delay determines the wait duration from the start of `PrepareProposal` ABCI2 call before fetching the next EVM block data to propose from EL via the [Engine API](/network/node-architecture/engine-api). Applicable to the current proposer only. If the node has a block optimistically built beforehand, the build delay is not used. ### Build Optimistic Type: `bool` Enable optimistic building of a block if true. A node will deterministically build the next block if it finds itself as the next proposer in the current block. Optimistic building starts with requesting the next EVM block data (for the next CL block) immediately after the `FinalizeBlock` of ABCI2. ### Head Table Type: `ExecutionHeadTable` Head table stores the latest execution head data to be used for partial validation of EVM blocks received from other validators. When the chain initializes, the execution head is populated with the genesis execution hash loaded from `genesis.json`. The following execution head is stored in the table. ```protobuf protobuf theme={null} message ExecutionHead { option (cosmos.orm.v1.table) = { id: 1; primary_key: { fields: "id", auto_increment: true } }; uint64 id = 1; // Auto-incremented ID (always and only 1). uint64 created_height = 2; // Consensus chain height this execution block was created in. uint64 block_height = 3; // Execution block height. bytes block_hash = 4; // Execution block hash. uint64 block_time = 5; // Execution block time. } ``` ### Upgrade Contract Type: `*bindings.UpgradeEntrypoint` Upgrade contract is used to filter and parse upgrade-related events from EL. ### UBI Contract Type: `*bindings.UBIPool` UBI contract is used to filter and parse UBI-related events from EL. ### Mutable Payload Type: struct Mutable payload stores the optimistic block built, if optimistic building is enabled. #### Genesis State The module's `GenesisState` defines the state necessary for initializing the chain from a previously exported height. ```protobuf protobuf theme={null} message GenesisState { Params params = 1 [(gogoproto.nullable) = false]; } message Params { bytes execution_block_hash = 1 [ (gogoproto.moretags) = "yaml:\"execution_block_hash\"" ]; } ``` ## Prepare Proposal At each block, if the node is the proposer, ABCI2 triggers `PrepareProposal` which 1. Loads staking & reward withdrawals from the [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) module. 2. Builds a valid EVM block. * If optimistic building: loads the optimistically built block. * Non-optimistic: requests and retrieves an EVM block from EL. 3. Collects the EVM logs of the previous/parent block. 4. Assembles `MsgExecutionPayload` with the built EVM block and previous EVM logs. 5. Returns a transaction containing the assembled `MsgExecutionPayload` data. This CL block is then propagated to all other validators. ## Process Proposal At each block, if the node is not a proposer but a validator, ABCI2 triggers `ProcessProposal` with received commits (which should be a transaction of `MsgExecutionPayload` data in the honest case). The node first validates that the received commit has only one transaction with at least 2/3 of votes committed. Then, the node validates that the one transaction only contains one unmarshalled `MsgExecutionPayload` data. Finally, the node processes the received data and broadcasts its acceptance of the proposal to the network. If any of the validation or processing fails, the node rejects the proposal. More specifically, the node processes the received `MsgExecutionPayload` data in the following manner: 1. Validates the fields of the received `MsgExecutionPayload` (outlined in [Messages](#msgexecutionpayload)). 2. Compare local stake & reward withdrawals with the received withdrawals data. 3. Push the received execution payload to EL via the Engine API and wait for payload validation. 4. Update the EL forkchoice to the execution payload's block hash. 5. Process staking events using the [evmstaking](/network/node-architecture/cosmos-modules/evmstaking-module) module. 6. Process upgrade events. 7. Update the execution head to the execution payload (finalized block). ## Post Finalize If optimistic building is enabled, `PostFinalize` is triggered immediately after `FinalizeBlock` set through custom ABCI callback. During this process, the node peeks the staking and reward queues from the evmstaking module, and builds a new execution payload on top of the current execution head. It sets the optimistic block to be used in the next block's `PrepareProposal` phase and returns the response from the forkchoice update. ## Messages In this section we describe the processing of the evmengine messages and the corresponding updates to the state. All created/modified state objects specified by each message are defined within the state section. ### MsgExecutionPayload ```protobuf protobuf theme={null} message MsgExecutionPayload { option (cosmos.msg.v1.signer) = "authority"; string authority = 1; bytes execution_payload = 2; repeated EVMEvent prev_payload_events = 3; } message EVMEvent { bytes address = 1; repeated bytes topics = 2; bytes data = 3; bytes tx_hash = 4; } ``` This message is expected to fail if: * authority is invalid (not evmengine authority) * execution payload fails to unmarshal to [ExecutableData](https://github.com/piplabs/story/blob/c38b80c13579d3df7174ea10c3368ef0692f52da/client/x/evmengine/types/executable_data.go#L17-L35) for reasons such as invalid fields * execution payload's block number does not match CL head's block number + 1 * execution payload's block parent hash does not match CL head's hash * execution payload's timestamp is invalid * execution payload's RANDAO does not match CL head's hash (ie. parent hash) * execution payload's `Withdrawals`, `BlobGasUsed`, and `ExcessBlobGas` fields are nil * execution payload's `Withdrawals` count does not match local node's sum of dequeued stake & reward withdrawals The message must contain previous block's events, which gets processed at the current CL block (in other words, execution events from EL block n-1 are processed at CL block n). In the future, the message will remove `prev_payload_events` and rely on [Engine API](/network/node-architecture/engine-api) to get the current finalized EL block's events. Also note that EVM events are processed in CL in the order they are generated in EL. ## UBI All UBI-related changes must be triggered from the canonical UBI contract in the EVM execution layer. This module handles the execution handling of those triggers in CL. Read more about [UBI for validators](/network/tokenomics-staking#ubi-for-validators) ### Set UBI Distribution The `UBIPool` contract emits the UBI distribution set event, which is parsed by the module to set the UBI percentage in the distribution module. ## Upgrades All chain upgrade-related logics must be triggered from the canonical upgrade contract in the EVM execution layer. This module handles the execution handling of those triggers in CL. ### Software Upgrade The `UpgradeEntrypoint` contract emits the software upgrade event, which is parsed by the module to schedule an upgrade at a given height for a given binary name. Currently, all upgrades must either be set via forks or by the software upgrade events; the latter process is a multisig-controlled process, which will transition into a voting-based process in the future. ### Cancel Upgrade Similar to the software upgrade, the module processes the cancel upgrade event from EVM logs of the previous block, and clears an existing upgrade plan. # EVM staking module Source: https://docs.datafdn.org/network/learn/node-software/cosmos-modules/evmstaking-module Module that facilitates consensus layer staking-related logic ## Abstract This document specifies the internal `x/evmstaking` module of the DATA Network. In the DATA Network, the gas token resides on the execution layer (EL) to pay for transactions and interact with smart contracts. However, the consensus layer (CL) manages the consensus staking, slashing, and rewarding. This module exists to facilitate CL-level staking-related logic, such as delegating to validators with custom lock periods. ## Contents 1. **[State](#state)** 2. **[Two Queue System](#two-queue-system)** 3. **[Withdrawal Queue Content](#withdrawal-queue-content)** 4. **[End Block](#end-block)** 5. **[Processing Staking Events](#processing-staking-events)** 6. **[Withdrawing Delegations](#withdrawing-delegations)** 7. **[Withdrawing Rewards](#withdrawing-rewards)** 8. **[Withdrawing UBI](#withdrawing-ubi)** ## State ### Withdrawal Queue Type: `Queue[types.Withdrawal]` The (stake) withdrawal queue stores the pending unbonded stakes to be burned on CL and minted on EL. Stakes that are unbonded after 14 days of unstaking period are added to the queue to be processed. ### Reward Withdrawal Queue Type: `Queue[types.Withdrawal]` The reward withdrawal queue stores the pending rewards from stakes to be burned on CL and minted on EL. All rewards above a threshold are eligible to be queued in this queue, but there exists a parameter of maximum additions per block. ### Parameters ```protobuf protobuf theme={null} message Params { uint32 max_withdrawal_per_block = 1 [ (gogoproto.moretags) = "yaml:\"max_withdrawal_per_block\"" ]; uint32 max_sweep_per_block = 2 [ (gogoproto.moretags) = "yaml:\"max_sweep_per_block\"" ]; uint64 min_partial_withdrawal_amount = 3 [ (gogoproto.moretags) = "yaml:\"min_partial_withdrawal_amount\"" ]; string ubi_withdraw_address = 4 [ (gogoproto.moretags) = "yaml:\"ubi_withdraw_address\"" ]; } ``` * `max_withdrawal_per_block` is the maximum number of withdrawals (reward and unstakes, each) to process per block. This parameter prevents nodes from processing a large amount of withdrawals at once, which could exceed the max chain timeout. * `max_sweep_per_block` is the maximum number of validator-delegator delegations to sweep per block. This parameter prevents nodes from processing a large amount of delegations at once. * `min_partial_withdrawal_amount` is the minimum amount required for rewards to get added to the reward withdrawal queue. * `ubi_withdrawal_address` is the UBI contract address to which UBI withdrawals should be deposited. ### Delegator Withdraw Address Type: `Map[string, string]` The delegator-withdraw address mapping tracks the address to which a delegator receives their withdrawn stakes. The (stake) withdrawal queue uses this map to determine the `execution_address` in the `Withdrawal` struct used in building an EVM block payload. While the delegator can change the withdraw address at any time, existing stake withdraw requests in the (stake) withdrawal queue will maintain their original values. ### Delegator Reward Address The delegator-reward address mapping tracks the address to which a delegator receives their reward stakes, similar to the delegator-withdraw mapping. While the delegator can change the reward address at any time, existing reward withdraw requests in the reward withdrawal queue will maintain their original values. Type: `Map[string, string]` ### Delegator Operator Address Type: `Map[string, string]` The delegator-operator address mapping tracks the address to which a delegator has given the privilege to delegate (stake), undelegate (unstake), and redelegate on behalf of themselves. ### IP Token Staking Contract Type: `*bindings.IPTokenStaking` IPTokenStaking contract is used to filter and parse staking-related events from EL. ## Two Queue System The module departs from traditional Cosmos SDK staking module's unstaking system, where all unbonded entries (stakes that have unbonded after 14 days of unbonding period) are immediately distributed into delegators account. Instead, the DATA Foundation's unstaking system assimilates Ethereum 2.0's unstaking system, where 16 full or partial (reward) withdrawals are processed per slot. In a single queue of withdrawals, reward withdrawals can significantly delay stake withdrawals. Hence, the DATA Network implements a two-queue system where a max amount to process per block is enforced per queue. In other words, the stake/ubi withdrawal and reward withdrawal queues can each process the max parameter per block. ## Withdrawal Queue Content Since the module only processes unstakes/rewards/ubi and stores them in queues, the actual dequeueing for withdrawal to the execution layer is carried out in the [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) module. More specifically, a proposer dequeues the max number of withdrawals from each queue and adds them to the EVM block payload, which gets executed by EL via the [Engine API](/network/node-architecture/engine-api). When validators receive proposed block payload from the proposer, they individually peek the local queues and compare them against the received block's withdrawals. Mismatching withdrawals indicate non-determinism in staking logics and should result in chain halt. In other words, the `evmstaking` module is in charge of parsing, processing, and inserting withdrawal requests to two queues, while the `evmengine` module is in charge of validating and dequeuing withdrawal requests, as well as depositing them to corresponding withdrawal addresses in EL. ## End Block The `EndBlock` ABCI2 call is responsible for fetching the unbonded entries (stakes that have unbonded after 14 days) from the [staking](/network/node-architecture/cosmos-modules/staking-module) module and inserting them into the (stake) withdrawal queue. Furthermore, it processes stake reward withdrawals into the reward withdrawal queue and UBI withdrawals into the (stake) withdrawal queue. If the network is in the [Singularity period](/network/tokenomics-staking#singularity), the End Block is skipped as there are no staking rewards and withdrawals available during this period. Otherwise, refer to [Withdrawing Delegations](#withdrawing-delegations) and [Withdrawing Rewards](#withdrawing-rewards) for detailed withdrawal processes. ## Processing Staking Events The module parses and processes staking events emitted from the [IPTokenStaking contract](https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol), which are collected by the [evmengine](/network/node-architecture/cosmos-modules/evmengine-module) module. The list of events are: ### Staking Events * Create Validator * Deposit (delegate) * Withdraw (undelegate) * Redelegate * Unjail: anyone can request to unjail a jailed validator by paying the unjail fee in the contract. These operations incur a fixed gas cost to prevent spam. ### Parameter Events * Update Validator Commission: update the validator commission. * Set Withdrawal Address: delegator can modify their withdrawal address for future unstakes/undelegations. * Set Reward Address: delegator can modify their withdrawal address for future reward emissions. * Set Operator: delegator can modify their operator with privileges of delegation, undelegation, and redelegation. * Unset Operator: delegator can remove operator. These operations incur a fixed gas cost to prevent spam. ## Withdrawal Both withdrawal queues hold withdrawals of type: ```protobuf protobuf theme={null} message Withdrawal { option (gogoproto.equal) = true; option (gogoproto.goproto_getters) = false; uint64 creation_height = 1; string execution_address = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString", (gogoproto.moretags) = "yaml:\"execution_address\"" ]; uint64 amount = 3 [ (gogoproto.moretags) = "yaml:\"amount\"" ]; WithdrawalType withdrawal_type = 4 [ (gogoproto.moretags) = "yaml:\"withdrawal_type\"" ]; string validator_address = 5 [ (gogoproto.moretags) = "yaml:\"validator_address\"" ]; } ``` * `creation_height` is the block height at which the withdrawal is created. * `execution_address` is the EVM address receiving the withdrawn fund, which is burned in CL. * `amount` is the amount to burn on CL and mint on EL. * `withdrawal_type` is the type of withdrawal: $0$ for unstakes, $1$ for reward, and $2$ for UBI. * `validator_address` is the EVM validator address. ### Withdrawing Delegations Delegations that have unbonded after 14 days of unbonding period (ie. unbonded entries) gets added to the (stake) withdrawal queue at the end of each block. If validator is totally-unstaked, ie. all delegations and self-delegations are unbonded, then validator's commission is also withdrawn. ### Withdrawing Rewards Inflation rewards allocated to delegations are auto-swept at the end of each block. If a delegation's accrued reward is greater than the parameterized threshold, the reward is added to the reward withdrawal queue to be credited to the delegator's EVM reward address. # Token Minting Module Source: https://docs.datafdn.org/network/learn/node-software/cosmos-modules/mint-module Module responsible for token minting and inflation in the DATA Network ## Contents 1. [Contents](#contents) 2. [State](#state) 3. [Begin Block](#begin-block) 4. [Parameters](#parameters) 5. [Events](#events) ## State ### Params * Params: `mint/params -> legacy_amino(params)` ```protobuf protobuf theme={null} message Params { option (amino.name) = "client/x/mint/Params"; // type of coin to mint string mint_denom = 1; // inflation amount per year string inflations_per_year = 2 [ (cosmos_proto.scalar) = "cosmos.Dec", (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", (gogoproto.nullable) = false ]; // expected blocks per year uint64 blocks_per_year = 3; } ``` ## Begin Block Minting parameters are calculated and inflation paid at the beginning of each block. ### Inflation Amount Calculation Inflation amount is calculated using an "inflation calculation function" that's\ passed to the `NewAppModule` function. If no function is passed, then the SDK's default inflation function will be used (`DefaultInflationCalculationFn`). In case a custom inflation calculation logic is needed, this can be achieved by defining and passing a function that matches `InflationCalculationFn`'s signature. ```go theme={null} type InflationCalculationFn func(ctx sdk.Context, minter Minter, params Params, bondedRatio math.LegacyDec) math.LegacyDec ``` ## Parameters The minting module contains the following parameters: | Key | Type | Example | | ----------------- | --------------- | ------------------- | | MintDenom | string | "stake" | | InflationsPerYear | string (dec) | "20000000000000000" | | BlocksPerYear | string (uint64) | "10368000" | * `MintDenom` is the coin denominator used. * `InflationsPerYear` is the target inflation per year, in 1e18 decimals. * `BlocksPerYear` is the target number of blocks per year. ## Events The minting module emits the following events: ### BeginBlocker | Type | Attribute Key | Attribute Value | | :--- | :------------ | :-------------- | | mint | amount | "1000" | # Staking Module Source: https://docs.datafdn.org/network/learn/node-software/cosmos-modules/staking-module Modified staking module with reward multipliers for locked and unlocked tokens ## Abstract The staking module has been modified to accommodate for the following changes below. Refer to the Cosmos SDK's [staking module docs](https://docs.cosmos.network/main/build/modules/staking) for more information. ## Reward Multiplier ### Validators Validators can choose to accept either locked tokens or unlocked tokens as delegations. Validators for locked tokens are conditioned to half the inflation allocation of validators for unlocked tokens. Since each validator receives different inflation distribution based on delegations, the inflation distribution Ivi for validator vi in the rewards pool is calculated as follows: where * Ivi is the total inflationary token rewards for vi * Svi is the staked tokens for vi * Mvi is the rewards multiplier for vi * 0.5 for locked tokens * 1 for unlocked tokens * Rn is the total inflationary tokens allocated for the rewards pool in block n, calculated in the [mint](/network/node-architecture/cosmos-modules/mint-module) module ### Delegations Delegators can delegate with four different staking lock times, which results in different staking reward multiplier for each delegation (delegator-validator pair of stakes). The inflation distribution for each delegation Di is calculated as follows: where * Sdi is the staked tokens of delegation di on validator vd * Mdi is the rewards multiplier of di on vd * Iv is the total inflationary token rewards for vd * Cv is the commission rate for vd #### Time-Weighted Reward Multiplier Mdi * *Flexible* (no lockup): 1 * *Short* (90 days): 1.1 * *Medium* (360 days): 1.5 * *Long* (540 days): 2.0 # Engine API Source: https://docs.datafdn.org/network/learn/node-software/engine_api The Engine API is a collection of JSON-RPC methods that facilitate communication between the execution layer (EL) and the consensus layer (CL) of an EVM node. The DATA Foundation's execution layer, which offers full EVM compatibility, supports all standard JSON-RPC methods defined by the [Ethereum Engine API](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md). Meanwhile, the DATA Foundation's consensus layer, built on Cosmos modules, utilizes the Engine API to coordinate with the execution layer. ## Functionalities The Engine API facilitates seamless interaction between the EL and the CL by providing essential coordination mechanisms, including: * **Handshake** * **Synchronization** * **Block Validation** * **Block Proposal** ## Execution Layer Implementation The EL in the DATA Network implements the following standard Engine API methods to support these functionalities: * `engine_exchangeCapabilities`: Exchanges supported methods. * `engine_getClientVersion`: Exchanges client version data. * `engine_newPayload`: Inserts the given payload into the local chain. * `engine_forkchoiceUpdate`: Updates the canonical chain marker and generates the payload with given attributes. * `engine_getPayload`: Retrieves the pre-generated payload. ## Consensus Layer Interaction How does the DATA Foundation's Consensus Layer (CL) interact with these methods? The answer lies in CometBFT ABCI++. CometBFT is a state machine replication engine which provides consensus and security for Cosmos modules. ABCI++, also known as ABCI 2.0, is the interface between CometBFT and the actual state machine being replicated(i.e. EL's state machine). ABCI++ comprises of a set of methods that interact with the Engine API, as outlined below: ### **1. PrepareProposal** (Proposing a New Block) * The CL checks whether a payload is already being generated using `payloadID`. * If not, the CL calls `engine_forkchoiceUpdate` to trigger a new payload generation. * The CL then calls `engine_getPayload` with `payloadID` to fetch the payload and propose a new block. ### **2. ProcessProposal** (Processing a New Block) * The CL calls `engine_newPayload` to delivers the new block to the EL. * The EL validates payload of the new block, executes transactions deterministically and updates its state. ### **3. FinalizeBlock** (Finalizing a Decided Block) * The CL calls `engine_newPayload` to delivers the finalized block to the EL. * If the block has not yet been incorporated into the EL, the EL validates payload of the new block, executes transactions deterministically and updates its state. * Since CometBFT provides instant finality, the CL calls `engine_forkchoiceUpdate` to finalize the block. * Finally, the CL calls `engine_forkchoiceUpdate` again, with extra attributes, to start an optimistic build of the next block if enabled, and if the validator is the next proposer. This interaction ensures smooth coordination between the EL and the CL, maintaining the integrity and efficiency of the DATA Network. # Execution Layer (EL) Source: https://docs.datafdn.org/network/learn/node-software/execution_layer The **Execution Layer (EL)** is a fork of the Geth client, with the addition of the [IPGraph Precompile](/network/node-architecture/precompile) and [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) precompile. It handles transaction execution, broadcasting and state storage while maintaining full compatibility with the Ethereum Virtual Machine (EVM) and supporting all Ethereum JSON-RPC methods. Currently we support all features introduced by the Pectra upgrade. Checkout [story-geth](https://github.com/piplabs/story-geth) repo to review the codes. # Run a localnet Source: https://docs.datafdn.org/network/learn/node-software/localnet Guide to setting up and running a local DATA Network for development and testing # Overview You can easily set up your own local DATA Network using docker compose, consisting of one boot node and four validator nodes. With this local network, you can test the consensus layer of the DATA Network or deploy your application using the precompiled primitive, the IP graph, to conduct various tests. Additionally, you can reset the network at any time as needed. # Run a Local DATA Network For more detailed information for running the DATA Foundation local network, please refer the repository: [https://github.com/piplabs/story-localnet](https://github.com/piplabs/story-localnet) ## Prerequisite To set up a local network, [Docker](https://docs.docker.com/get-started/get-docker/) is required. ## Step 1 - Start Docker Please run Docker. ## Step 2 - Clone Repository You need to clone three repositories: `story`, `story-geth`, and `story-localnet`.\ Make sure all three repositories are located within the same subfolder. ```bash theme={null} # clone repositories git clone https://github.com/piplabs/story.git git clone https://github.com/piplabs/story-geth.git git clone https://github.com/piplabs/story-localnet.git ``` ## Step 3 - Start Nodes Navigate to story-localnet project and start the local network. ```bash theme={null} # move to story-localnet cd story-localnet # start story local network ./start.sh ``` ## Step 4 - Terminate Nodes If you want to stop the DATA Foundation local network, you can do so by executing the script below. ```bash theme={null} # terminate story local network ./terminate.sh ``` *** ## How to Allocate Token to Your Account From Genesis You may need to allocate DATA tokens to your account for testing in the local network.\ To allocate tokens to your account in the genesis block, follow these steps: 1. Add your account information to the alloc section in `config/story/genesis-geth.json`: ```json theme={null} "": { "nonce": "0x0", "balance": "", "code": "0x", "storage": {} } ``` 2. Run the `update-genesis-hash.sh` script to update the genesis block hash: ```bash theme={null} ./update-genesis-hash.sh ``` *** ## How to Interact With the DATA Foundation Local Network By default, the DATA Foundation local network has the following ports open for interaction. | Port | Service | Role | | :---- | :--------- | :------------------------------------------------------------------------------- | | 8545 | story-geth | Endpoint of RPC server for the DATA Foundation execution client | | 1317 | story-node | Endpoint of API server for interacting with the DATA Foundation consensus client | | 26657 | story-node | Endpoint of cosmos-sdk RPC server for the DATA Foundation consensus client | *** ## Monitoring Systems This setup includes a monitoring stack to provide centralized metrics and logs\ visualization for the blockchain network. Tools include **Prometheus**, **Loki**, **Promtail**, and **Grafana**, all integrated through Docker Compose. ### **Components and Access Information** | Service | Role | Default Port | Access URL | | :--------- | :---------------------------------------------------------------- | :----------------------------- | :---------------------- | | Prometheus | Collects metrics from nodes and itself for performance monitoring | `9090` | `http://localhost:9090` | | Loki | Aggregates and stores logs from the network nodes via Promtail | `3100` | `http://localhost:3100` | | Promtail | Scrapes logs from Docker containers and sends them to Loki | `9080` (API), `9095` (Metrics) | `http://localhost:9080` | | Grafana | Provides a dashboard interface for metrics and logs visualization | `3000` | `http://localhost:3000` | # Learn about the DATA Foundation's node software Source: https://docs.datafdn.org/network/learn/node-software/overview The DATA Foundation's node client software has been implemented to support full EVM equivalency and fast block time and one-shot finality. The DATA Foundation's node software consists of two components: an [Execution Layer (EL)](/network/learn/node-software/execution_layer) responsible for the execution environment and a [ Consensus Layer (CL)](/network/learn/node-software/consensus_layer) responsible for the consensus and block formation. These two layers communicate via the [Engine API](/network/learn/node-software/engine-api) presented [Here](https://hackmd.io/@danielrachi/engine_api). # Precompiles Source: https://docs.datafdn.org/network/learn/node-software/precompiled-contracts Specialized smart contracts implemented in the DATA Foundation's execution layer ## Introduction Precompiled contracts are specialized smart contracts implemented directly in the execution layer of a blockchain. Unlike user-deployed smart contracts that execute EVM bytecode, precompiled contracts offer optimized native implementations for complex cryptographic and computational operations. This significantly improves efficiency and reduces gas costs. Precompiled contracts exist at fixed addresses within the execution client and each precompile has a predefined gas cost based on its computational complexity, ensuring predictable execution fees. The DATA Foundation introduces two precompiled contracts: * `p256Verify` precompile to support signature verifications in the secp256r1 elliptic curve. * `ipgraph` precompile to enhance on-chain intellectual property management. In addition, the DATA Foundation’s execution layer supports all standard EVM precompiled contracts, ensuring full compatibility with Ethereum-based tooling and applications. ## Precompiled Contracts | Address | Functionality | | ------- | ------------------------------------------------------------- | | byte1 | `ecrecover`- ECDSA signature recovery | | byte2 | `sha256` - SHA-256 hash computation | | byte3 | `ripemd160` - RIPEMD-160 hash computation | | byte4 | `identity` - Identity function | | byte5 | `modexp` - Modular exponentiation | | byte6 | `bn256Add` - BN256 elliptic curve addition | | byte7 | `bn256ScalarMul` - BN256 elliptic curve scalar multiplication | | byte8 | `bn256Pairing` - BN256 elliptic curve pairing check | | byte9 | `blake2f` - Blake2 hash function | | byte10 | `kzgPointEvaluation` - KZG polynomial commitment evaluation | | byte0 | `p256Verify` - Secp256r1 signature verification | | byte1 | `ipgraph` - Intellectual property management | ## p256Verify Precompile Refer to [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) for more information. ## IPgraph Precompile The `ipgraph` precompile enables efficient querying and modification of IP relationships and royalty structures while minimizing gas costs. This contract is deployed at `0x0000000000000000000000000000000000000101` and access is controlled through this contract `0x1640A22a8A086747cD377b73954545e2Dfcc9Cad`. This precompile provides multiple functions based on the function selector: the first 4 bytes of the input. | Function Selector | Description | Gas computation formula | Gas Cost | | :----------------------- | :-------------------------------------------------------------- | :---------------------------------------------------- | :--------------------------------- | | `addParentIp` | Adds a parent IP record | `intrinsicGas + (ipGraphWriteGas * parentCount)` | Larger than 1100 | | `hasParentIp` | Checks if an IP is parent of another IP | `ipGraphReadGas * averageParentIpCount` | 40 | | `getParentIps` | Retrieves parent IPs | `ipGraphReadGas * averageParentIpCount` | 40 | | `getParentIpsCount` | Gets the number of parent IPs | `ipGraphReadGas` | 10 | | `getAncestorIps` | Retrieves ancestor IPs | `ipGraphReadGas * averageAncestorIpCount * 2` | 600 | | `getAncestorIpsCount` | Gets the number of ancestor IPs | `ipGraphReadGas * averageParentIpCount * 2` | 80 | | `hasAncestorIp` | Checks if an IP is ancestor of another IP | `ipGraphReadGas * averageAncestorIpCount * 2` | 600 | | `setRoyalty` | Sets royalty details of an IP | `ipGraphWriteGas` | 1000 | | `getRoyalty` | Retrieves royalty details of an IP | `varies by royalty policy` | LAP:900, LRP:620, other:1000 | | `getRoyaltyStack` | Retrieves royalty stack of an IP | `varies by royalty policy` | LAP:50, LRP: 600, other:1000 | | `hasParentIpExt` | Checks if an IP is parent of another IP through external call | `ipGraphExternalReadGas * averageParentIpCount` | 8400 | | `getParentIpsExt` | Retrieves parent IPs through external call | `ipGraphExternalReadGas * averageParentIpCount` | 8400 | | `getParentIpsCountExt` | Gets the number of parent IPs through external call | `ipGraphExternalReadGas` | 2100 | | `getAncestorIpsExt` | Retrieve ancestor IPs through external call | `ipGraphExternalReadGas * averageAncestorIpCount * 2` | 126000 | | `getAncestorIpsCountExt` | Gets the number of ancestor IPs through external call | `ipGraphExternalReadGas * averageParentIpCount * 2` | 16800 | | `hasAncestorIpExt` | Checks if an IP is ancestor of another IP through external call | `ipGraphExternalReadGas * averageAncestorIpCount * 2` | 126000 | | `getRoyaltyExt` | Retrieves royalty details of an IP through external call | `varies by royalty policy` | LAP:189000, LRP:130200, other:1000 | | `getRoyaltyStackExt` | Retrieves royalty stack of an IP through external call | `varies by royalty policy` | LAP:10500, LRP:126000, other:1000 | # Learn about the DATA Foundation Source: https://docs.datafdn.org/network/learn/overview **The DATA Foundation** is a purpose-built decentralized blockchain supercharged by a *multi-core* execution environment. Its architecture comprises a main execution core alongside multiple highly customized cores. The main core provides EVM equivalence, enabling rapid adoption of existing applications from the ecosystem. The Intellectual Property (IP) core, one of the specialized cores, efficiently handles intellectual property registration as a native asset class while optimizing operations across complex IP relationship graphs. This core transforms intelligence into programmable IP assets. Although the DATA Foundation focuses primarily on intellectual property, its flexible architecture enables the adoption of future cores that can expand far beyond IP-related applications. Learn more about the architecture in the [whitepaper](https://www.datafdn.org/whitepaper.pdf) and understand the token economy design in the DATA Foundation's [Staking Desgin](/network/learn/token-economy) documentation. For details on implementation, see the [Node Software](/network/learn/node-software) chapter. # Staking Design Source: https://docs.datafdn.org/network/learn/token-economy Detailed overview of the DATA Network's staking mechanics and tokenomics # Purpose This document walks through the staking specification for the DATA Foundation. The goal is to provide clarity to network participants and technical partners on how the DATA Foundation's staking mechanics work and how users can interface with our chain. # Tokenomics ## Genesis The DATA Foundation genesis allocation will consist of 1 billion tokens, distributed among ecosystem participants, the foundation, investors, and the core team. Please refer this document for the detailed [Token Distribution](https://www.datafdn.org/blog/introducing-ip). ## Locked vs Unlocked Tokens Unlocked tokens have no restrictions imposed on them and can be used for gas consumption, transfers, and staking. Unlike unlocked tokens, locked tokens cannot be transferred or traded and are unlocked based on an unlock schedule. However, locked tokens may be staked to earn staking rewards, with the locked staking reward rate being half of that of unlocked tokens. Staked locked and unlocked tokens have the same voting power. That means that a validator with 100 staked locked tokens has the same network voting power as a validator with 100 staked unlocked tokens. Both types of tokens can be slashed if their validators get slashed. ## Token Emissions A fixed number of tokens will be allocated for emissions in the first year, with the quantity determined by the foundation at Genesis. For subsequent years, the number of emitted tokens will be controlled by an emissions algorithm whose parameters may be updated via governance or subject to change via hard forks. The emissions per block are controlled by the following two parameters: * blocks\_per\_year: 10368000 blocks * The number of blocks expected to be produced in a year * inflations\_per\_year: 20,000,000 tokens * The total number of inflationary tokens to be emitted in a year New emissions will flow to two places: 1. Block Rewards 2. Community Pool ## Token Burn Since the DATA Foundation uses a fork of geth as the execution client, the burning mechanism follows Ethereum's EIP-1559. # Staking > 🔗 [Stake with the Staking Dashboard ↗️](https://staking.datafdn.org/) The DATA Foundation supports the below staking-related operations * Create validator * Update validator commission * Stake * Stake on behalf * Unstake * Unstake on behalf * Redelegate * Redelegate on behalf * Set withdraw address * Set reward address * Unjail * Unjail on behalf Before explaining the behavior of each of these operations, some high-level concepts like **Token Staking Types**, **Validator Set Status**, **Unbonding**, and **Staking Period** will be explained first: ## Token Staking Types As staking is enabled for both locked and unlocked tokens, validators must choose which type of token staking they want to support. Once a token staking type is selected, validators cannot switch to a different type. ## Validator Set Status In the DATA Network, validators are grouped into one of two sets, (1) the active (bonded) validator set, which participates in consensus and receives block rewards, or (2) the non-active (unbonded) validator set, which does not contribute to the consensus process. To be selected as part of the active validator set, a validator must be one of the top 64 validators ranked by staked tokens. Note that all priority fees on the DATA Network go directly to the block proposer. ## Unbonding Unstaking for delegators is subject to an unbonding process. Users must wait for an unbonding time before any tokens return to their accounts. This is the same for validators who self-delegate to themselves. They also need to go through the unbonding process when they want to unstake. The unbonding time is 14 days. During the unbonding period, the delegator/validator will not earn block rewards. But they may still be slashed. For each validator/delegator pair, the maximum ongoing unbonding transactions is 14. More unbonding requests beyond this limit will fail. ## Staking Period Delegators can decide how flexible and how long they want to stake their tokens. By default, for both locked and unlocked tokens, delegators can stake and then unstake immediately and get their token back after the unbonding time. We call this **flexible staking** in this document. For unlocked tokens, a few more fixed staking periods are supported: 90 days, 360 days, and 540 days. In this case, users can only call unstake after the staking period is mature. Any call earlier than the mature day will be discarded. Unstaking from a mature staking period is still subject to the unbonding process, meaning users will get their staked tokens back after 14 days of unbonding time. Staking in these fixed staking periods earns more rewards. The longer the period, the bigger the reward weight multiplier. Reward multiplier for different periods: * Locked flexible period - **0.5** * Flexible period - **1.0** * 90 days - **1.1** * 360 days - **1.5** * 540 days - **2** For locked tokens, only flexible staking is allowed and the reward multiplier is **0.5**. If a user delegates their locked tokens to a staking period, we will convert that to a flexible staking delegation. After the staking period ends, users can choose not to unstake. In this case, they will continue earning the same reward rate based on the reward rate of the corresponding staking period until they unstake manually. They can unstake at any time after the staking period ends. For example, if the 1-year staking period's reward rate is 0.02% per block, after staking for 1 year, users can still earn 0.02% per block of the reward until they unstake. ## Decimal for Stake Amounts The decimal for stake operations (stake, unstake, redelegate, etc.) is 9. If a user specifies a smaller value, the dust will be refunded back to the users. Or if there is no token transfer involved, the specified value will be rounded down to 9 decimals. # Staking Operations ## Create Validator To become a validator, the validator must first run a validator node based on the latest released story binaries, then call the CreateValidator function with an initial staking amount, moniker, and commission rate. It also needs to set the max commission rate and max commission rate change to make sure it doesn't change the commission rate later dramatically. The minimum commission rate that a validator can set is 5%. The initial staking amount needs to be larger than a threshold, which is 1024 IP. The amount will be deducted from the caller's wallet. It can only be staked to a flexible period. If a validator tries to call create validator function the second time, it will be ignored. ## Update Validator Commission This operation allows validators to edit their validator commission rate. If the updated commission rate is larger than max commission rate or the commission rate change delta is larger than max commission rate change, the operation will fail. A fee of 1 IP will be charged for updating a validator to prevent spamming. The fee will be burnt by the contract. The commission rate can only be updated once per day. It will not throw an error from the contract. But it won't take effect in the consensus layer. ## Stake Both the validator and delegator can stake tokens to a validator. A validator can stake to itself, which is called self-delegation. Users can decide if they want to stake with a fixed staking period or stake without a period (flexible staking). If a fixed period is chosen, a delegation id will be returned to the users. Users must use this delegation id to unstake tokens from this stake operation. If flexible staking is chosen, the returned delegation id will be 0. The staking amount needs to be larger than a threshold, which is 1024 IP. If a delegator delegates to a non-existent validator, the tokens will NOT be refunded. If users specify the token amount that has more than 9 decimal units, the actual staking amount will be rounded down to 9 decimal and refund the remaining back to the users. ## Unstake When staking without a staking period, users can unstake anytime. The tokens will be distributed to the user's account after the unbonding time. A fee of 1 IP will be charged for unstaking to prevent spamming. The fee will be burnt by the contract. When staking with a staking period, users can only unstake after the staking period is mature. The tokens will be distributed to the user's account after the unbonding time. Unstaking requests before the staking period matures will be ignored. The minimum unstaking amount is 1024 IP. After the unstaking request is processed, if the remaining staked amount is less than 1024 IP, the remaining part will also be unstaked together. The unstaking request will first go through the unbonding process, which is 14 days. After that, the unbonded requests are sent to a withdrawal queue, distributing a maximum of 32 withdrawals per block. If there are more than 32 withdrawal requests in the withdrawal queue, the next 32 withdrawal requests will be processed in the next block. Partial unstake of a delegation is supported. For example, if a 1-year long delegation has 1 million tokens, after 1 year, users can unstake 500k from this delegation and keep the remaining staked to continue earning rewards. Unstake can fail if the validator, delegator and delegation id passed in is incorrect. Unstake can also fail if the maximum concurrent unbonding request (currently 14) has been reached for the validator/delegator pair. If the unstake amount passed in is larger than the total unstakable tokens, the current total unstakable amounts will be unstaked. For example, if users unstake 1024 IP and only have 1023 IP stake, 1023 IP will be withdrawn. If a validator exits, by either being offline and getting jailed, or not having enough stakes to be in the top 64 validator set, the delegators can unstake their tokens if the tokens are not in a staking period or their staking period is mature. Otherwise, delegators must wait until the staking period matures to unstake. If users specify the token amount that has more than 9 decimal units, the actual unstaking amount will be rounded down to 9 decimal. ## Redelegate Redelegate operation allows a delegator to move its staked tokens from one validator to another. The tokens can be redelegated to the new validator immediately and start earning rewards. However, the redelegated tokens are still subject to the unbonding process, IF the source validator is in the active validator set or unbonding from the active validator set. During this 14 days unbounding time, it will be slashed if the original validator gets slashed. A fee of 1 IP will be charged for redelegation to prevent spamming. The fee will be burnt by the contract. The minimum redelegation amount is 1024 IP. If a delegator's initial stake is 1024 IP but later gets slashed, it can still redelegate its tokens to another validator even if the token amount is less than 1024 IP. Similarly to unstaking, if the redelegation amount passed in is larger than the total redelegatable tokens, the total redelegatable amounts will be redelegated. If the remaining balance after redelegation is less than 1024 IP, all remaining tokens will be redelegated together. The delegation id will stay the same after the redelegation. Redelegation has its own maximum ongoing unbonding transaction limit per delegator/source validator/destination validator pair, which is also 14. Delegators can choose to redelegate their tokens to another active validator even if their tokens are still in an immature staking period. Their staking period maturation date and reward rate will stay the same. Redelegation can only be triggered when the source and destination validators support the same token type. If users specify the token amount that has more than 9 decimal units, the actual reledegated amount will be rounded down to 9 decimal. ## Set Withdrawal/Reward Address Delegators can call the staking contract to set a withdrawal address. The unstaked tokens will be sent to this withdrawal address. Similarly, delegators can set a separate reward address. All reward distributions will be sent to this address. A fee of 1 IP will be charged for updating either the withdrawal address or the reward address to prevent spamming. The fee will be burnt by the contract. The address change will take effect in the next block. ## Slash/Unjail Slashing penalizes bad behaviors on the validators by slashing out a fraction of their staked tokens. Two types of behaviors can get slashed in the DATA Network: **double sign** and **downtime**. * **double sign**: If a validator double signs for a block, they will get slashed 5% of their tokens and get permanently jailed (called tombstoned). * **downtime**: If a validator is offline for too long and misses 95% of the past 28,800 blocks, they will get slashed 0.02% of their tokens and get jailed. A validator will also get jailed after self-undelegation if the validator's remaining self-delegation amount is smaller than the minimum self-delegation (1024 IP). A jailed validator cannot participate in the consensus and earn any reward. But they can unjail themselves after a cooldown time, which is currently set to 10 minutes. After 10 minutes, it can call the DATA Foundation's staking contract to unjail itself IF their stake is more than minimum stake amount (1024 IP), after which it can participate in the consensus again if it's still within the top 64 validators. A jailed validator can still withdraw all their stakes. Delegators can still stake and unstake from a jailed validator as long as there are remaining stakes on this jailed validator. The jailed validator will only be removed from the chain (hence not able to be staked/unstaked) when there is no remaining stake on it. A fee of 1 IP will be charged for unjailing a validator to prevent spamming. The fee will be burnt by the contract. ## On Behalf Functions Most of the staking-related operations can be done from another wallet on behalf of the validators or delegators. Most of these on-behalf functions are permissionless since they spend tokens from the wallet that calls the on-behalf operations, not from the actual validators or delegators. ## Add Operator If a delegator wants to allow another wallet to unstake or redelegate on their behalf, they must call the staking contract to add that wallet as the operator for their delegator. After that, the operator can unstake and redelegate the delegator's tokens on behalf of the delegator. The same applies to a validator who wants to allow another wallet to unjail on its behalf. A fee of 1 IP will be charged for adding an operator. ## An Additional Data Field Each function will include an additional unformatted `data` input field to accommodate potential future changes. It can avoid changing user interfaces in the future. ## Validator Key Format Validator public keys are secp256k1 keys. The keys have a 33 bytes compressed version and 65 bytes uncompressed version. When interacting with the DATA Foundation's smart contracts, a 33 bytes compressed key is used to identify validators. # Rewards ## Rewards Pool Allocation For every block, a fixed proportion of token inflation will go to the rewards distribution pool, which will be shared among all 64 active validators according to each of their share weights. *These allocated tokens will then be shared among the validator and its delegators in a fashion described by the next section.* The validator share weight is calculated based on the total token staking amount, and whether or not the token staking type is locked or unlocked. As an example, assume that we have 100 tokens allocated for the validator rewards distribution pool, and assume that we only have 3 active validators: * validatorA with 10 locked tokens staked * validatorB with 10 locked tokens staked * validatorC with 10 unlocked tokens staked To calculate how many tokens each validator receives, we first calculate each of their weighted shares, which is defined as the number of staked tokens multiplied by their rewards multiplier (0.5 if staking locked tokens, 1 if staking unlocked tokens). This gives us: * validatorA with 10 \* 0.5 = 5 shares * validatorB with 10 \* 0.5 = 5 shares * validatorC with 10 \* 1 = 10 shares With the weighted and total shares calculated, we can then get the total number of inflationary tokens allocated for each validator: * validatorA with 100 \* (5 / 20) = 25 tokens * validatorB with 100 \* (5 / 20) = 25 tokens * validatorC with 100 \* (10 / 20) = 50 tokens The formula for calculating the total number of tokens allocated for a validator is as follows: where * R\_i is the total inflationary token rewards for validator i * S\_i is the staked tokens for validator i * M\_i is the rewards multiplier (0.5 for locked tokens, 1 for unlocked tokens) * R\_total is the total inflationary tokens allocated for the rewards pool ## Validator And Delegator Rewards Total rewards allocations (*whose calculations are shown in the prior section*) for each validator are shared between the validator itself and all of its delegators: * The validator takes a fixed percentage commission, set by the validator itself * Remaining rewards are distributed among delegators according to their share weights Calculation of delegator rewards is similar to that of validator rewards, where the proportion of tokens received for each delegator out of the remaining validator rewards is calculated based on each delegator's staking multiplier (described in the staking section). As an example, assume a validator has 100 total rewards allocated to it, with a validator commission of 20%, and 3 delegators delegating to it: * delegatorA with 10 tokens staked and a staking multiplier of 1 * delegatorB with 10 tokens staked and a staking multiplier of 1 * delegatorC with 10 tokens staked and a staking multiplier of 2 To calculate how many tokens each delegator receives, we first calculate each of their weighted shares, which is defined as the number of staked tokens multiplied by their staking rewards multiplier. This gives us: * delegatorA with 10 \* 1 = 10 shares * delegatorB with 10 \* 1 = 10 shares * delegatorC with 10 \* 2 = 20 shares With the weighted and total shares calculated, we can then get the total number of inflationary tokens allocated for each delegator, noting that the total number of tokens to be distributed among delegators is give by 100 - (100 \* 0.20) = 80: * delegatorA with 80 \* (10 / 40) = 20 tokens * delegatorB with 80 \* (10 / 40) = 20 tokens * delegatorC with 80 \* (20 / 40) = 40 tokens The formula for calculating the delegator token reward can be found below: where * D\_i is the total inflationary token rewards for delegator i * S\_i is the staked tokens for delegator i * M\_i is the staked rewards multiplier for delegator i * R\_total is the total inflationary tokens allocated for the validator * C is the commission rate for the validator The validator commission is also treated as a reward and will follow the same auto-reward distribution rule described below. The minimal validator commission is set to 5% to avoid a cut-throat competition of lower commission rates among validators. The reward calculation results will be rounded down to gwei. Anything smaller than 1 gwei will be truncated. ## Auto Reward Distribution The reward is accumulated per block and can be distributed per block. However, it will only be automatically distributed to the delegator's account when it is larger than a threshold. The default and also minimal threshold is 8 IP, which means that only if the delegator's reward is more than 8 IP, it will be sent to the delegator's account. The reward distribution will go to a reward distribution queue, which only processes a fixed amount of reward distribution requests per block. The reward distribution per block is 32. The staking reward cannot be manually withdrawn by design. # Community Pool A percentage of the newly minted tokens in every block will go to a community pool contract. The foundation will determine how to use the tokens sent to the pool. The maximum community pool percentage that can be set is 20%. The community pool contract address: **0xcccccc0000000000000000000000000000000002** # Singularity The first 1,580,851 blocks after the genesis is called Singularity, during which everyone can create a validator and stake tokens but the active validator set will only have the genesis validators. There is also no new token emission, hence no reward. Unstake and redelegate are also not supported. The Genesis validator set consists of 8 validators, setup by the foundation and trusted staking institutions. 4 of them support locked tokens and the other 4 support unlocked tokens. Each of them has an initial stake of 0.001 IP. Each of them will set a commission rate. During the Singularity, the genesis valdiators will need to self delegate at least 1024 IP to perform validator operations like editing validator commission rate. After Singularity, the top 64 validator nodes with the highest stakes will be selected to participate in consensus and receive rewards. Slashing/Jail won't happen during Singularity. # Staking Contract The DATA Foundation's staking contract will handle all validators/delegators related operations. It's deployed to address: **0xcccccc0000000000000000000000000000000001** The contract interfaces are defined here: [https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol](https://github.com/piplabs/story/blob/main/contracts/src/protocol/IPTokenStaking.sol) # Whitepaper Source: https://docs.datafdn.org/network/learn/whitepaper # Welcome to the DATA Network Source: https://docs.datafdn.org/network/overview **The DATA Foundation** is a purpose-built Layer 1 blockchain transforming how intellectual property (IP) is registered, managed, and monetized in the digital era. Through its full EVM compatibility and optimized execution layer, the DATA Foundation handles complex IP data structures efficiently, delivering high speed at low cost. ## Getting Started Connect and use the network. Learn more about the architecture. Become a validator. Build applications on the DATA Foundation. # Forum Source: https://docs.datafdn.org/network/participate/forum # Governance Source: https://docs.datafdn.org/network/participate/governance # Participate Source: https://docs.datafdn.org/network/participate/overview There are many ways to participate in the DATA Foundation ecosystem. Choose from the options below to get started. Join the network as a validator and help secure the DATA Network Participate in protocol governance and help shape the future of the DATA Foundation Contribute to DATA Foundation Proposals and protocol development Join discussions and connect with the DATA Foundation community # DATA Foundation Proposals Source: https://docs.datafdn.org/network/participate/sip # DKG Validator Guide Source: https://docs.datafdn.org/network/participate/validators/dkg/dkg-validator-guide Guide to setting up and running story-kernel for DKG committee participation DKG is currently only available on the **Aeneid testnet**. Mainnet support will follow in a future release. ## Overview Starting from the v1.6.0 upgrade, validators can participate in the DKG (Distributed Key Generation) committee. Participation requires running [**story-kernel**](https://github.com/piplabs/story-kernel), a TEE client that executes inside an Intel SGX enclave alongside your validator node. DKG participation is **optional**. You can continue running a validator without joining the DKG committee by keeping `dkg.enable = false` in your `story.toml`. **What to know before joining:** * **SGX hardware required**: your machine must have Intel SGX support * **Self-undelegation is blocked** while you are an active DKG committee member. You cannot unstake your own delegation until the current DKG round ends (\~7 days with production parameters). Other delegators are not affected. * If your kernel goes down, your validator continues producing blocks normally. If the kernel restarts and the node finalizes successfully before the current round ends, it can rejoin that round; otherwise, it rejoins on the next one. ## Hardware Requirements story-kernel runs inside an SGX enclave that requires dedicated Enclave Page Cache (EPC) memory. The Gramine manifest configures a **4 GB enclave** for the Go runtime and DKG cryptographic operations. | Resource | Minimum | Recommended | Notes | | ---------- | ------------------ | ----------- | ----------------------------------------------------------- | | CPU | 2 cores, Intel SGX | 4+ cores | Xeon Platinum 8370C (Ice Lake-SP) or newer | | RAM | 8 GB | 16 GB | Enclave uses 4 GB EPC; host needs the rest for story + geth | | EPC Memory | 4 GB | 8 GB | Must be ≥ enclave\_size (4 GB) | | Disk | 50 GB | 128 GB+ | Kernel data is small (\~100 MB) | **Supported cloud instances (Azure):** | Instance Type | vCPUs | RAM | EPC | Notes | | -------------------- | ----- | ------- | ------ | ---------------------------------------------- | | Standard\_DC1s\_v3 | 1 | 8 GB | 4 GB | Meets minimum EPC but tight on RAM | | Standard\_DC2s\_v3 | 2 | 16 GB | 8 GB | Minimum recommended | | Standard\_DC4s\_v3 | 4 | 32 GB | 16 GB | Recommended | | Standard\_DC8s\_v3 | 8 | 64 GB | 32 GB | High-load validators | | Standard\_DC16s\_v3+ | 16+ | 128+ GB | 64+ GB | Up to DC48s\_v3 (48 vCPUs, 384 GB, 256 GB EPC) | **Bare metal** is also supported: any Intel server with SGX enabled in BIOS. Check EPC size with `dmesg | grep "sgx: EPC section"`. AMD SEV-SNP instances (e.g., Azure DCasv5) and ARM instances are **not supported** at the moment. ## Software Requirements All validators **must** use the exact same versions below to produce identical MRENCLAVE (code commitment) values. | Component | Required Version | Why | | --------- | ---------------- | ------------------------------------------------ | | Ubuntu | 24.04 LTS | Library paths are measured into MRENCLAVE | | Go | 1.24.0 | Different versions produce different binaries | | Gramine | 1.9 | Must be installed via apt, not built from source | *** ## Setup Guide ### Step 1: Verify SGX Support ```bash theme={null} ls /dev/sgx_enclave && echo "SGX available" || echo "SGX NOT available" ``` If `/dev/sgx_enclave` does not exist, SGX is not supported or not enabled in BIOS/cloud settings. ### Step 2: Install Dependencies #### Intel SGX SDK and DCAP ```bash theme={null} sudo mkdir -p /etc/apt/keyrings wget -qO- https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key \ | sudo tee /etc/apt/keyrings/intel-sgx-keyring.asc > /dev/null echo "deb [signed-by=/etc/apt/keyrings/intel-sgx-keyring.asc arch=amd64]\ https://download.01.org/intel-sgx/sgx_repo/ubuntu noble main" \ | sudo tee /etc/apt/sources.list.d/intel-sgx.list sudo apt update sudo apt install -y build-essential cmake libssl-dev \ libsgx-dcap-default-qpl libsgx-enclave-common libsgx-quote-ex ``` #### Configure PCCS Edit `/etc/sgx_default_qcnl.conf`: ```json theme={null} { "pccs_url": "https://global.acccache.azure.net/sgx/certification/v4/", "collateral_service": "https://global.acccache.azure.net/sgx/certification/v4/" } ``` #### Install Gramine 1.9 ```bash theme={null} sudo curl -fsSLo /usr/share/keyrings/gramine-keyring.gpg \ https://packages.gramineproject.io/gramine-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/gramine-keyring.gpg]\ https://packages.gramineproject.io/ noble main" \ | sudo tee /etc/apt/sources.list.d/gramine.list sudo apt update sudo apt install -y gramine=1.9 ``` ### Step 3: Build Story-Kernel ```bash theme={null} git clone https://github.com/piplabs/story-kernel.git cd story-kernel git checkout # use the tag from the upgrade announcement make setup-cbmpc # first time only make build-with-cpp make all-gramine ``` Note the **MRENCLAVE** value from the output: ``` Code Commitment: mr_enclave: <64-char hex> ``` All validators must produce the **same MRENCLAVE**. If yours differs, verify you are on the exact same commit, OS version, Go version, and Gramine version. ### Step 4: Set Up Data Directory ```bash theme={null} sudo mkdir -p /opt/story-kernel sudo chown $USER:$USER /opt/story-kernel ``` ### Step 5: Initialize and Configure #### Initialize ```bash theme={null} gramine-sgx story-kernel init --home /opt/story-kernel ``` #### Configure Edit `/opt/story-kernel/config.toml`: ```toml theme={null} log-level = "info" [grpc] listen_addr = ":50051" [light_client] chain_id = "devnet-1" rpc_addr = "http://localhost:26657" primary_addr = "http://localhost:26657" witness_addrs = ["http://:26657", "http://:26657"] trusted_height = trusted_hash = "" ``` The **trusted block must be within the last 2 weeks**. The light client uses a trust period, so if the trusted block is older, header verification will fail. Get a recent trusted block: ```bash theme={null} curl -s 'http://localhost:26657/block' | python3 -c " import json, sys r = json.load(sys.stdin)['result'] print(f'trusted_height = {r[\"block\"][\"header\"][\"height\"]}') print(f'trusted_hash =\"{r[\"block_id\"][\"hash\"]}\"') " ``` | Field | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `chain_id` | CL chain ID: `devnet-1` for Aeneid | | `witness_addrs` | At least 2 CometBFT RPC endpoints for light client cross-validation. The same address can be repeated (e.g., `["http://x:26657", "http://x:26657"]`), but using 2 different validators is recommended for better security. Use internal/private IPs if on the same network. | ### Step 6: Configure Story Client Apply the following changes to `~/.story/story/config/story.toml`: #### Add `engine-chain-id` (If Not Present) ```toml theme={null} engine-chain-id = 1315 # for Aeneid ``` #### Add DKG Options Section ```toml theme={null} ####################################################################### ### DKG Options ### ####################################################################### [dkg] # Enable defines if the DKG client is enabled or not. enable = true # Comma-separated list of story-kernel (TEE) endpoints. kernel-endpoints = ["127.0.0.1:50051"] # The RPC endpoint of execution layer. engine-rpc-endpoint = "http://127.0.0.1:8545" # TEE enclave type identifier (1 for SGX). enc-type = 1 # TLS configuration for kernel gRPC connections (optional). #kernel-tls-ca-file = "/path/to/ca.crt" #kernel-tls-cert-file = "/path/to/client.crt" #kernel-tls-key-file = "/path/to/client.key" ``` ### Step 7: Start Services **Start order matters.** Start story-kernel AFTER the chain is running, since the kernel needs CometBFT RPC (port 26657) for light client initialization. #### Start Story-Kernel ```bash theme={null} sudo tee /etc/systemd/system/story-kernel.service > /dev/null < ``` #### DKG Registration ```bash theme={null} journalctl -u story --no-pager | grep "DKG_REG_STATUS_VERIFIED" ``` *** ## Troubleshooting ### Kernel Won't Start | Error | Fix | | --------------------------------------- | ------------------------------------------------------------------------------- | | `/dev/sgx_enclave: No such file` | Enable SGX in BIOS or use an SGX-capable VM | | Stuck at "Parsing TOML manifest file" | Normal, wait 1-3 minutes | | `config not found` | Ensure config exists at `/opt/story-kernel/config.toml` | | `height requested is too high` | Delete `/opt/story-kernel/light_client/` and restart with a fresh trusted block | | `at least 2 witness addresses required` | Add at least 2 entries to `witness_addrs` in config | ### DKG Registration Fails | Error | Fix | | -------------------------------------- | --------------------------------------------------------------------------------------- | | `no kernel client for code commitment` | Check story logs for `Connected to kernel`; verify MRENCLAVE matches on-chain whitelist | | `execution reverted` | DCAP attestation failed: check SGX setup and PCCS config | | `Failed to generate the sealed key` | Check kernel logs for details | ### Light Client Issues ```bash theme={null} sudo systemctl stop story-kernel rm -rf /opt/story-kernel/light_client/ # Update trusted_height and trusted_hash in config.toml sudo systemctl start story-kernel ``` *** ## Important Notes ### Self-Undelegation Restriction While your validator is a **finalized member of the active DKG round**, self-undelegation is blocked. This prevents committee members from leaving mid-round, which could compromise threshold cryptography. * Only **self-undelegation** is blocked; other delegators can unstake normally * The restriction lifts when the current round ends * A full DKG round with production parameters takes approximately **7 days** ### Running Kernel on a Separate Machine story-kernel can run on a dedicated SGX machine. Update `kernel-endpoints` in `story.toml`: ```toml theme={null} kernel-endpoints = [":50051"] ``` The SGX machine needs network access to your CometBFT RPC (port 26657). *** ## Additional Information ### Build Environment | Component | Version | | ------------------ | ------------ | | Ubuntu | 24.04.4 LTS | | Go | 1.24.0 | | Gramine | 1.9 (apt) | | gramine-ratls-dcap | 1.9 | | story-kernel | v0.1.0 (tag) | | cb-mpc | v0.0.1-alpha | ### Enclave Measurement The measurements below correspond to `story-kernel` release `v0.1.0`. They can change when `story-kernel` or its measured build inputs are upgraded. ``` MRENCLAVE: 6b2fb25e0084ad6ecbf6cfcefe09e2fa0fca2b84092f72c01f8fe98e9d7db5cd Binary hash: 320a744e87426e36de63ebc8c7ff3af23e00ac7b29ad6093acdafd0bb029ca36 ``` *** ## FAQ **Do I need SGX to run a validator?** No. SGX is only needed for DKG committee participation. Set `dkg.enable = false` to run without it. **What happens if my kernel goes down?** Your validator continues producing blocks. If the kernel restarts and the node finalizes successfully before the current DKG round ends, it can rejoin that round; otherwise, it rejoins on the next one. **What is MRENCLAVE?** A cryptographic hash of the SGX enclave contents. All validators must produce the same value to participate in the same DKG committee. **Can I opt out after joining?** Yes. Set `dkg.enable = false` and restart story. You stop participating after the current round ends. # Kernel Upgrade Source: https://docs.datafdn.org/network/participate/validators/dkg/kernel-upgrade Guide to upgrading the story-kernel binary on DKG committee validators ## Kernel Upgrade Workflow (Aeneid) Upgrade the story-kernel binary on Aeneid testnet DKG committee validators. This uses the DKG on-chain upgrade mechanism: whitelist new MRENCLAVE, schedule upgrade, dual-kernel resharing, then cutover. *** ## Prerequisites * It is recommended to start the upgrade when current DKG round is in **Active** stage * New story-kernel binary built on all validator machines (must produce identical MRENCLAVE) * Timelock/owner access to DKG contract for `whitelistEnclaveType` and `scheduleUpgrade` * SGXValidationHook proxy address known for the new kernel client ## Phase 1: Build New Kernel Build the new story-kernel binary **on each validator machine** (never SCP binaries, since MRENCLAVE must match). ```bash theme={null} cd ~/story-kernel git pull origin release/0.1 make clean && make build-with-cpp && make all-gramine NEW_MRENCLAVE=$(cat story-kernel.manifest.sgx.d/mrenclave.txt) echo "New MRENCLAVE: $NEW_MRENCLAVE" ``` Verify all validators produce the **same** `NEW_MRENCLAVE` value before proceeding. ## Phase 2: Start Dual Kernels The new kernel runs alongside the old kernel on a separate port. DATA Foundation CL identifies each kernel by its `code_commitment` (MRENCLAVE). ```bash theme={null} # Old kernel: already running on :50051 # New kernel: start on :50052 with separate home dir # Verify both are listening sudo lsof -i :50051 | grep LISTEN # old sudo lsof -i :50052 | grep LISTEN # new ``` The new kernel needs its own: * Home directory (separate light client state) * Gramine manifest with different `listen_addr` (`:50052`) ## Phase 3: Update DATA Foundation Config + Restart Add the new kernel endpoint to `story.toml`: ```toml theme={null} kernel-endpoints = ["127.0.0.1:50051", "127.0.0.1:50052"] ``` Restart story: ```bash theme={null} sudo systemctl restart story ``` Verify **both** kernels connected: ```bash theme={null} journalctl -u story --since '1 minute ago' | grep "Connected to kernel" # Must see TWO entries with different code_commitment values # Verify: connected_clients=2 ``` ## Phase 4: Whitelist + Schedule Upgrade On-Chain Wait for the current DKG round to be in **Active** stage, then: ```bash theme={null} DKG="0xCcCcCC0000000000000000000000000000000004" ENCLAVE_TYPE="0x0000000000000000000000000000000000000000000000000000000000000001" SGX_HOOK="" # 1. Whitelist new MRENCLAVE cast send $DKG 'whitelistEnclaveType(bytes32,(bytes32,address),bool)' \ $ENCLAVE_TYPE "(0x$NEW_MRENCLAVE,$SGX_HOOK)" true \ --rpc-url $RPC --private-key $KEY --legacy --gas-price 30000000000 # 2. Schedule upgrade (activation = current height + buffer) CURRENT=$(cast block-number --rpc-url $RPC) ACTIVATION=$((CURRENT + 50)) cast send $DKG 'scheduleUpgrade(uint256,string)' $ACTIVATION "v" \ --rpc-url $RPC --private-key $KEY --legacy --gas-price 30000000000 ``` On Aeneid, DKG contract ops go through Timelock (minDelay=600s). Schedule the Timelock tx, wait 10 min, then execute. ## Phase 5: Wait for Upgrade Resharing ```bash theme={null} # Monitor activation while true; do HEIGHT=$(curl -s localhost:26657/status | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['sync_info']['latest_block_height'])") echo "Height: $HEIGHT / $ACTIVATION" if [ "$HEIGHT" -ge "$ACTIVATION" ]; then break; fi sleep 5 done # Verify upgrade resharing round started journalctl -u story --since '5 minutes ago' | grep 'is_upgrade.*true' # Wait for completion timeout 3600 bash -c "while ! journalctl -u story --since '1 hour ago' | grep -q 'DKG finalization phase complete'; do sleep 15; done" ``` ## Phase 6: Cutover to New Kernel After upgrade resharing completes successfully: ```bash theme={null} # 1. Stop old kernel sudo systemctl stop story-kernel # old on :50051 # 2. Update story.toml to only new kernel sed -i 's|kernel-endpoints = \["127.0.0.1:50051", "127.0.0.1:50052"\]|kernel-endpoints = ["127.0.0.1:50052"]|' \ ~/.story/story/config/story.toml # 3. Restart story sudo systemctl restart story # 4. Verify journalctl -u story --since '1 minute ago' | grep "Connected to kernel" # Should see 1 entry with the NEW code_commitment ``` *** ## Verification Checklist * [ ] All validators built identical `NEW_MRENCLAVE` * [ ] Both kernels connected on all validators (`connected_clients=2`) * [ ] `whitelistEnclaveType` tx confirmed (new MRENCLAVE on enclave type 1) * [ ] `scheduleUpgrade` tx confirmed with target activation height * [ ] Upgrade resharing round initiated with `is_upgrade=true` * [ ] Old kernel generates deals, new kernel processes responses * [ ] `DKG finalization phase complete` on all committee members * [ ] Old kernel stopped, config updated to new kernel only * [ ] New DKG round proceeds normally on new kernel ## Troubleshooting | Symptom | Cause | Fix | | ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------- | | `connected_clients=1` after restart | New kernel not running or port mismatch | Verify `lsof -i :50052`, check Gramine manifest | | "no new kernel client found for upgrade" | DATA Foundation not connected to new kernel | Ensure `kernel-endpoints` has both ports, restart story | | Upgrade round doesn't start at activation height | Not in Active stage when scheduled | Reschedule during next Active stage | | Finalization fails | Insufficient committee members upgraded | Ensure all validators have dual kernels running | # Full Node Source: https://docs.datafdn.org/network/participate/validators/node-setup-mainnet Guide to setting up a DATA Foundation node for mainnet This section will guide you through how to setup a DATA Foundation node for mainnet. The DATA Foundation draws inspiration from ETH PoS in decoupling execution and consensus clients. The execution client `story-geth` relays EVM blocks into the `story` consensus client via Engine API, using an ABCI++ adapter to make EVM state compatible with that of CometBFT. With this architecture, consensus efficiency is no longer bottlenecked by execution transaction throughput. The `story` and `geth` binaries, which make up the clients required for running DATA Foundation nodes, are available from our latest `release` pages: | Network | story-geth | story | | ------- | ----------------- | ---------------- | | Mainnet | v1.2.0 (Yasunari) | v1.4.2 (Terence) | | Aeneid | v1.2.0 (Yasunari) | v1.4.2 (Terence) | * **`story-geth`execution client:** * Release Link: [**Click here**](https://github.com/piplabs/story-geth/releases) * Latest Stable Binary (v1.2.0): [**Click here**](https://github.com/piplabs/story-geth/releases/tag/v1.2.0) * **`story-geth`execution client:** (For Aeneid testnet) * Release Link: [**Click here**](https://github.com/piplabs/story-geth/releases) * Latest Stable Binary (v1.2.0): [**Click here**](https://github.com/piplabs/story-geth/releases/tag/v1.2.0) * **`story`consensus client:** * Releases link: [**Click here**](https://github.com/piplabs/story/releases) * Latest Stable Binary (v1.4.2): [**Click here**](https://github.com/piplabs/story/releases/tag/v1.4.2) * **`story`consensus client:** (For Aeneid testnet) * Releases link: [**Click here**](https://github.com/piplabs/story/releases) * Latest Stable Binary (v1.4.2): [**Click here**](https://github.com/piplabs/story/releases/tag/v1.4.2) # DATA Foundation Node Installation Guide ## Pre-Installation Checklist * [ ] Verify system meets hardware requirements * [ ] Operating system: Ubuntu 22.04 LTS * [ ] Required ports are available * [ ] Sufficient disk space available * [ ] Root or sudo access ## Quick Reference * Installation time: \~30 minutes * Network: DATA Network or Aeneid testnet * Required versions: * Check Latest Release ## 1. System Preparation ### 1.1 System Requirements For optimal performance and reliability, we recommend running your node on either: * A Virtual Private Server (VPS) * A dedicated Linux-based machine ### System Specs | Hardware | Minimal Requirement | | --------- | ------------------- | | CPU | Dedicated 8 Cores | | RAM | 32 GB | | Disk | 500 GB NVMe Drive | | Bandwidth | 25 MBit/s | ### 1.2 Required Ports *Ensure all ports needed for your node functionality are needed, described below* * `story-geth` * 8545 * Required if you want your node to interface via JSON-RPC API over HTTP * 8546 * Required for websockets interaction * 30303 (TCP + API) * MUST be open for p2p communication * `story` * 26656 * MUST be open for consensus p2p communication * 26657 * Required if you want your node interfacing for Tendermint RPC * 26660 * Needed if you want to expose prometheus metrics ### 1.3 Install Dependencies ```bash theme={null} # Update system sudo apt update && sudo apt-get update # Install required packages sudo apt install -y \ curl \ git \ make \ jq \ build-essential \ gcc \ unzip \ wget \ lz4 \ aria2 \ gh ``` ### 1.4 Install Go For Odyssey, we need to install Go 1.22.0 ```bash theme={null} # Download and install Go 1.22.0 cd $HOME # Set Go version GO_VERSION="1.22.0" # Download Go binary wget "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" # Remove existing Go installation and extract new version sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" # Clean up downloaded archive rm "go${GO_VERSION}.linux-amd64.tar.gz" # Add Go to PATH echo "export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin" >> ~/.bash_profile source ~/.bash_profile # Verify installation go version ``` ## 2. DATA Foundation Node Installation ### 2.1 Install Story-Geth 1. Download and setup binary ```bash theme={null} cd $HOME wget https://github.com/piplabs/story-geth/releases/download/v1.2.0/geth-linux-amd64 sudo mv ./geth-linux-amd64 story-geth sudo chmod +x story-geth sudo mv ./story-geth $HOME/go/bin/ source $HOME/.bashrc # Verify installation story-geth version ``` You will see the version of the geth binary. ``` Geth version: 1.2.0-stable ``` (Mac OS X only) The OS X binaries have yet to be signed by our build process, so you may need to unquarantine them manually: ```bash theme={null} sudo xattr -rd com.apple.quarantine ./geth ``` 2. Configure and start service ```bash theme={null} # Setup systemd service sudo tee /etc/systemd/system/story-geth.service > /dev/null < ```bash theme={null} # Setup systemd service sudo tee /etc/systemd/system/story-geth.service > /dev/null < ### 2.2 Install DATA Foundation Consensus Client #### Cosmovisor Installation For updating the story client, we recommend using Cosmovisor. 1. Install Cosmovisor ```bash theme={null} go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.6.0 cosmovisor version ``` 2. Configure Cosmovisor ```bash theme={null} # Set daemon configuration export DAEMON_NAME=story export DAEMON_HOME=$HOME/.story/story export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup sudo mkdir -p \ $DAEMON_HOME/cosmovisor/backup \ $DAEMON_HOME/data # Persist configuration echo "export DAEMON_NAME=story" >> $HOME/.bash_profile echo "export DAEMON_HOME=$HOME/.story/story" >> $HOME/.bash_profile echo "export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup" >> $HOME/.bash_profile echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=false" >> $HOME/.bash_profile ``` #### Install Story Client ```bash theme={null} cd $HOME wget https://github.com/piplabs/story/releases/download/v1.4.2/story-linux-amd64 sudo mv story-linux-amd64 story sudo chmod +x story sudo mv ./story $HOME/go/bin/ source $HOME/.bashrc story version ``` > You should expect to see version 1.4.2-stable (Mac OS X Only) The OS X binaries have yet to be signed by our build process, so you may need to unquarantine them manually: ```bash theme={null} sudo xattr -rd com.apple.quarantine ./story ``` #### Init DATA Foundation With Cosmovisor ```bash theme={null} cosmovisor init ./story cosmovisor run init --network story --moniker ${moniker_name} cosmovisor version ``` ```bash theme={null} cosmovisor init ./story cosmovisor run init --network aeneid --moniker ${moniker_name} cosmovisor version ``` #### Custom Configuration To override your own node settings, you can do the following: * `${DATAFDN_DATA_ROOT}/config/config.toml` can be modified to change network and consensus settings * `${DATAFDN_DATA_ROOT}/config/story.toml` to update various client configs * `${DATAFDN_DATA_ROOT}/priv_validator_key.json` is a sensitive file containing your validator key, but may be replaced with your own #### Custom Automation Below we list a sample `Systemd` configuration you may use on Linux The DATA Foundation API endpoint (`--api-address`) can be modified as needed depending on your environment or use case. ```bash theme={null} # story sudo tee /etc/systemd/system/story.service > /dev/null < ```bash theme={null} # story sudo tee /etc/systemd/system/story.service > /dev/null < ```bash theme={null} # story sudo tee /etc/systemd/system/story.service > /dev/null < #### Start the Service ```bash theme={null} sudo systemctl daemon-reload sudo systemctl enable story sudo systemctl start story # Monitor logs journalctl -u cosmovisor -f -o cat ``` #### Debugging If you would like to check the status of `story` while it is running, it is helpful to query its internal JSONRPC/HTTP endpoint. Here are a few helpful commands to run: * `curl localhost:26657/net_info | jq '.result.peers[].node_info.moniker'` * This will give you a list of consesus peers the node is sync'd with by moniker * `curl localhost:26657/health` * This will let you know if the node is healthy - `{}` indicates it is ## 3. Verify Installation ### 3.1 Check Geth Status ```bash theme={null} # Check sync status curl -X POST -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ http://localhost:8545 ``` ### 3.2 Check Consensus Client ```bash theme={null} # Check node status curl localhost:26657/status # Check peer connections curl localhost:26657/net_info | jq '.result.peers[].node_info.moniker' ``` ## Clean Status If you ever run into issues and would like to try joining the network from a cleared state, run the following: ### Geth ```bash theme={null} rm -rf ${GETH_DATA_ROOT} && ./geth --story --syncmode full ``` Mac OS X: `rm -rf ~/Library/Story/geth/* && ./geth --story --syncmode full` Linux: `rm -rf ~/.story/geth/* && ./geth --story --syncmode full` ```bash theme={null} rm -rf ${GETH_DATA_ROOT} && ./geth --aeneid --syncmode full ``` Mac OS X: `rm -rf ~/Library/Story/geth/* && ./geth --aeneid --syncmode full` Linux: `rm -rf ~/.story/geth/* && ./geth --aeneid --syncmode full` ### DATA Foundation ```bash theme={null} rm -rf ${DATAFDN_DATA_ROOT} && ./story init --network story && ./story run ``` Mac OS X: `rm -rf ~/Library/Story/story/* && ./story init --network story && ./story run` Linux: `rm -rf ~/.story/story/* && ./story init --network story && ./story run` ```bash theme={null} rm -rf ${DATAFDN_DATA_ROOT} && ./story init --network aeneid && ./story run ``` Mac OS X: `rm -rf ~/Library/Story/story/* && ./story init --network aeneid && ./story run` Linux: `rm -rf ~/.story/story/* && ./story init --network aeneid && ./story run` # Node Upgrade Source: https://docs.datafdn.org/network/participate/validators/node-upgrade Guide to upgrading your DATA Foundation node clients There are three types of upgrades 1. Upgrade the story geth client 2. Upgrade the story client manually 3. Schedule the upgrade with Cosmovisor ### Upgrade the Story Geth Client ```bash theme={null} # Stop the services sudo systemctl stop story sudo systemctl stop story-geth # Download the new binary wget ${DATAFDN_GETH_BINARY_URL} sudo mv ./geth-linux-amd64 story-geth sudo chmod +x story-geth sudo mv ./story-geth $HOME/go/bin/story-geth source $HOME/.bashrc # Restart the service sudo systemctl start story-geth sudo systemctl start story ``` ### Upgrade the Story Client Manually ```bash theme={null} # Stop the service sudo systemctl stop story # Download the new binary wget ${DATAFDN_BINARY_URL} sudo mv story-linux-amd64 story sudo chmod +x story sudo mv ./story $HOME/go/bin/story # Schedule the update sudo systemctl start story ``` ### Schedule the Upgrade With Cosmovisor The following steps outline how to schedule an upgrade using Cosmovisor: 1. Create the upgrade directory and download the new binary ```bash theme={null} # Download the new binary wget ${DATAFDN_BINARY_URL} # Schedule the upgrade source $HOME/.bash_profile cosmovisor add-upgrade ${UPGRADE_NAME} ${UPGRADE_PATH} \ --force \ --upgrade-height ${UPGRADE_HEIGHT} ``` 2. Verify the upgrade configuration ```bash theme={null} # Check the upgrade info cat $HOME/.story/story/data/upgrade-info.json ``` The upgrade-info.json should show: ```json theme={null} { "name": "v1.0.0", "time": "2025-02-05T12:00:00Z", "height": 858000 } ``` 3. Monitor the upgrade ```bash theme={null} # Watch the node logs for the upgrade journalctl -u story -f -o cat ``` Note: Cosmovisor will automatically handle the binary switch once the specified block height is reached. Before the upgrade, confirm that your node is fully synced and has enough disk space available. ### Use Cosmovisor While Running DATA Foundation Node This guide is for people who are running story without using cosmovisor, but still want to use cosmovisor to schedule the upgrade. 1. Install Cosmovisor ```bash theme={null} # Install Cosmovisor go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@1.6.0 cosmovisor version ``` You will see the version of cosmovisor. ``` cosmovisor version: v1.6.0 Error: failed to run version command: DAEMON_NAME is not set DAEMON_HOME is not set DAEMON_DATA_BACKUP_DIR must not be empty ``` 2. Set the environment variables ```bash theme={null} # Set daemon configuration export DAEMON_NAME=story export DAEMON_HOME=$HOME/.story/story export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup sudo mkdir -p $DAEMON_HOME/cosmovisor/backup $DAEMON_HOME/data # Persist configuration echo "export DAEMON_NAME=story" >> $HOME/.bash_profile echo "export DAEMON_HOME=$HOME/.story/story" >> $HOME/.bash_profile echo "export DAEMON_DATA_BACKUP_DIR=${DAEMON_HOME}/cosmovisor/backup" >> $HOME/.bash_profile echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=false" >> $HOME/.bash_profile ``` If you have any permission issues, you can run the following command to fix it. ```bash theme={null} sudo chown -R $USER:$USER $HOME/.story ``` 3. Setup the cosmovisor ```bash theme={null} # Create the cosmovisor directory mkdir -p $HOME/.story/cosmovisor/genesis/bin # Copy the new binary to the cosmovisor directory cp $HOME/go/bin/story $HOME/.story/cosmovisor/genesis/bin/ ``` 4. Add cosmovisor to the systemd service ```bash theme={null} sudo tee /etc/systemd/system/cosmovisor.service > /dev/null < Download the latest Story Geth client releases Download the latest DATA Foundation consensus client releases # Overview This section will guide you through how you can run your own validator. Validator operations may be done via the `story` consensus client. The below operations do not require running a node! However, if you would like to participate in staking rewards, you must run a validator node. Before proceeding, it is important to familiarize yourself with the difference between a delegator and a validator: * A **validator** is a full node that participates in consensus whose signed key resides in the `priv_validator_key.json` file under your `story` data directory. To print out your validator key details you may refer to the [validator key export section](/network/become-a-validator#validator-key-export) * A **delegator** refers to an account operator that holds `IP` and wishes to participate in consensus rewards but without needing to run a validator themselves. In the same folder as where your `story` binary resides, add a `.env` file with a `PRIVATE_KEY` whose account has `IP` funded. **We recommend using your delegator account for all below operations.** You may also issue transactions as the validator itself. To get the EVM private key corresponding to your validator, please refer to the [Validator Key Export](#validator-key-export) section. From **Story v1.2.0**, user must use `.env` for all operations. The `.env` file should look like the following *(make sure not to add a 0x prefix):* ```bash theme={null} # ~/.env PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` With this, you are all set to perform different validator operations! Below, we will guide you through all of those supported via the CLI: ## Validator Key Export By default, when you run `./story init` a validator key is created for you. To view your validator key, run the following command: ```bash theme={null} ./story validator export [flags] ``` This will print out your validator public key file in compressed and uncompressed formats. By default, we use the hex-encoded compressed key for public identification. ```text theme={null} Compressed Public Key (hex): 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 Compressed Public Key (base64): A73HuJQLq+kibVLX+imaH689ZKgvgJiJJWyPFGlYpjmE Uncompressed Public Key (hex): 04bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a6398496b9e2af0a3a1d199c3cc1d09ee899336a530c185df6b46a9735b25e79a493af EVM Address: 0x9EacBe2C3B1eb0a9FC14106d97bd3A1F89efdDCc Validator Address: storyvaloper1p470h0jtph4n5hztallp8vznq8ehylsw9vpddx Delegator Address: story1p470h0jtph4n5hztallp8vznq8ehylswtr4vxd ``` **Available Flags:** * `--export-evm-key`: (bool) Exports the derived EVM private key of your validator into the default data config directory * `--export-evm-key-path`: (string) Specifies a different download location for the derived EVM private key of your validator * `--keyfile`: (string) Path to the Tendermint key file (default "/home/ubuntu/.story/story/config/priv\_validator\_key.json") If you would like to issue transactions as your validator, and not as a delegator, you may export the key to your `.env` file and ensure it has IP sent to it, e.g. via `./story validator export --export-evm-key --evm-key-path .env` ## Validator Creation To create a new validator, run the following command: ```bash Locked token node theme={null} ./story validator create --stake ${AMOUNT_TO_STAKE_IN_WEI} \ --moniker ${VALIDATOR_NAME} \ --rpc ${rpc} \ --chain-id ${chain_id} \ --commission-rate ${rate} \ --unlocked=false ``` ```Text Unlocked token node theme={null} ./story validator create --stake ${AMOUNT_TO_STAKE_IN_WEI} \ --moniker ${VALIDATOR_NAME} \ --rpc ${rpc} \ --chain-id ${chain_id} \ --commission-rate ${rate} \ ``` This will create the validator corresponding to your validator key saved in `priv_validator_key.json`, providing the validator with `{$AMOUNT_TO_STAKE_IN_WEI}` IP to self-stake. To participate in consensus, at least 1024 IP must be staked (equivalent to `1024000000000000000000 wei`)! Below is a list of optional flags to further customize your validator setup: **Available Flags:** * `--stake`: Sets the amount the validator will self-delegate in wei (default is `1024000000000000000000` wei). * `--moniker`: Defines a custom name for the validator, visible to users on the network. * `--chain-id`: Specifies the Chain ID for the transaction. By default, this is set to `1516`. * `--commission-rate`: Sets the validator's commission rate in bips (1% = 100 bips). For instance, `1000` represents a 10% commission (default is `1000`). * `--explorer`: Specifies the URL of the blockchain explorer (default: [https://www.datanetscan.io](https://www.datanetscan.io)). * `--keyfile`: Points to the path of the Tendermint key file (default: `$HOME/.story/story/config/priv_validator_key.json`). * `--max-commission-change-rate`: Sets the maximum rate at which the validator's commission can change, in bips. For example, `100` represents a maximum change of 1% (default is `1000`). * `--max-commission-rate`: Defines the maximum commission rate the validator can charge, in bips. For instance, `5000` allows a 50% maximum rate (default is `5000`). * `--rpc`: Sets the RPC URL to connect to the network (default: [https://mainnet.datarpc.io](https://mainnet.datarpc.io)). * `--unlocked`: Determines if unlocked token staking is supported (`true` for unlocked staking, `false` for locked staking). By default, this is set to `true`. * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Creation Command Use ```bash theme={null} story validator create --stake 1024000000000000000000 --moniker "timtimtim" --commission-rate 700 --validator-pubkey "" # if you dont have a .env --rpc "https://mainnet.datarpc.io" --chain-id 1514 ``` ### Verifying Your Validator Once created, please use the `Explorer URL` to confirm the transaction. If successful, you should see your validator pub key (*found in your`priv_validator_key.json` file)* listed as part of the following endpoint: ```bash theme={null} curl https://testnet.datarpc.io/validators | jq . ``` Congratulations, you are now one of the DATA Foundation’s very first IP validators! ## Validator Staking To stake to an existing validator, run the following command: ```bash theme={null} ./story validator stake \ --validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \ --stake ${AMOUNT_TO_STAKE_IN_WEI} --staking-period ${STAKING_PERIOD} ``` * Note that your own `${VALIDATOR_PUB_KEY_IN_HEX}`may be found by running the `./story validator export` command as the `Compressed Public Key (hex)`. * You must stake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid Once staked, you may use the `Explorer URL` to confirm the transaction. As mentioned earlier, you may use our [validator endpoint](https://mainnet.datarpc.io/validators) to confirm the new voting power of the validator. **Available Flags:** * `--validator-pubkey`: (string) The public key of the validator to stake to * `--stake`: (string) The amount of IP to stake in wei * `--chain-id`: (int) Chain ID to use for the transaction (default: 1514) * `--explorer`: (string) URL of the blockchain explorer * `--help`, `-h`: Display help information for stake command * `--rpc`: (string) RPC URL to connect to the network * `--staking-period`: (stakingPeriod) Staking period (options: "flexible", "short", "medium", "long") (default: flexible) * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Staking Command Use ```bash theme={null} ./story validator stake \ --validator-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \ --stake 1024000000000000000000 --staking-period "short" ``` ## Validator Unstaking To unstake from a validator, run the following command: ```bash theme={null} ./story validator unstake \ --validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \ --unstake ${AMOUNT_TO_UNSTAKE_IN_WEI} \ --delegation-id ${ID_STAKING_PERIOD} ``` This will unstake `${AMOUNT_TO_UNSTAKE_IN_WEI}` IP from the selected validator. You must unstake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid. Like in the staking operation, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.datarpc.io/validators) to double-check the newly reduced voting power of the validator. **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction (default: 1514) * `--delegation-id`: (uint32) The delegation ID (0 for flexible staking) * `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--help`, `-h`: Help for unstake command * `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") * `--unstake`: (string) Amount to unstake in wei * `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Unstaking Command Use ```bash theme={null} ./story validator unstake \ --validator-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \ --unstake 1024000000000000000000 \ --delegation-id 1 ``` ## Validator Stake-on-Behalf To stake on behalf of another delegator, run the following command: ```bash theme={null} ./story validator stake-on-behalf \ --delegator-address ${DELEGATOR_EVM} \ --validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \ --stake ${AMOUNT_TO_STAKE_IN_WEI} \ --staking-period ${STAKING_PERIOD} \ --rpc --chain-id ``` This will stake `${AMOUNT_TO_STAKE_IN_WEI}` IP to the validator on behalf of the provided delegator. You must stake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid. Like in the other staking operations, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.datarpc.io/validators) to double-check the increased voting power of the validator. **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction (default: 1514) * `--delegator-address`: (string) Delegator's EVM address * `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--help`, `-h`: Help for stake-on-behalf command * `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") * `--stake`: (string) Amount for the validator to self-delegate in wei * `--staking-period`: (stakingPeriod) Staking period (options: "flexible", "short", "medium", "long") (default: flexible) * `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Stake-on-Behalf Command Use ```bash theme={null} ./story validator stake-on-behalf \ --delegator-address 0xF84ce113FCEe12d78Eb41590c273498157c91520 \ --validator-pubkey 03e42b4d778cda2f3612c85161ba7c0aad1550a872f3279d99e028a1dfa7854930 \ --stake 1024000000000000000000 \ --staking-period "short" \ --rpc \ --chain-id ``` ## Validator Unstake-on-Behalf You may also unstake on behalf of delegators. However, to do so, you must be registered as an authorized operator for that delegator. To unstake on behalf of another delegator as an operator, run the following command: ```bash theme={null} ./story validator unstake-on-behalf \ --delegator-address ${DELEGATOR_PUB_KEY_IN_HEX} \ --validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \ --unstake ${AMOUNT_TO_STAKE_IN_WEI} \ --rpc \ --chain-id ``` This will unstake `${AMOUNT_TO_STAKE_IN_WEI}` IP from the validator on behalf of the delegator, assuming you are a registered operator for that delegator. You must unstake at least 1024 IP worth (`*1024000000000000000000 wei`) for the transaction to be valid. Like in the other staking operations, please use the `Explorer URL` to confirm the transaction and our [validator endpoint](https://mainnet.datarpc.io/validators) to double-check the decreased voting power of the validator. **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction (default: 1514) * `--delegator-address`: (string) Delegator's EVM address * `--explorer`: (string) URL of the blockchain explorer (default: "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--help`, `-h`: Help for unstake-on-behalf command * `--rpc`: (string) RPC URL to connect to the network (default: "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") * `--unstake`: (string) Amount to unstake in wei * `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Unstake-on-Behalf Command Use ```bash theme={null} ./story validator unstake-on-behalf \ --delegator-address 0xF84ce113FCEe12d78Eb41590c273498157c91520 \ --validator-pubkey 03e42b4d778cda2f3612c85161ba7c0aad1550a872f3279d99e028a1dfa7854930 \ --unstake 1024000000000000000000 \ --rpc \ --chain-id ``` ## Validator Unjail In case a validator becomes jailed, for example if it experiences substantial downtime, you may use the following command to unjail the targeted validator: ```Text Bash theme={null} ./story validator unjail \ --rpc --chain-id ``` Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid. **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction * `--explorer`: (string) URL of the blockchain explorer * `--rpc`: (string) RPC URL to connect to the network * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Unjail Command Use ```bash theme={null} ./story validator unjail \ --rpc \ --chain-id ``` ## Validator Unjail-on-Behalf If you are an authorized operator, you may unjail a validator on their behalf using the following command: ```bash theme={null} ./story validator unjail-on-behalf \ --validator-pubkey ${VALIDATOR_PUB_KEY_IN_HEX} \ --rpc \ --chain-id ``` **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction * `--explorer`: (string) URL of the blockchain explorer * `--rpc`: (string) RPC URL to connect to the network * `--validator-pubkey`: (string) Validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Unjail-on-Behalf Command Use ```bash theme={null} ./story validator unjail-on-behalf \ --rpc \ --chain-id ``` ## Validator Rollback The snapshot must be taken from a node that has already been upgraded. That means the snapshot node has to do the rewind. Recovery Options for Nodes Stuck in Long Rewind After Upgrade: Wait for resync: Let the node catch up naturally. This may take time depending on the rewind depth and network bandwidth. Apply a snapshot: Use a snapshot from a node that has already been upgraded to Pectra. You can generate your own or obtain one from trusted community providers. ```bash theme={null} ./story validator rollback \ --rpc \ --chain-id ``` \*\* Avaliable Flages:\*\* * `--api-address`: (string) The API server address to listen on (default "\***\*\*\*\***:1317") * `--api-enable`: (bool) Define if the API server should be enabled * `--api-enable-unsafe-cors`: (bool) Enable unsafe CORS for API server * `--api-idle-timeout`: (uint) Define the API server idle timeout (in seconds) (default 10) * `--api-max-header-bytes`: (uint) Define the API server max header (in bytes) (default 8192) * `--api-read-header-timeout`: (uint) Define the API server read header timeout (in seconds) (default 10) * `--api-read-timeout`: (uint) Define the API server read timeout (in seconds) (default 10) * `--api-write-timeout`: (uint) Define the API server write timeout (in seconds) (default 10) * `--app-db-backend`: (string) The type of database for application and snapshots databases (default "goleveldb") * `--engine-endpoint`: (string) An EVM execution client Engine API http endpoint (default "[http://localhost:8551](http://localhost:8551)") * `--engine-jwt-file`: (string) The path to the Engine API JWT file * `--evm-build-delay`: (duration) Minimum delay between triggering and fetching a EVM payload build (default 600ms) * `--evm-build-optimistic`: (bool) Enables optimistic building of EVM payloads on previous block finalize (default true) * `--help`: (bool) help for rollback * `--home`: (string) The application home directory containing config and data (default "/home/timothyshen/.story/story") * `--log-color`: (string) Log color (only applicable to console format); auto, force, disable (default "auto") * `--log-format`: (string) Log format; console, json (default "console") * `--log-level`: (string) Log level; debug, info, warn, error (default "info") * `--min-retain-blocks`: (uint) Minimum block height offset during ABCI commit to prune CometBFT blocks * `--network`: (string) DATA Network to participate in: story, odyssey, aeneid or local * `--number`: (uint) number of blocks to rollback (default 1) * `--pruning`: (string) Pruning strategy (default|nothing|everything) (default "nothing") * `--snapshot-interval`: (uint) State sync snapshot interval (default 1000) * `--snapshot-keep-recent`: (uint) State sync snapshot to keep (default 2) * `--tracing-endpoint`: (string) Tracing OTLP endpoint * `--tracing-headers`: (string) Tracing OTLP headers ### Example Rollback Command Use ```bash theme={null} ./story validator rollback \ --rpc \ --chain-id ``` ## Validator Redelegate To redelegate from one validator to another, run the following command: ```bash theme={null} ./story validator redelegate \ --validator-src-pubkey ${VALIDATOR_SRC_PUB_KEY_IN_HEX} \ --validator-dst-pubkey ${VALIDATOR_DST_PUB_KEY_IN_HEX} \ --redelegate ${AMOUNT_TO_REDELEGATE_IN_WEI} --rpc \ --chain-id ``` **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction (default 1514) * `--delegation-id`: (uint32) The delegation ID (0 for flexible staking) * `--explorer`: (string) URL of the blockchain explorer (default "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--help`, `-h`: Help for redelegate command * `--redelegate`: (string) Amount to redelegate in wei * `--rpc`: (string) RPC URL to connect to the network (default "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") * `--validator-dst-pubkey`: (string) Dst validator's hex-encoded compressed 33-byte secp256k1 public key * `--validator-src-pubkey`: (string) Src validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ≈ ```bash theme={null} ./story validator redelegate \ --validator-src-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \ --validator-dst-pubkey 02ed58a9319aba87f60fe08e87bc31658dda6bfd7931686790a2ff803846d4e59c \ --redelegate 1024000000000000000000 \ --rpc \ --chain-id ``` ## Validator Redelegate-on-Behalf If you are an authorized operator, you may redelegate from one validator to another on behalf of a delegator using the following command: ```bash theme={null} ./story validator redelegate-on-behalf \ --delegator-address ${DELEGATOR_EVM_ADDRESS} \ --validator-src-pubkey ${VALIDATOR_SRC_PUB_KEY_IN_HEX} \ --validator-dst-pubkey ${VALIDATOR_DST_PUB_KEY_IN_HEX} \ --redelegate ${AMOUNT_TO_REDELEGATE_IN_WEI} \ --rpc \ --chain-id ``` **Available Flags:** * `--chain-id`: (int) Chain ID to use for the transaction (default 1514) * `--delegation-id`: (uint32) The delegation ID (0 for flexible staking) * `--delegator-address`: (string) Delegator's EVM address * `--explorer`: (string) URL of the blockchain explorer (default "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--help`, `-h`: Help for redelegate-on-behalf command * `--redelegate`: (string) Amount to redelegate in wei * `--rpc`: (string) RPC URL to connect to the network (default "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") * `--validator-dst-pubkey`: (string) Dst validator's hex-encoded compressed 33-byte secp256k1 public key * `--validator-src-pubkey`: (string) Src validator's hex-encoded compressed 33-byte secp256k1 public key * `--story-api`: Prevent potential fund losses. By default, you should set `http://localhost:1317`as the value ### Example Redelegate-on-Behalf Command Use ```bash theme={null} ./story validator redelegate-on-behalf \ --delegator-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \ --validator-src-pubkey 03bdc7b8940babe9226d52d7fa299a1faf3d64a82f809889256c8f146958a63984 \ --validator-dst-pubkey 02ed58a9319aba87f60fe08e87bc31658dda6bfd7931686790a2ff803846d4e59c \ --redelegate 1024000000000000000000 \ --rpc \ --chain-id ``` ## Set Operator Delegators may add operators to unstake or redelegate on their behalf. To add an operator, run the following command: * `--chain-id` int Chain ID to use for the transaction (default 1514) * `--explorer` string URL of the blockchain explorer (default "[https://www.datanetscan.io](https://www.datanetscan.io)") * `--operator` string Sets an operator to your delegator * `--rpc` string RPC URL to connect to the network (default "[https://mainnet.datarpc.io](https://mainnet.datarpc.io)") ```bash theme={null} ./story validator set-operator \ --operator ${OPERATOR_EVM_ADDRESS} \ --rpc \ --chain-id \ --story-api ${DATAFDN_API_URL} ``` Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid. ### Example Add Operator Command Use ```bash theme={null} ./story validator set-operator \ --operator 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \ --rpc \ --chain-id \ --story-api http://localhost:1317 ``` ## Unset Operator To remove an operator, run the following command: ```bash theme={null} ./story validator unset-operator \ --operator ${OPERATOR_EVM_ADDRESS} \ --rpc \ --chain-id \ --story-api ${DATAFDN_API_URL} ``` ### Example Remove Operator Command Use ```bash theme={null} ./story validator remove-operator \ --operator 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab \ --rpc \ --chain-id \ --story-api http://localhost:1317 ``` ## Set Rewards Address To change the address that your delegator receives staking and withdrawal rewards from, you can run the following: ```bash theme={null} ./story validator set-rewards-address \ --rewards-address ${OPERATOR_EVM_ADDRESS} \ --story-api ${DATAFDN_API_URL} ``` Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid. ### Example Set Withdrawal Address Command Use ```bash theme={null} ./story validator set-rewards-address \ --rewards-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab --story-api http://localhost:1317 ``` ## Set Withdrawal Address To change the address that your delegator receives staking and withdrawal rewards from, you can run the following: ```bash theme={null} ./story validator set-withdrawal-address \ --withdrawal-address ${OPERATOR_EVM_ADDRESS} \ --story-api ${DATAFDN_API_URL} ``` Note that you will need at least 1 IP in the wallet submitting the transaction for the transaction to be valid. ### Example Set Withdrawal Address Command Use ```bash theme={null} ./story validator set-withdrawal-address \ --withdrawal-address 0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab --story-api http://localhost:1317 ``` ## Update Validator Commission To change the commission rate for your validator, you can run the following: ``` ./story validator update-validator-commission \ --commission-rate ${NEW_COMMISSION} ``` ### Example Update Validator Commission ``` ./story validator update-validator-commission \ --commission-rate 5000 ``` ## Enabling DATA Foundation API Prerequisites: 1. Ensure your full node is synced and caught up with latest blocks Steps to enable: 1. Navigate to `${DATAFDN_DATA_ROOT}/config/story.toml` 2. Set `enable = true` under the `[api]` section 3. Restart the node Then you could use `http://localhost:1317` as the `-story-api` value ## Migrating a Validator to Another Machine Before migrating your validator node to a new machine, make sure the current node is fully shut down. Attempting to restore an active validator could result in "double signing," a critical error that may lead to the slashing of your delegated shares. 1. Begin by configuring a new environment for your validator. Ensure that the new full node is fully synced to the latest block on the network. 2. To avoid accidental double-signing, it’s essential to fully shut down the original validator node before activating the new instance. We recommend deleting the Story service file to prevent it from automatically restarting after a system reboot. Additionally, back up both `priv_validator_key.json` and `priv_validator_state.json` and remove it from the current server running the active validator. Skipping these steps could result in missed blocks or other penalties. ```bash theme={null} # Step 1: Stop the original validator node sudo systemctl stop .service # Step 2: Disable the Story service to prevent automatic restarts sudo systemctl disable .service # Step 3: Delete the Story service file to prevent it from starting on reboot sudo rm /etc/systemd/system/.service # Step 4: Back up the `priv_validator_key.json` file securely, e.g., using SFTP: # Use an SFTP client or a secure method to download the file without displaying it in the terminal # If needed for verification purposes only, you may view it with the following command: cat ~/.story/story/config/priv_validator_key.json # Step 5: Remove the `priv_validator_key.json` file from the current server rm ~/.story/story/config/priv_validator_key.json ``` 3. Locate `priv_validator_key.json` and `priv_validator_state.json` in the `~/.story/story/config/` directory on your new machine. Replace this file with the backup copy from your old validator. Before proceeding, shut down the old validator on the original server and do not restart it! 4. After transferring the private key file, restart the validator node on your new setup. This will reintegrate your validator with the network, enabling it to resume its validation role. ## Private Key Encryption for Validators This feature allows operators to securely generate, store, and manage private keys in encrypted form through CLI commands, including encrypting keys during setup, migrating unencrypted keys, and safely accessing them for validator operations. ### Overview of Private Key Encryption Private key encryption is a critical security measure that encrypts sensitive cryptographic keys with a user-defined password. In the context of validator operations, this means that even if a private key file is exposed or accessed by unauthorized parties, it remains unusable without the correct password. Story CLI helps operators enforce stronger security controls while maintaining ease of use by integrating encryption directly into the validator workflow. This feature is designed to mitigate common attack vectors, such as accidental key leaks or unauthorized server access, by preventing private keys from being stored or used in plaintext. ### Initializing a Validator With Private Key Encryption When setting up a new validator using the Story CLI, operators can generate and encrypt the validator’s private key in one step. By adding the `--encrypt-priv-key` flag during initialization, the CLI will prompt the operator to create a password, which is used to encrypt the generated private key. This encrypted key is then securely stored at `story/config/priv_validator_key.enc`. Through this enhancement, private keys are never written to disk in plaintext during initialization, significantly reducing the risk of accidental exposure. Once encrypted, the key remains protected, and password decryption is required before any validator-related actions can be performed. **Example usage:** ```bash theme={null} ./story init --encrypt-priv-key --network local ``` In this example, the validator is initialized on the local network, automatically triggering the private key encryption process. During this step, the operator is prompted to input and confirm a password. The resulting encrypted key file becomes the foundation for all validator operations. ### Using the Encrypted Private Key Once a private key is encrypted, decryption is required for any key-dependent validator operation. The Story CLI will automatically detect the encrypted key file and prompt for a password when running a validator node. For validator-related CLI commands, however, the path of the encrypted key file should be specified. If no key file path is provided, the CLI will fall back to the private key in the .env file. The password prompt appears interactively via the CLI, blocking execution if the wrong password is provided. As a result, only authorized operators with the correct password can unlock and use the private key, adding protection against unauthorized access. This workflow integrates with typical validator commands, preserving the developer experience while strengthening security. For environments that require automation, additional tooling (e.g., password managers or secure key vaults) can be used to manage passwords securely. Still, caution is advised to avoid undermining the purpose of encryption. **Example:** ```bash theme={null} ./story validator start # CLI will prompt: "Enter password to decrypt private key:" ``` This behavior applies consistently across all validator commands requiring private key access. ### Encrypting an Existing Private Key For operators already running validators with unencrypted private keys, Story CLI now provides a simple migration path to adopt encrypted key storage. Using the encrypt command, you can securely encrypt the existing private key (typically defined in the .env file). Once the command is executed, the CLI will prompt you to set a password. The private key will be encrypted and stored at a specified path using the --enc-key-file flag. The encrypted key becomes the default for validator operations, allowing the original key in the .env file to be manually removed for better security. This does not apply to validator-related CLI commands, which still require explicit key input. **Example usage:** ```bash theme={null} ./story encrypt --encrypt-priv-key ``` This enables existing validators to benefit from the enhanced security of encrypted key management without requiring re-initialization. It’s recommended that the password and encrypted key file be securely backed up before deleting the plaintext key from the `.env` file. ### Viewing the Encrypted Private Key Story CLI introduces a show command to give operators transparency and control over their encrypted keys. This command decrypts the encrypted private key file (specified via the `--enc-key-file` flag) and displays essential information such as the public key and validator address. It mirrors the functionality of the traditional export command but supports encrypted keys. Operators will be prompted for the encryption password before revealing key details, protecting sensitive information from unauthorized access. For advanced users, the optional `--show-private` flag will reveal the hex-encoded private key. However, this should be used cautiously as it defeats the purpose of encryption and exposes the key to potential compromise. **Example usage:** ```bash theme={null} # View public information (e.g., public key, address) ./story key show-encrypted --encrypt-key-file # View public information and the raw private key (use with caution) ./story key show-encrypted --show-private --enc-key-file ``` It is recommended that `--show-private` be used only in secure environments and for exceptional operational needs, such as recovery or migration to other systems. # Release Notes Source: https://docs.datafdn.org/network/participate/validators/release-notes Information on DATA Foundation execution and consensus client software releases This page provides information on the story execution and consensus client software release information. You may find execution client releases in [story-geth](https://github.com/piplabs/story-geth/releases) repo, and consensus client releases in [story](https://github.com/piplabs/story/releases) repo. ### Production Releases There are generally four types of releases: * Major: It requires hardfork upgrade with a predefined upgrade height. Node operators need to upgrade before or on the height. The release will increase minor version number. * Minor: It doesn't require hardfork upgrade. Node operators are required to upgrade binaries as soon as possible. The release will increase patch version number. * Fix: It is an urgent fix. Node operators are required to upgrade binaries as soon as possible. The release will increase minor version or patch version number. * Optional: It is an optional fix. Node operators can upgrade binaries based on needs. The release will increase patch version number. Each release comes with a release note describing a list of new features or fixes. Released software binaries are also attached in the release note. We currently provide binaries supporting four types of systems: darwin-amd64, darwin-arm64, linux-amd64, and linux-arm64. You may also build your binaries using the commit hash in the release note. ### Release Entries Refer to the following release matrix to run nodes for Mainnet and Aeneid Testnet. | Network | story-geth | story | | ------- | ----------------- | ---------------- | | Mainnet | v1.2.0 (Yasunari) | v1.4.2 (Terence) | | Aeneid | v1.2.0 (Yasunari) | v1.4.2 (Terence) | ### Terence [Full release note](https://github.com/piplabs/story/releases/tag/v1.4.2) * Security fixes ### Polybius [Full release note](https://github.com/piplabs/story/releases/tag/v1.3.3) * Backport the fixes for release v1.3.3 (#598) ### Polybius [Full release note](https://github.com/piplabs/story/releases/tag/v1.3.2) * Increases the max number of validators from 64 to 80 ### Polybius [Full release note](https://github.com/piplabs/story/releases/tag/v1.3.1) handling residual rewards by version ### Cosmas [Full release note](https://github.com/piplabs/story-geth/releases) Enables Pectra Upgrade for Mainnet ### Cosmas (Testnet Only) [Full release note](https://github.com/piplabs/story-geth/releases) * EIP-7702 – Set EOA account code * EIP-2537 – BLS12-381 curve operations * EIP-7623 – Increase calldata cost * EIP-7685 – Execution layer requests (EIP-7685) ### Ovid [Full release note](https://github.com/piplabs/story/releases/tag/v1.2.0) * (app) change MaxBytes of block in consensus params (#529) * (x/evmengine) support snap sync for execution engine (#506) * (cli) add encryption for validator private key (#494) * (cli) add with-comet flag (#518) * (api) add withdrawal queue query (#496) * (app) disallow unexported fields in cosmos tx (#529) * (x/evmengine) add validation for max size of tx (#529) * (cli) fix validator not found error during validator creation (#515) * (cli) add validation for max commission change rate (#489) * (cli) add self-delegation validation to unjail command (#510) * (api) fix incorrect type conversion between integer types (#492) ### Ovid [Full release note](https://github.com/piplabs/story-geth/releases/tag/v1.0.2) * \[ipgraph] prevent overflow in calculating gas of add parent (#102) * \[ipgraph] apply acl to function hasAncestorIp (#106) # Troubleshooting Source: https://docs.datafdn.org/network/participate/validators/troubleshooting Common problems and solutions when running DATA Foundation nodes Welcome to DATA Foundation node troubleshooting! This section covers common problems and solutions when running DATA Foundation nodes. ### Node Setup See the [system specs](/network/operating-a-node/node-setup-mainnet) \~700 Yes, it's EVM-compatible. The DATA Foundation's execution client is a fork of Geth with our custom precompiles, which enhance the IP graph's performance while maintaining strict EVM compatibility. Other Ethereum execution clients, such as RETH and Erigon, can be supported later. Our consensus mechanism is CometBFT Batch RPCs are supported - for Geth there is a 1K limit and on the consensus side there is 10 request limit Yes, WS is enabled on the execution client, and is recommended for subscription use-cases. It is open on port 8546 Please see Geth's latest JSON-RPC documentation for a full comprehensive list [here](https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion). In the future, we may add more. We recommend employing standard in-memory caching with a 1-10 min TTL based on the RPC method Use `eth_syncing` RPC call on the execution client to check if the node is sync and `eth_blockNumber` for getting the latest block `eth_call` / `eth_getLogs` / `eth_getBlockByNumber` \ We are still running latency tests to get a sense of response times. No, not at the moment. Not yet, but we are working on it.
### Common Issues **Error:** ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly !! err="create db: failed to initialize database: ``` **Solution:** 1. Save your validator state: ```bash theme={null} cp $HOME/.story/story/data/priv_validator_state.json $HOME/.story/story/priv_validator_state.json.backup ``` > 🚧 Be very careful with this file, especially if your validator is already signing blocks. * Check your the database backend type, your node must support the same as you are using the snapshot: ```bash theme={null} cat $HOME/.story/story/config/story.toml ``` Default is `app-db-backend = "goleveldb"`. The fallback is the `db_backend` value set in CometBFT's `config.toml`. ```bash theme={null} cat $HOME/.story/story/config/config.toml ``` **Problem:** Need to adjust gas fees on RPC node **Solution:** Add the `--rpc.txfee` flag to your geth startup command: ```bash theme={null} sudo tee /etc/systemd/system/story-geth.service > /dev/null < **Error:** ```bash theme={null} ERRO Failed to send PacketPing module=p2p peer=19fa6dd52e72e4e85bbb873b705282cf73217a6b@158.220.80.96:40128 err="write tcp 139.59.139.135:26656->158.220.80.96:40128: write: broken pipe" ``` Solution: * If the node is synchronized, you can ignore this error. Your client may be a little behind. * If the node stops, you should restart the services. An error occurs when starting the cosmovisor: ```bash theme={null} panic: failed to read upgrade info from disk unexpected end of JSON input ``` Solution: * You must ensure that the installed cosmovisor version must be at least [v1.7.0.](https://docs.cosmos.network/main/build/tooling/cosmovisor) * Then check your info file (edit version `v0.13.0` in your case): ```bash theme={null} cat $HOME/.story/story/cosmovisor/upgrades/v0.13.0/upgrade-info.json ``` If you don\`t have create new one: ```bash theme={null} echo '{"name":"v0.13.0","time":"0001-01-01T00:00:00Z","height":858000}' > $HOME/.story/story/cosmovisor/upgrades/v0.13.0/upgrade-info.json ``` Find out more about automatic updates with cosmovisor [here](/network/operating-a-node/node-setup-mainnet#custom-automation). Error: ```bash theme={null} INFO HTTP server stopped INFO IPC endpoint closed ``` Solution: * It looks like port 8551 stopping, the background process running `iptables` blocking ip and port and access posix. * For solution try uninstall `ufw posix` and `iptables`: ```bash theme={null} iptables -I INPUT -s localhost -j ACCEPT ``` Error: ```bash theme={null} panic: Faile to consensus state: found signature from the same key ``` Solution: * The validator has been double signed. It is currently not possible to restore the validator after it has been double signed. * To avoid such situations, see this post on how to correctly [migrate a validator to another machine](/network/become-a-validator#migrating-a-validator-to-another-machine). Error: ```bash theme={null} 4-11-26 08:42:20.302 ERRO !! Fatal error occurred, app died️ unexpectedly !! err="failed to validate create flags: missing required flag(s): moniker" stacktrace="[errors.go:39 flags.go:173 validator.go:168 validator.go:384 command.go:985 command.go:1117 command.go:1041 command.go:1034 cmd.go:34 main.go:10 proc.go:271 asm_amd64.s:1695]" ``` Solution: * You missed flag `--moniker`. * The command to create a new validator should look like this: ```bash theme={null} ./story validator create --stake ${AMOUNT_TO_STAKE_IN_WEI} --moniker ${VALIDATOR_NAME} ``` See more options [here](/network/become-a-validator#validator-creation). Error: ```bash theme={null} ERRO failed to process message msg_type= *consensus.VoteMessage err:" error adding vote" ``` Solution: * It looks like your node is down. To get started, check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet). * If you have up-to-date binary - try updating peers, this usually happens when a node loses p2p communication: ```bash theme={null} PEERS="..." sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers *=.*/persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml ``` Error: ```bash theme={null} ERRO failed signing vote module=consensus height=403750 round=0 vote="Vote{23:B12C6AE31E8E 403750/00/SIGNED_MSG_TYPE_PREVOTE(Prevote) FA591EB1E540 000000000000 000000000000 @ 2024-11-08T16:58:10.375918193Z}" err="error signing vote: height regression. Got 403750, last height 420344" ``` Solution: * Looks like you have a problem with your `priv_validator_state` of validator. > 🚧 Be very careful with this file, especially if your validator is already signing blocks. * You can make a copy of your state with a command: ```bash theme={null} cp $HOME/.story/story/data/priv_validator_state.json $HOME/.story/story/priv_validator_state.json.backup ``` Check your validator state: ```bash theme={null} cat $HOME/.story/story/data/priv_validator_state.json ``` * If you get this error, you can reset your state (🚧 ONLY IF YOUR VALIDATOR HAS NOT YET SIGNET BLOCKS). * Stop node. ```bash theme={null} sudo tee $HOME/.story/story/data/priv_validator_state.json > /dev/null < Error: ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly !! err="unknown flag: --home" ``` Solution: * It looks like a misconfiguration. You must try to remove the `--home` flag from the startup command. * Your systemd to run might look like this: Error: ```bash theme={null} Fatal: Failed to register the Ethereum service: incompatible state scheme, stored: path, provided: hash ``` Solution: * You have problems with the state of validator or a corrupted database. * Try using a snapshot. > 🚧 Be very careful with this file, especially if your validator is already signing blocks. * We have described how to reset your state [here](/network/more/troubleshooting#error-signing-vote). ## Failed to reconnect to peer Error: ```bash theme={null} 24-09-25 06:38:45.235 ERRO Failed to reconnect to peer. Beginning exponential backoff module=p2p addr=e0600fa5f2129e647ef30a942aac1695201ff135@65.109.115.98:26656 elapsed=2m29.598884906s ``` Solution: * If the node is synchronized and not far behind, you can ignore this error. * If the node is lagging or has stopped completely, try updating peers, this usually happens when a node loses p2p communication: ```bash theme={null} PEERS="..." sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers =./persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml ``` Warn: ```bash theme={null} WARN Processing finalized payload halted while evm syncing (will retry) payload_height=... ``` Solution: * It just means that story-geth is syncing, you can ignore this warn. * However, if it takes a long time, we recommend that you stop the processes one at a time and start them again later in the following order: ```bash theme={null} sudo systemctl stop story-geth story sudo systemctl daemon-reload sudo systemctl start story-geth sudo systemctl enable story-geth sudo systemctl daemon-reload sudo systemctl start story sudo systemctl enable story ``` Error: ```bash theme={null} ERRO error in proxyAppConn.FinalizeBlock module=consensus err="module manager preblocker: wrong app version 0, upgrade handler is missing for upgrade plan" ``` Solution: * Looks like you missed an update. * To get started, check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet). Error: ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly !! err="home directory contains unexpected file(s), use --force to initialize anyway" ``` Solution: * This means that you have already initialized the node. * `$HOME/.story/story` directory created, and there are files in it. Delete it, or try with it. Error: ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node ``` Solution: * It appears that your node is using incorrect versions. * Check the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet). * And most likely you need to perform a rollback binary to current versions. Error: ```bash theme={null} ERRO catchup replay: WAL does not contain ``` Solution: * Looks like an `AppHash` issue. * To get started, upgrade to the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet). * If your versions are newer than the current ones, perform a rollback. Error: ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly !! err="load engine JWT file: read jwt file: open /root/.story/geth/odyssey/geth/jwtsecret: no such file or directory ``` Solution: * It seems your node can't get `jwtsecret`. * Check your `WorkingDirectory` in your `geth-service` , by default `WorkingDirectory=$HOME/.story/geth`. * Check all paths, you can get your `jwtsecret`with command (for odyssey network): ```bash theme={null} cat .story/geth/odyssey/geth/jwtsecret ``` Error: ```bash theme={null} ERRO Couldn't connect to any seeds module=p2p ``` Solution: * If the node is synchronized and not far behind, you can ignore this error. * If the node is lagging or has stopped completely, try updating seeds/peers, it usually happens when a node loses p2p communication (we recommend that you stop the node and delete the addrbook). ```bash theme={null} rm -rf $HOME/.story/story/config/addrbook.json SEEDS="..." PEERS="..." sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*seeds *=.*/seeds = \"$SEEDS\"/}" \ -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers *=.*/persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml ``` Warn: ```bash theme={null} WRN Processing finalized payload; evm syncing WRN Processing finalized payload failed: evm fork choice update (will retry) status="" err="rpc forkchoice updated v3: beacon syncer reorging" ``` Solution: * Everything is fine, it just means that `story-geth` is syncing, which takes some time. * If the node is not far behind, you can ignore this warning. ## Dial tcp 127.0.0.1:9090 Warn: ```bash theme={null} WRN error getting latest block error:"rpc error: dial tcp 127.0.0.1:9090" ``` Solution: * The logs show a connection failure on port `9090`. * Check the listening ports: ```bash theme={null} sudo ss -tulpn | grep LISTEN ``` * If other node uses `9090`, then modify it to another. * Normally, this WARNING should not affect the performance of your node. Error: ```bash theme={null} ERRO Error in validation module=blocksync err="wrong Block[dot]Header[dot]AppHash Expected [...] ``` Solution: * `Wrong AppHash` type logs means the story node version you are using is wrong. * Upgrade to the current versions of the binaries [here](/network/operating-a-node/node-setup-mainnet). * If your versions are newer than the current ones, perform a rollback. Error: ```bash theme={null} ERRO Connection failed @ sendRoutine module=p2p peer=... ERRO Stopping peer for error module=p2p peer=... ``` Solution: * If the node is synchronized and not far behind, you can ignore this error. * If the node is lagging or has stopped completely, try updating peers, this usually happens when a node loses p2p communication: ```bash theme={null} PEERS="..." sed -i -e "/^\[p2p\]/,/^\[/{s/^[[:space:]]*persistent_peers =./persistent_peers = \"$PEERS\"/}" $HOME/.story/story/config/config.toml ``` Error: ```bash theme={null} ERRO !! Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node: info.Moniker must be valid non-empty ``` Solution: * Looks like a problem with your node moniker. * Be sure to use `""` when executing init: ```bash theme={null} story init --network "..." --moniker "..." ``` * Go to config, find the moniker and put it inside `""` only: ```bash theme={null} sudo nano ~/.story/story/config/config.toml ``` Error: ```bash theme={null} Fatal error occurred, app died️ unexpectedly ! err="create comet node: create node: invalid address (26656): ``` Solution: * The logs report a connection failure on port `26656`. * Check the listening ports: ```bash theme={null} sudo ss -tulpn | grep LISTEN ``` * If another node is using `26656`, change it to another and keep the default `26656` for story in the `P2P configuration` options in `config`: ```bash theme={null} sudo nano ~/.story/story/config/config.toml ``` Warn: ```bash theme={null} WARN Beacon client online, but no consensus updates received in a while. Please fix your beacon client to follow the chain! Served eth_coinbase eth_coinbase does not exist ``` Solution: * This error indicates that the network has stopped. Warn: ```bash theme={null} WARN Verifying proposal failed: push new payload to evm (will retry) status="" err="new payload: rpc new payload v3: Post \"http://localhost:8551\": round trip: dial tcp 127.0.0.1:8551: connect: connection refused" stacktrace="[errors.go:39 jwt.go:41 client.go:259 client.go:180 client.go:724 client.go:590 http.go:229 http.go:173 client.go:351 engineclient.go:101 msg_server.go:183 proposal_server.go:34 helpers.go:30 proposal_server.go:33 tx.pb.go:299 msg_service_router.go:175 tx.pb.go:301 msg_service_router.go:198 prouter.go:74 abci.go:520 cmt_abci.go:40 abci.go:85 local_client.go:164 app_conn.go:89 execution.go:166 state.go:1381 state.go:1338 state.go:2055 state.go:910 state.go:836 asm_amd64.s:1695]" WARN Verifying proposal ``` Solution: * It looks like port 8551 stopping, the background process running `iptables` blocking ip and port and access posix. * For solution try uninstall `ufw posix` and `iptables`: ```bash theme={null} iptables -I INPUT -s localhost -j ACCEPT ``` # Disclaimers Source: https://docs.datafdn.org/notices # Quickstart Source: https://docs.datafdn.org/quickstart Start building on the DATA Foundation quickly. You want to start building on the DATA Foundation quickly... so let's get started! The DATA Foundation *** ## Add Network Enable DATA Foundation's mainnet or testnet for your wallet. Connect your wallet to DATA Network. Connect your wallet to the Aeneid testnet. ## Skip everything. Go to the code. This is a clone-able quickstart for you to check out. You can clone it directly and follow the associated README. This is a clone-able quickstart for you to check out. You can clone it directly and follow the associated README. This is a boilerplate for you to check out. You can clone it directly, study the example smart contracts, and follow the associated README for running the tests. ## DATA Network Infra See [Network Info](/network/overview) for all RPC, explorer, and faucet info. ## Use Our SDKs For confidential data, [🔒 Confidential Data Rails (CDR)](/developers/cdr-sdk/overview) is the primary way to encrypt, store, and gate access to data on the DATA Foundation. Start with the [CDR SDK Guide](/developers/cdr-sdk/overview) (`@piplabs/cdr-sdk`). For IP and licensing, check out the entire [SDK Reference](/sdk-reference) to see an explanation + example for every function in our 🛠️ **TypeScript SDK** (can use this in React as well) and 🐍 **Python SDK**. We have also built a [🛠️ TypeScript SDK Guide](/developers/typescript-sdk), a step-by-step walkthrough for registering IP and attaching licenses. ## Deployed Smart Contracts Check out the addresses for the deployed smart contracts [here](/developers/deployed-smart-contracts). Note that there are two different kinds of contracts: * [DATA Foundation Core](https://github.com/thedatafoundation/protocol-core-v1) - This repository contains the core protocol logic, consisting of a thin IP registry (the IP Asset Registry), a set of modules defining logic around [📜 Licensing](/concepts/licensing-module), and a module manager for administering module and user access control. * [DATA Foundation Periphery](https://github.com/thedatafoundation/protocol-periphery-v1)- Whereas the core contracts deal with the underlying protocol logic, the periphery contracts deal with protocol extensions that greatly increase UX and simplify IPA management. This is mostly handled through the [📦 SPG](/concepts/spg). ## Use Our API Check out the entire [API Reference](/api-reference) for learning how to use our API. For common things like fetching gas price, average block time, market cap, token price, and more, check out the [Blockscout API](/api-reference/blockscout-api). ## Register IP on the DATA Foundation Let's start with the most basic question: *"What does it take to register IP on the DATA Foundation in my app? How do I do this?"* To register IP on the DATA Foundation, you'll first need an NFT. If your IP is an ERC-721 NFT (ex. an Azuki or Pudgy Penguin on the DATA Foundation), you're already set. If not, you must mint an NFT to represent your off-chain IP. And don't worry, we'll help you do this in the following tutorials. Next you'd register that NFT on the DATA Foundation, ultimately creating an [🧩 IP Asset](/concepts/ip-asset). An "IP Asset" is your IP registered on the DATA Foundation, empowered by: * the DATA Foundation's [📜 Licensing Module](/concepts/licensing-module), enabling transparent on-chain licensing * IP protection through the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license) * the ability to gate [confidential data (CDR)](/developers/cdr-sdk/overview) on the licenses minted from it Follow the below tutorials to register IP on the DATA Foundation: Learn how to register IP on the DATA Foundation using the TypeScript SDK. Learn how to register IP on the DATA Foundation using the Smart Contracts. ### Difference Between IP Metadata vs. NFT Metadata A common question we get from developers while registering their IP on the DATA Foundation is: *"What metadata should be/is expected to be attached to the NFT, and then separately, the IP Asset?"* To answer that question, please see [NFT vs. IP Metadata](/concepts/ip-asset/overview#nft-vs-ip-metadata). ## Licensing Your IP You may be wondering, *"How do I take advantage of the DATA Foundation's on-chain licensing? How do I make sure my registered IP has a license ready to go?"* Before you attach any sort of licenses or license terms to your [🧩 IP Asset](/concepts/ip-asset), it would be best to first understand what the [💊 Programmable IP License (PIL)](/concepts/programmable-ip-license) actually is. This "PIL" is what defines the available [License Terms](/concepts/licensing-module/license-terms) on the DATA Foundation, which in turn - when attached to an IP Asset - is what defines how others can use (commercially, create derivatives, etc) that IP Asset. Our tutorials will show you exactly how to attach license terms to your IP Asset: Learn how to attach license terms to your IP on the DATA Foundation using the TypeScript SDK. Learn how to attach license terms to your IP on the DATA Foundation using the Smart Contracts. For more information on licensing and the terminology behind it, check out the [📜 Licensing Module](/concepts/licensing-module). ## Gate the License Token to Confidential Data The License Token you minted above is the key primitive CDR uses for **IP-gated content**: you can encrypt data with [Confidential Data Rails (CDR)](/developers/cdr-sdk/overview) and require a wallet to hold a License Token for a given IP Asset before it can decrypt. This ties confidential data directly to the on-chain licensing terms you defined. Encrypt data and gate decryption on holding a License Token. Install the CDR SDK and run your first encrypt/decrypt flow. ## Prove Data Provenance With Trace If you're a data provider, [Trace](/trace/overview) gives you verifiable, provider-normalized provenance for the data you handle (content hashes, contributor consent, and KYC signals, with public audit views) through a simple REST integration. Understand how Trace records and audits data provenance. The full write, read, and search API for providers. # Consumer Source: https://docs.datafdn.org/sdk-reference/cdr/consumer Methods for requesting and performing CDR decryption. ## Consumer The `Consumer` sub-client handles read requests, partial decryption collection from validators, and final decryption. Requires a `walletClient`. ```typescript theme={null} const consumer = client.consumer; ``` The SDK also exposes `readVault` as an alias for `accessCDR`, and `readFileVault` as an alias for `downloadFile`. Partial decryptions are collected from the DATA Foundation API REST endpoint (`/dkg/cdr_partials`), keyed by `(uuid, requesterPubKey)`. The keeper verifies each validator's signature on ingress, so the SDK does not re-verify signatures locally. For defense-in-depth, pass an `attestationConfig` to verify each validator's SGX enclave before accepting their partials. ### Methods * accessCDR * downloadFile * read * collectPartials * decryptDataKey * prefetchRegistry *** ### accessCDR High-level method that submits a read request, collects partial decryptions from validators, and combines them to recover the original data. | Method | Type | | ----------- | --------------------------------------------------------- | | `accessCDR` | `(params: AccessCDRParams) => Promise` | Parameters: * `params.uuid`: `number` - The vault UUID * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition * `params.requesterPubKey` *(optional)*: `` `0x${string}` `` - Uncompressed secp256k1 public key (65 bytes, `0x04` prefix). If omitted, the SDK generates an ephemeral keypair. * `params.recipientPrivKey` *(optional)*: `Uint8Array` - 32-byte secp256k1 private key (for ECIES decryption of partials). If omitted, the SDK generates an ephemeral keypair and zeroes it after use. * `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key (from `observer.getGlobalPubKey()`). If omitted, the SDK queries it for you. * `params.timeoutMs` *(optional)*: `number` - Timeout for collecting partials. `120_000` is a good starting point. * `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query (strict-equality semantics, not a way to pay a different amount). * `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks. * `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave before accepting their partials. ```typescript Example theme={null} const { dataKey, txHash } = await client.consumer.accessCDR({ uuid: 42, accessAuxData: "0x", timeoutMs: 120_000, }); const secret = new TextDecoder().decode(dataKey); console.log(`Read tx: ${txHash}`); console.log(`Decrypted: ${secret}`); ``` If you want the shortest high-level path, you can omit `requesterPubKey`, `recipientPrivKey`, and `globalPubKey` and let `accessCDR()` fill them in. The threshold is derived automatically from the partial-decryption bucket's DKG round. Throws `EmptyVaultError` synchronously if the vault has never been written to. This is raised by a preflight chain read *before* the fee-bearing `read()` transaction is submitted, so no fee is spent. ```typescript AccessCDRResponse theme={null} interface AccessCDRResponse { dataKey: Uint8Array; // the recovered plaintext txHash: `0x${string}`; // read request transaction hash } ``` *** ### downloadFile High-level method that reads the encrypted file key through CDR, downloads the encrypted blob from a `StorageProvider`, and returns the decrypted file bytes. Parameters: * `params.uuid`: `number` - The vault UUID * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition * `params.storageProvider`: `StorageProvider` - Backend used to fetch the encrypted content * `params.requesterPubKey` *(optional)*: `` `0x${string}` `` - Explicit requester public key for the read flow * `params.recipientPrivKey` *(optional)*: `Uint8Array` - Explicit recipient private key for the read flow * `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key. Auto-queried if omitted. * `params.timeoutMs` *(optional)*: `number` - Timeout for validator partial collection * `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query. * `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks. * `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave before accepting their partials. * `params.skipCidVerification` *(optional)*: `boolean` - Skip CID integrity verification of the downloaded encrypted file (default: `false`) ```typescript Example theme={null} import { writeFile } from "node:fs/promises"; const { content } = await client.consumer.downloadFile({ uuid: 42, accessAuxData: "0x", storageProvider, timeoutMs: 120_000, }); await writeFile("./example.decrypted.pdf", Buffer.from(content)); console.log("Saved ./example.decrypted.pdf"); ``` For DATA Foundation license-gated reads, `accessAuxData` should encode the caller's license token IDs as `abi.encode(uint256[] licenseTokenIds)`. `downloadFile()` inherits the same optional key auto-management behavior as `accessCDR()`, and returns `cid` and `txHash` alongside `content`. `content` is the raw decrypted file bytes. Decode it as text only if the original file was text-based. *** ### read Submits a read request on-chain. The caller must satisfy the vault's read condition. This prompts validators to submit encrypted partial decryptions. | Method | Type | | ------ | ----------------------------------------------- | | `read` | `(params: ReadParams) => Promise` | Parameters: * `params.uuid`: `number` - The vault UUID * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the read condition * `params.requesterPubKey`: `` `0x${string}` `` - Your ephemeral uncompressed secp256k1 public key. Partials are indexed by this value. * `params.feeOverride` *(optional)*: `bigint` - Explicit read fee. Skips the `readFee()` auto-query. ```typescript Example theme={null} const { txHash } = await client.consumer.read({ uuid: 42, accessAuxData: "0x", requesterPubKey, }); ``` ```typescript ReadResponse theme={null} interface ReadResponse { txHash: `0x${string}`; } ``` *** ### collectPartials Polls the DATA Foundation API REST endpoint (`/dkg/cdr_partials`) until at least a threshold's worth of partial-decryption submissions have been surfaced for the given `(uuid, requesterPubKey)`. | Method | Type | | ----------------- | ---------------------------------------------------------------------- | | `collectPartials` | `(params: CollectPartialsParams) => Promise` | Parameters: * `params.uuid`: `number` - The vault UUID * `params.requesterPubKey`: `` `0x${string}` `` - The uncompressed secp256k1 public key used in the matching `read()` request * `params.timeoutMs` *(optional)*: `number` - Timeout in milliseconds. `120_000` is a good starting point. * `params.pollIntervalMs` *(optional)*: `number` - Polling interval in milliseconds * `params.onInvalidPartial` *(optional)*: `(event, error) => void` - Called when a validator's partial is excluded because its attestation failed the `attestationConfig` checks * `params.attestationConfig` *(optional)*: `AttestationConfig` - Verifies each validator's SGX enclave and excludes partials from untrusted validators The required threshold is derived from the partial-decryption bucket's own DKG round (`observer.getThresholdAt(round)`), so a DKG rollover mid-poll does not measure the bucket against the wrong round. The vault ciphertext is read once at the start of the call and pinned for the rest of the poll loop. Throws `PartialCollectionTimeoutError` if the timeout is reached before enough partials are collected. Throws `EmptyVaultError` if the vault has never been written to. ```typescript Example theme={null} const partials = await client.consumer.collectPartials({ uuid: 42, requesterPubKey, timeoutMs: 120_000, }); console.log(`Collected ${partials.length} partials`); ``` ```typescript PartialDecryptionEvent theme={null} interface PartialDecryptionEvent { validator: `0x${string}`; round: number; pid: number; // 1-based participant index encryptedPartial: `0x${string}`; // AES-GCM encrypted ephemeralPubKey: `0x${string}`; // 65 bytes, uncompressed secp256k1 pubShare: `0x${string}`; // 34 bytes, Ed25519 with curve-code prefix uuid: number; ciphertext: `0x${string}`; // TDH2 ciphertext this partial decrypts } ``` Every event returned from a successful `collectPartials` call shares the same `round` and `ciphertext`: the result is filtered to the bucket matching the vault's current ciphertext. *** ### decryptDataKey Decrypts the collected partial decryptions using ECIES, then combines them via TDH2 to recover the original plaintext. | Method | Type | | ---------------- | ------------------------------------------------ | | `decryptDataKey` | `(params: DecryptParams) => Promise` | Parameters: * `params.ciphertext`: `TDH2Ciphertext` - The encrypted data (`{ raw, label }`) * `params.partials`: `PartialDecryptionEvent[]` - Collected partial decryptions * `params.recipientPrivKey`: `Uint8Array` - Your ephemeral secp256k1 private key (32 bytes) * `params.globalPubKey`: `Uint8Array` - DKG global public key * `params.label`: `Uint8Array` - 32-byte label (from `uuidToLabel(uuid)`) The TDH2 combine threshold is taken implicitly as `partials.length`, `collectPartials` already returns exactly the threshold count needed for reconstruction. Pass exactly the partials you want combined. Throws `InsufficientPartialsError` if fewer partials are passed than the ciphertext requires. Throws `InvalidCiphertextError` if the ciphertext is empty or malformed. ```typescript Example theme={null} import { uuidToLabel } from "@piplabs/cdr-sdk"; const label = uuidToLabel(uuid); const dataKey = await client.consumer.decryptDataKey({ ciphertext: { raw: ciphertextBytes, label }, partials, recipientPrivKey, globalPubKey, label, }); const secret = new TextDecoder().decode(dataKey); ``` *** ### prefetchRegistry Warms the validator `commPubKey` + attestation cache for the active DKG round. The first `accessCDR()` / `downloadFile()` call after construction would otherwise stall on this fetch. Frontends that know a read is imminent (for example, right after wallet connection) can call this in the background. | Method | Type | | ------------------ | --------------------- | | `prefetchRegistry` | `() => Promise` | ```typescript Example theme={null} // Best-effort warm-up, safe to call repeatedly client.consumer.prefetchRegistry().catch(() => {}); ``` # Crypto Utilities Source: https://docs.datafdn.org/sdk-reference/cdr/crypto Low-level TDH2 and ECIES cryptographic primitives used by the CDR SDK. ## Crypto Utilities These are the low-level cryptographic functions used internally by the CDR SDK. You typically won't need to call these directly unless you're building custom decryption flows. All crypto functions require WASM to be initialized first via `initWasm()`. The current Aeneid release also includes SGX attestation verification utilities, including `verifyAttestation()`, for checking validator attestations against expected enclave measurements and security version values. ### Functions * initWasm * tdh2Encrypt * tdh2Verify * tdh2Combine * decryptPartial * parseSgxQuote * verifyAttestation * uuidToLabel *** ### initWasm Initializes the WebAssembly module required for all TDH2 cryptographic operations. Must be called once before any other crypto function. | Function | Type | | ---------- | --------------------- | | `initWasm` | `() => Promise` | ```typescript Example theme={null} import { initWasm } from "@piplabs/cdr-sdk"; await initWasm(); // Now TDH2 functions are ready to use ``` Throws `WasmNotInitializedError` if you call any TDH2 function before `initWasm()` completes. *** ### tdh2Encrypt Encrypts plaintext using TDH2 threshold encryption against the DKG global public key. | Function | Type | | ------------- | -------------------------------------------------------- | | `tdh2Encrypt` | `(params: TDH2EncryptParams) => Promise` | Parameters: * `params.plaintext`: `Uint8Array` - The data to encrypt * `params.globalPubKey`: `Uint8Array` - DKG global public key (34 bytes with Ed25519 prefix) * `params.label`: `Uint8Array` - 32-byte label binding ciphertext to a specific vault ```typescript Example theme={null} import { tdh2Encrypt, uuidToLabel } from "@piplabs/cdr-sdk"; const ciphertext = await tdh2Encrypt({ plaintext: new TextEncoder().encode("secret"), globalPubKey, label: uuidToLabel(42), }); ``` ```typescript TDH2Ciphertext theme={null} interface TDH2Ciphertext { raw: Uint8Array; // serialized ciphertext (cb-mpc format) label: Uint8Array; // 32-byte context binding } ``` *** ### tdh2Verify Validates a TDH2 ciphertext without decrypting. Useful for checking integrity. | Function | Type | | ------------ | ------------------------------------------------ | | `tdh2Verify` | `(params: TDH2VerifyParams) => Promise` | Parameters: * `params.ciphertext`: `Uint8Array` - The raw ciphertext bytes * `params.globalPubKey`: `Uint8Array` - DKG global public key * `params.label`: `Uint8Array` - The 32-byte label used during encryption ```typescript Example theme={null} import { tdh2Verify } from "@piplabs/cdr-sdk"; const isValid = await tdh2Verify({ ciphertext: ciphertext.raw, globalPubKey, label: uuidToLabel(42), }); console.log(`Ciphertext valid: ${isValid}`); ``` *** ### tdh2Combine Combines threshold partial decryptions to recover the original plaintext. | Function | Type | | ------------- | ---------------------------------------------------- | | `tdh2Combine` | `(params: TDH2CombineParams) => Promise` | Parameters: * `params.ciphertext`: `TDH2Ciphertext` - The encrypted data (`{ raw, label }`) * `params.partials`: `DecryptedPartial[]` - Array of decrypted partials (must have `>= threshold`) * `params.globalPubKey`: `Uint8Array` - DKG global public key * `params.label`: `Uint8Array` - 32-byte label * `params.threshold`: `number` - Minimum number of partials required ```typescript Example theme={null} import { tdh2Combine } from "@piplabs/cdr-sdk"; const plaintext = await tdh2Combine({ ciphertext, partials: decryptedPartials, globalPubKey, label: uuidToLabel(42), threshold: 2, }); ``` ```typescript DecryptedPartial theme={null} interface DecryptedPartial { name: string; // validator name: key in the access structure pubShare: Uint8Array; // 34 bytes (Ed25519 with 0x043f prefix) partial: Uint8Array; // raw decrypted partial bytes } ``` *** ### decryptPartial Decrypts a single encrypted partial decryption from a validator using ECIES (ECDH + HKDF + AES-256-GCM). | Function | Type | | ---------------- | ------------------------------------------------------- | | `decryptPartial` | `(params: DecryptPartialParams) => Promise` | Parameters: * `params.encryptedPartial`: `Uint8Array` - The encrypted partial (nonce || ciphertext || tag) * `params.ephemeralPubKey`: `Uint8Array` - Validator's ephemeral public key (65 bytes, uncompressed secp256k1) * `params.recipientPrivKey`: `Uint8Array` - Your ephemeral private key (32 bytes, secp256k1) ```typescript Example theme={null} import { decryptPartial } from "@piplabs/cdr-sdk"; const rawPartial = await decryptPartial({ encryptedPartial: partialBytes, ephemeralPubKey: validatorEphemeralPubKey, recipientPrivKey: myEphemeralPrivKey, }); ``` **ECIES Protocol:** 1. ECDH: `sharedPoint = secp256k1(recipientPrivKey, ephemeralPubKey)` 2. HKDF-SHA256: `aesKey = hkdf(sha256, sharedSecret, info="dkg-tdh2-partial", 32)` 3. AES-256-GCM: decrypt with 12-byte nonce from the encrypted partial *** ### verifyAttestation Verifies a validator SGX attestation document against the expected enclave measurements and minimum security version checks. Use it with the data returned by `observer.getValidatorAttestations()` when your application wants an explicit `MRENCLAVE`, `MRSIGNER`, and `SVN` trust check. Parameters: * `report`: `Uint8Array` - Raw SGX DCAP Quote v3 bytes * `config.expectedMrEnclave` *(optional)*: `` `0x${string}` `` - Expected enclave measurement * `config.expectedMrSigner` *(optional)*: `` `0x${string}` `` - Expected signer measurement * `config.minSecurityVersion` *(optional)*: `number` - Minimum allowed ISV SVN ```typescript Example theme={null} import { verifyAttestation } from "@piplabs/cdr-sdk"; const result = await verifyAttestation(report, { expectedMrEnclave: "0x51c08cf3...", expectedMrSigner: "0xa6f9d44c...", minSecurityVersion: 1, }); if (!result.valid) { console.error(result.error); } ``` `verifyAttestation()` performs client-side field checks. The quote signature chain itself is verified on-chain; this helper is an extra allowlist / policy check for SDK consumers. *** ### parseSgxQuote Parses the key fields from an SGX DCAP Quote v3 without applying any policy. | Function | Type | | --------------- | ------------------------------------------------------------------ | | `parseSgxQuote` | `(report: Uint8Array) => { mrEnclave, mrSigner, securityVersion }` | Parameters: * `report`: `Uint8Array` - Raw SGX DCAP Quote v3 bytes Returns: * `mrEnclave`: `` `0x${string}` `` - Parsed enclave measurement * `mrSigner`: `` `0x${string}` `` - Parsed signer measurement * `securityVersion`: `number` - Parsed ISV SVN ```typescript Example theme={null} import { parseSgxQuote } from "@piplabs/cdr-sdk"; const fields = parseSgxQuote(report); console.log(fields.mrEnclave); console.log(fields.mrSigner); console.log(fields.securityVersion); ``` *** ### uuidToLabel Derives a deterministic 32-byte label from a vault UUID. Used to bind ciphertext to a specific vault. | Function | Type | | ------------- | ------------------------------ | | `uuidToLabel` | `(uuid: number) => Uint8Array` | Parameters: * `uuid`: `number` - The vault UUID (uint32) Returns a 32-byte `Uint8Array`: 28 zero bytes followed by the 4-byte big-endian UUID. ```typescript Example theme={null} import { uuidToLabel } from "@piplabs/cdr-sdk"; const label = uuidToLabel(42); // Uint8Array(32) [0, 0, ..., 0, 0, 0, 0, 42] ``` # Observer Source: https://docs.datafdn.org/sdk-reference/cdr/observer Read-only methods for querying CDR vault data, fees, and DKG state. ## Observer The `Observer` sub-client is always available, even without a `walletClient`. It provides read-only access to CDR and DKG state. ```typescript theme={null} const observer = client.observer; ``` The Observer reads from two backends: CDR contract state (vaults, fees, `maxEncryptedDataSize`, operational threshold) over the EVM `publicClient`, and DKG state (active round, global public key, threshold, validators, attestations) over the DATA Foundation API REST endpoint configured by `apiUrl`. Round-keyed DKG snapshots are cached for the lifetime of the Observer for rounds in the stable Active and Ended stages, with in-flight request deduplication. `getActiveRound()` always re-fetches, since the active round can transition at any time. ### Methods * getVault * getAllocateFee * getWriteFee * getReadFee * getMaxEncryptedDataSize * getOperationalThreshold * getActiveRound * getGlobalPubKey * getParticipantCount * getThreshold * getThresholdAt * getRegisteredValidators * getValidatorAttestations *** ### getVault Fetch a vault's on-chain data by UUID. | Method | Type | | ---------- | ---------------------------------- | | `getVault` | `(uuid: number) => Promise` | Parameters: * `uuid`: The vault's unique identifier (uint32) ```typescript Example theme={null} const vault = await client.observer.getVault(42); console.log(vault.uuid); // 42 console.log(vault.updatable); // false console.log(vault.writeConditionAddr); // "0x..." console.log(vault.readConditionAddr); // "0x..." console.log(vault.encryptedData); // "0x..." (hex-encoded TDH2 ciphertext) ``` ```typescript Vault theme={null} interface Vault { uuid: number; updatable: boolean; writeConditionAddr: `0x${string}`; readConditionAddr: `0x${string}`; writeConditionData: `0x${string}`; readConditionData: `0x${string}`; encryptedData: `0x${string}`; } ``` *** ### getAllocateFee Returns the current fee (in wei) required to allocate a new vault. | Method | Type | | ---------------- | ----------------------- | | `getAllocateFee` | `() => Promise` | ```typescript Example theme={null} const fee = await client.observer.getAllocateFee(); console.log(`Allocate fee: ${fee} wei`); ``` *** ### getWriteFee Returns the current fee (in wei) required to write data to a vault. | Method | Type | | ------------- | ----------------------- | | `getWriteFee` | `() => Promise` | ```typescript Example theme={null} const fee = await client.observer.getWriteFee(); console.log(`Write fee: ${fee} wei`); ``` *** ### getReadFee Returns the current fee (in wei) required to submit a read request. | Method | Type | | ------------ | ----------------------- | | `getReadFee` | `() => Promise` | ```typescript Example theme={null} const fee = await client.observer.getReadFee(); console.log(`Read fee: ${fee} wei`); ``` *** ### getMaxEncryptedDataSize Returns the maximum encrypted payload size (in bytes) supported by the on-chain vault path. The CDR contract treats this as a constant, so it is cached for the lifetime of the Observer. | Method | Type | | ------------------------- | ----------------------- | | `getMaxEncryptedDataSize` | `() => Promise` | ```typescript Example theme={null} const maxSize = await client.observer.getMaxEncryptedDataSize(); console.log(`Max encrypted payload size: ${maxSize} bytes`); ``` *** ### getOperationalThreshold Returns the DKG operational threshold as a basis-points constant read from the DKG contract (e.g., `667` = 66.7%). | Method | Type | | ------------------------- | ----------------------- | | `getOperationalThreshold` | `() => Promise` | ```typescript Example theme={null} const threshold = await client.observer.getOperationalThreshold(); console.log(`Threshold: ${Number(threshold) / 10}%`); // e.g. "66.7%" ``` *** ### getActiveRound Returns the currently active DKG round number. Always hits the DATA Foundation API REST endpoint, since the active round can transition at any time, so it is never served from cache. | Method | Type | | ---------------- | ----------------------- | | `getActiveRound` | `() => Promise` | ```typescript Example theme={null} const round = await client.observer.getActiveRound(); console.log(`Active DKG round: ${round}`); ``` *** ### getGlobalPubKey Returns the DKG global public key from the active round, used for TDH2 encryption. This is an Ed25519 point with a 2-byte curve-code prefix (`0x043f`), 34 bytes total, so it can be passed directly to the WASM TDH2 functions. | Method | Type | | ----------------- | --------------------------- | | `getGlobalPubKey` | `() => Promise` | ```typescript Example theme={null} const globalPubKey = await client.observer.getGlobalPubKey(); console.log(globalPubKey.length); // 34 ``` *** ### getParticipantCount Returns the number of validators selected to participate in the active DKG round. | Method | Type | | --------------------- | ----------------------- | | `getParticipantCount` | `() => Promise` | ```typescript Example theme={null} const count = await client.observer.getParticipantCount(); console.log(`Participating validators: ${count}`); ``` *** ### getThreshold Returns the absolute threshold for the currently active DKG round: the minimum number of partial decryptions needed to combine. If `minThresholdRatio` is set on the client, the effective value is `max(network.threshold, ceil(participants * minThresholdRatio))`. | Method | Type | | -------------- | ----------------------- | | `getThreshold` | `() => Promise` | ```typescript Example theme={null} const threshold = await client.observer.getThreshold(); console.log(`Need ${threshold} partials to decrypt`); ``` Use this for UI / status display. Do **not** use it to validate a specific partial-decryption bucket from `collectPartials`, since a bucket carries its own round, which can differ from the active round during DKG rollover. Use `getThresholdAt` for that. *** ### getThresholdAt Returns the absolute threshold for a specific DKG round, computed against that round's own participant total. Used internally by `collectPartials` so a DKG rollover mid-poll does not measure a bucket against the wrong round. | Method | Type | | ---------------- | ------------------------------------ | | `getThresholdAt` | `(round: number) => Promise` | Parameters: * `round`: The DKG round number to compute the threshold for ```typescript Example theme={null} const threshold = await client.observer.getThresholdAt(4); console.log(`Round 4 threshold: ${threshold}`); ``` *** ### getRegisteredValidators Returns a map of validator address → `commPubKey` bytes for the given DKG round (defaults to the active round). Only includes validators with `Finalized` status. The `commPubKey` is the secp256k1 public key the validator's TEE uses to sign partial decryption responses. | Method | Type | | ------------------------- | ------------------------------------------------------------------- | | `getRegisteredValidators` | `(params?: { round?: number }) => Promise>` | Parameters: * `params.round` *(optional)*: DKG round number. Defaults to the active round. ```typescript Example theme={null} const validators = await client.observer.getRegisteredValidators(); for (const [address, commPubKey] of validators) { console.log(address, commPubKey.length); } ``` *** ### getValidatorAttestations Returns a map of validator address → `enclaveReport` (raw SGX quote bytes) for the given DKG round (defaults to the active round). Only includes validators with `Finalized` status. Shares a per-round cache with `getRegisteredValidators`. | Method | Type | | -------------------------- | ------------------------------------------------------------------- | | `getValidatorAttestations` | `(params?: { round?: number }) => Promise>` | Parameters: * `params.round` *(optional)*: DKG round number. Defaults to the active round. ```typescript Example theme={null} const attestations = await client.observer.getValidatorAttestations(); for (const [address, enclaveReport] of attestations) { console.log(address, enclaveReport.length); } ``` Use with `verifyAttestation()` to verify each validator's TEE enclave before trusting their partial decryptions. # CDR SDK Reference Overview Source: https://docs.datafdn.org/sdk-reference/cdr/overview A detailed description of every function in the CDR SDK This reference tracks the Aeneid release of `@piplabs/cdr-sdk` (`v0.2.1`), available on npm. The CDR SDK (`@piplabs/cdr-sdk`) provides a TypeScript client for interacting with the DATA Foundation's Confidential Data Rails system. It handles threshold encryption, vault management, and on-chain access control. | Language | Package | GitHub | | --------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | TypeScript | [npm](https://www.npmjs.com/package/@piplabs/cdr-sdk) | [Code](https://github.com/piplabs/cdr-sdk) | *** Learn CDR through a series of tutorials with the CDR SDK Integration Guide. ## CDRClient The main entry point. Provides access to three sub-clients: ```typescript theme={null} import { CDRClient } from "@piplabs/cdr-sdk"; const client = new CDRClient({ network: "testnet", publicClient, // viem PublicClient walletClient, // optional viem WalletClient apiUrl: "http://172.192.41.96:1317", // DATA Foundation API REST endpoint // minThresholdRatio: 0.67, // optional threshold override, in [0, 1] }); client.observer; // read-only queries client.uploader; // encryption & vault allocation client.consumer; // decryption & read requests ``` ## Current Surface Area * `observer`: vaults, fees, DKG state, validator registrations, and validator attestations * `uploader`: `uploadCDR`, `uploadFile`, `allocate`, `write`, and `encryptDataKey` * `consumer`: `accessCDR`, `downloadFile`, `read`, `collectPartials`, and `decryptDataKey` * `crypto`: low-level TDH2, ECIES, and SGX attestation verification helpers The client also exposes high-level aliases: * `createVault` as an alias for `uploadCDR` * `readVault` as an alias for `accessCDR` * `createFileVault` as an alias for `uploadFile` * `readFileVault` as an alias for `downloadFile` ## State Backends The client reads from two backends: | Backend | Configured by | Purpose | | ------------------------ | -------------- | ------------------------------------------------------------------------------- | | EVM | `publicClient` | CDR contract state: vaults, fees, `maxEncryptedDataSize`, operational threshold | | DATA Foundation API REST | `apiUrl` | DKG state: active round, global public key, threshold, validators, attestations | The `apiUrl` is a required parameter. See [Runtime Configuration](/developers/cdr-sdk/advanced-configuration#dkg-state-and-the-data-foundation-api-endpoint) for operational guidance and the optional `minThresholdRatio` override. ## Attestation Utilities The SDK also exposes SGX helper functions in the crypto module: * `parseSgxQuote()` to read `MRENCLAVE`, `MRSIGNER`, and `securityVersion` from a quote * `verifyAttestation()` to validate those fields against your expected values Use them together with `observer.getValidatorAttestations()` when your application wants an explicit validator enclave allowlist check. ## Sub-Clients Read-only queries for vault data, fees, and DKG state. Encrypt data, upload encrypted files, and write to CDR vaults. Request decryption, download encrypted files, and recover plaintext. ## Crypto Utilities Low-level TDH2 and ECIES cryptographic primitives. # Uploader Source: https://docs.datafdn.org/sdk-reference/cdr/uploader Methods for encrypting data and writing it to CDR vaults. ## Uploader The `Uploader` sub-client handles vault allocation, TDH2 encryption, and writing encrypted data on-chain. Requires a `walletClient`. ```typescript theme={null} const uploader = client.uploader; ``` Use `uploadCDR()` for small secrets stored directly in the vault and `uploadFile()` when the encrypted bytes should live in an external storage backend. The SDK also exposes `createVault` as an alias for `uploadCDR`, and `createFileVault` as an alias for `uploadFile`. ### Methods * uploadCDR * uploadFile * allocate * write * encryptDataKey *** ### uploadCDR High-level method that allocates a vault, encrypts your data, and writes the ciphertext in a single call. | Method | Type | | ----------- | --------------------------------------------------------- | | `uploadCDR` | `(params: UploadCDRParams) => Promise` | Parameters: * `params.dataKey`: `Uint8Array` - The secret payload bytes to encrypt. Despite the name, this can be arbitrary data, not only a cryptographic key. * `params.globalPubKey` *(optional)*: `Uint8Array` - The DKG global public key (from `observer.getGlobalPubKey()`). Auto-queried via the Observer if omitted. * `params.updatable`: `boolean` - Whether the vault can be rewritten after initial write * `params.writeConditionAddr`: `` `0x${string}` `` - Address of the write condition contract * `params.readConditionAddr`: `` `0x${string}` `` - Address of the read condition contract * `params.writeConditionData`: `` `0x${string}` `` - ABI-encoded data passed to the write condition * `params.readConditionData`: `` `0x${string}` `` - ABI-encoded data passed to the read condition * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to conditions during write * `params.allocateFeeOverride` *(optional)*: `bigint` - Skip fee query and use this value * `params.writeFeeOverride` *(optional)*: `bigint` - Skip fee query and use this value ```typescript Example theme={null} import { encodeAbiParameters } from "viem"; // DATA Foundation license-gated pattern: only the uploader can write, and only callers // holding a license token for `ipId` can read. See // /developers/cdr-sdk/ip-asset-vaults for the full setup. const OWNER_WRITE_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B"; const LICENSE_READ_CONDITION = "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3"; const LICENSE_TOKEN = "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC"; const writeConditionData = encodeAbiParameters( [{ type: "address" }], [walletClient.account!.address], ); const readConditionData = encodeAbiParameters( [{ type: "address" }, { type: "address" }], [LICENSE_TOKEN, ipId], ); const globalPubKey = await client.observer.getGlobalPubKey(); const dataKey = new TextEncoder().encode("my secret"); const { uuid, ciphertext, txHashes } = await client.uploader.uploadCDR({ dataKey, globalPubKey, updatable: false, writeConditionAddr: OWNER_WRITE_CONDITION, readConditionAddr: LICENSE_READ_CONDITION, writeConditionData, readConditionData, accessAuxData: "0x", }); console.log(`Vault UUID: ${uuid}`); console.log(`Allocate tx: ${txHashes.allocate}`); console.log(`Write tx: ${txHashes.write}`); ``` `OwnerWriteCondition` only implements `checkWriteCondition`, so it cannot be used as `readConditionAddr`. For an owner-only flow where the same wallet encrypts and decrypts, use the low-level `allocate()` example below with your wallet (EOA) address as both conditions and `skipConditionValidation: true`. Keep `uploadCDR()` payloads small enough that the resulting TDH2 ciphertext fits the vault limit (`observer.getMaxEncryptedDataSize()`, which is 1024 bytes on Aeneid). ```typescript UploadCDRResponse theme={null} interface UploadCDRResponse { uuid: number; ciphertext: TDH2Ciphertext; txHashes: { allocate: `0x${string}`; write: `0x${string}`; }; } ``` *** ### uploadFile High-level method that encrypts file bytes locally, uploads the encrypted blob through a `StorageProvider`, and writes the encrypted file key plus content pointer to CDR in one call. Parameters: * `params.content`: `Uint8Array` - File bytes to encrypt and upload * `params.storageProvider`: `StorageProvider` - Backend used for upload and download * `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key. Auto-queried via the Observer if omitted. * `params.updatable`: `boolean` - Whether the vault can be rewritten * `params.writeConditionAddr`: `` `0x${string}` `` - Address of the write condition contract * `params.readConditionAddr`: `` `0x${string}` `` - Address of the read condition contract * `params.writeConditionData`: `` `0x${string}` `` - ABI-encoded write condition data * `params.readConditionData`: `` `0x${string}` `` - ABI-encoded read condition data * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to conditions during write * `params.pin` *(optional)*: `boolean` - Whether the storage provider should pin the uploaded blob * `params.allocateFeeOverride` *(optional)*: `bigint` - Skip the allocate fee query * `params.writeFeeOverride` *(optional)*: `bigint` - Skip the write fee query ```typescript Example theme={null} import { HeliaProvider } from "@piplabs/cdr-sdk"; import { readFile } from "node:fs/promises"; import { createHelia } from "helia"; import { unixfs } from "@helia/unixfs"; import { CID } from "multiformats/cid"; import { encodeAbiParameters } from "viem"; const helia = await createHelia(); const storage = new HeliaProvider({ helia, unixfs: unixfs(helia), CID: (s) => CID.parse(s), }); // DATA Foundation license-gated pattern: see /developers/cdr-sdk/ip-asset-vaults for // the full license setup. For an owner-only file flow, use the low-level // `allocate()` path with your wallet (EOA) address as both conditions. const OWNER_WRITE_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B"; const LICENSE_READ_CONDITION = "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3"; const LICENSE_TOKEN = "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC"; const writeConditionData = encodeAbiParameters( [{ type: "address" }], [walletClient.account!.address], ); const readConditionData = encodeAbiParameters( [{ type: "address" }, { type: "address" }], [LICENSE_TOKEN, ipId], ); const fileBytes = await readFile("./example.pdf"); const globalPubKey = await client.observer.getGlobalPubKey(); const { uuid, cid } = await client.uploader.uploadFile({ content: new Uint8Array(fileBytes), storageProvider: storage, globalPubKey, updatable: false, writeConditionAddr: OWNER_WRITE_CONDITION, readConditionAddr: LICENSE_READ_CONDITION, writeConditionData, readConditionData, accessAuxData: "0x", }); console.log(`Vault UUID: ${uuid}`); console.log(`Stored CID: ${cid}`); ``` `HeliaProvider` is the only storage backend fully tested on Aeneid in the current release. `GatewayProvider`, `StorachaProvider`, and `SynapseProvider` are implemented but were not yet end-to-end validated in the release run. `uploadFile()` keeps the file bytes off-chain. The vault stores a TDH2 ciphertext of a small JSON payload containing `{ cid, key }`. In browser code, pass file bytes from `new Uint8Array(await file.arrayBuffer())` instead of `readFile(...)`. *** ### allocate Creates a new CDR vault on-chain with the specified access control conditions. | Method | Type | | ---------- | ------------------------------------------------------- | | `allocate` | `(params: AllocateParams) => Promise` | Parameters: * `params.updatable`: `boolean` - Whether the vault can be rewritten * `params.writeConditionAddr`: `` `0x${string}` `` - Write condition contract address * `params.readConditionAddr`: `` `0x${string}` `` - Read condition contract address * `params.writeConditionData`: `` `0x${string}` `` - ABI-encoded write condition data * `params.readConditionData`: `` `0x${string}` `` - ABI-encoded read condition data * `params.feeOverride` *(optional)*: `bigint` - Skip fee query * `params.skipConditionValidation` *(optional)*: `boolean` - Skip interface validation when intentionally using an EOA condition address ```typescript Example theme={null} const userAddress = walletClient.account!.address; const { txHash, uuid } = await client.uploader.allocate({ updatable: false, writeConditionAddr: userAddress, readConditionAddr: userAddress, writeConditionData: "0x", readConditionData: "0x", skipConditionValidation: true, }); console.log(`Vault ${uuid} allocated at tx: ${txHash}`); ``` `uploadCDR()` and `uploadFile()` do not expose `skipConditionValidation`, so use a real condition contract with those high-level helpers. ```typescript AllocateResponse theme={null} interface AllocateResponse { txHash: `0x${string}`; uuid: number; // parsed from VaultAllocated event } ``` *** ### write Writes encrypted data to an existing vault. The caller must satisfy the vault's write condition. | Method | Type | | ------- | ------------------------------------------------- | | `write` | `(params: WriteParams) => Promise` | Parameters: * `params.uuid`: `number` - The vault UUID * `params.accessAuxData`: `` `0x${string}` `` - Auxiliary data passed to the write condition * `params.encryptedData`: `` `0x${string}` `` - Hex-encoded TDH2 ciphertext * `params.feeOverride` *(optional)*: `bigint` - Skip fee query ```typescript Example theme={null} import { toHex } from "viem"; const { txHash } = await client.uploader.write({ uuid: 42, accessAuxData: "0x", encryptedData: toHex(ciphertext.raw), }); ``` ```typescript WriteResponse theme={null} interface WriteResponse { txHash: `0x${string}`; } ``` *** ### encryptDataKey Locally encrypts data using TDH2 threshold encryption. No blockchain interaction. | Method | Type | | ---------------- | ---------------------------------------------------- | | `encryptDataKey` | `(params: EncryptParams) => Promise` | Parameters: * `params.dataKey`: `Uint8Array` - The plaintext data to encrypt * `params.globalPubKey` *(optional)*: `Uint8Array` - DKG global public key (34 bytes). Auto-queried via the Observer if omitted. * `params.label`: `Uint8Array` - 32-byte label binding ciphertext to a vault (use `uuidToLabel(uuid)`) ```typescript Example theme={null} import { uuidToLabel } from "@piplabs/cdr-sdk"; const label = uuidToLabel(uuid); const ciphertext = await client.uploader.encryptDataKey({ dataKey: new TextEncoder().encode("secret"), globalPubKey, label, }); console.log(ciphertext.raw); // Uint8Array - serialized TDH2 ciphertext console.log(ciphertext.label); // Uint8Array - the label used ``` ```typescript TDH2Ciphertext theme={null} interface TDH2Ciphertext { raw: Uint8Array; // serialized ciphertext (cb-mpc format) label: Uint8Array; // 32-byte context binding } ``` # IP Account Source: https://docs.datafdn.org/sdk-reference/ipaccount IPAccountClient allows you to manage IP Account metadata and execute transactions. ## IPAccountClient ### Methods * setIpMetadata * execute * executeWithSig * transferErc20 ### setIpMetadata Sets the metadataURI for an IP asset. | Method | Type | | --------------- | --------------------------------------- | | `setIpMetadata` | `(SetIpMetadataRequest) => Promis` | Parameters: * `request.ipId`: The IP to set the metadata for. * `request.metadataURI`: The metadataURI to set for the IP asset. Should be a URL pointing to metadata that fits the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard). * `request.metadataHash`: The hash of metadata at metadataURI. ```typescript TypeScript theme={null} const txHash = await client.ipAccount.setIpMetadata({ ipId: "0x01", metadataURI: "https://ipfs.io/ipfs/bafkreiardkgvkejqnnkdqp4pamkx2e5bs4lzus5trrw3hgmoa7dlbb6foe", // example hash (not accurate) metadataHash: "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", }); ``` ```typescript Request Type theme={null} export type SetIpMetadataRequest = { ipId: Address; metadataURI: string; metadataHash: Hex; }; ``` ### execute Executes a transaction from the IP Account. | Method | Type | | --------- | --------------------------------------------------------------- | | `execute` | `(IPAccountExecuteRequest) => Promis` | Parameters: * `request.ipId`: The Ip Id to get ip account. * `request.to`: The recipient of the transaction. * `request.value`: The amount of Ether to send. * `request.data`: The data to send along with the transaction. ```typescript Request Type theme={null} export type IPAccountExecuteRequest = { ipId: Address; to: Address; value: number; data: Hex; }; ``` ```typescript Response Type theme={null} export type IPAccountExecuteResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### executeWithSig Executes a transaction from the IP Account. | Method | Type | | ---------------- | --------------------------------------------------------------- | | `executeWithSig` | `(IPAccountExecuteRequest) => Promis` | Parameters: * `request.ipId`: The Ip Id to get ip account. * `request.to`: The recipient of the transaction. * `request.data`: The data to send along with the transaction. * `request.signer`: The signer of the transaction. * `request.deadline`: The deadline of the transaction signature. * `request.signature`: The signature of the transaction, EIP-712 encoded. * `request.value`: \[Optional] The amount of Ether to send. ```typescript Request Type theme={null} export type IPAccountExecuteWithSigRequest = { ipId: Address; to: Address; data: Hex; signer: Address; deadline: number | bigint | string; signature: Address; value?: number | bigint | string; }; ``` ```typescript Response Type theme={null} export type IPAccountExecuteWithSigResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### transferErc20 Transfers an ERC20 token from the IP Account. | Method | Type | | --------------- | ----------------------------------------------------------------- | | `transferErc20` | `(request: TransferErc20Request) => Promise` | Parameters: * `request.ipId`: The `ipId` of the account * `request.tokens`: The token info to transfer * `request.tokens.address`: The address of the ERC20 token including WIP and standard ERC20. * `request.tokens.amount`: The amount of tokens to transfer * `request.tokens.target`: The address of the recipient. ```typescript Request Type theme={null} export type TransferErc20Request = { ipId: Address; tokens: { address: Address; amount: bigint | number; target: Address; }[]; }; ``` ```typescript Response Type theme={null} export type TransactionResponse = { txHash: Hex; /** Transaction receipt, only available if waitForTransaction is set to true */ receipt?: TransactionReceipt; }; ``` # IP Asset Source: https://docs.datafdn.org/sdk-reference/ipasset IPAssetClient allows you to create, get, and list IP Assets within the DATA Foundation. ## IPAssetClient ### Methods * registerIpAsset * registerDerivativeIpAsset * linkDerivative ### registerIpAsset Register your IP as an [🧩 IP Asset](/concepts/ip-asset). It supports the following workflows: 1. Register an IP Asset 1a. register an existing NFT as an IP Asset 1b. mint a new NFT and register as an IP Asset 2. Attach license terms to the IP Asset 3. Distribute royalty tokens Note that this function will also set the underlying NFT's `tokenUri` to whatever is passed under `ipMetadata.nftMetadataURI`. | Method | Type | | ----------------- | ----------------------------------------------------------------------- | | `registerIpAsset` | `(request: RegisterIpAssetRequest) => Promise` | Parameters: * `request.nft`: You have two options here * `{ type: "minted", nftContract: Address, tokenId: number | bigint }`: Register an existing NFT as an IP Asset. This is typically the harder option because you need to already have an NFT minted. * `{ type: "mint", spgNftContract: Address, recipient?: Address, allowDuplicates?: boolean }`: Mint a new NFT and register as an IP Asset. This is typically the easier option because you don't need to worry about already having an NFT minted. Just create an spgNftContract, or use a default one, to mint for you. * `request.licenseTermsData`: If you want to attach license terms. * `request.licenseTermsData.terms`: The [license terms](/concepts/programmable-ip-license/pil-terms) to attach to the IP Asset. * `request.licenseTermsData.licensingConfig`: The [licensing config](/concepts/licensing-module/license-config) to attach to the IP Asset. * `request.licenseTermsData.maxLicenseTokens`: The max number of license tokens that can be minted from this license term. * `request.royaltyShares`: If you want to distribute royalty tokens out. * `request.royaltyShares.recipient`: The address of the recipient of the royalty shares. * `request.royaltyShares.percentage`: The percentage of the royalty shares. * `request.ipMetadata`: The metadata of the IP Asset * `request.ipMetadata.ipMetadataURI`: The URI of the metadata for the IP. * `request.ipMetadata.ipMetadataHash`: The hash of the metadata for the IP. * `request.ipMetadata.nftMetadataURI`: The URI of the metadata for the NFT. * `request.ipMetadata.nftMetadataHash`: The hash of the metadata for the IP NFT. * `request.deadline`: The deadline for the signature in milliseconds. **Defaults to 1000**. ```typescript Example theme={null} import { PILFlavor, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk"; import { toHex } from "viem"; // an example of an SPG NFT contract address // you can create one via `client.nftClient.createNFTCollection` const spgNftContract = "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc"; const response = await client.ipAsset.registerIpAsset({ nft: { type: "mint", spgNftContract: spgNftContract }, licenseTermsData: [ { terms: PILFlavor.creativeCommonsAttribution({ currency: WIP_TOKEN_ADDRESS, // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", }), }, { terms: PILFlavor.commercialRemix({ defaultMintingFee: 10000n, commercialRevShare: 20, // 20% currency: WIP_TOKEN_ADDRESS, // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", }), maxLicenseTokens: 100, }, ], royaltyShares: [ { recipient: "0x123...", percentage: 10, }, ], ipMetadata: { ipMetadataURI: "https://ipfs.io/ipfs/bafkreiardkgvkejqnnkdqp4pamkx2e5bs4lzus5trrw3hgmoa7dlbb6foe", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "https://ipfs.io/ipfs/bafkreicexrvs2fqvwblmgl3gnwiwh76pfycvfs66ck7w4s5omluyhti2kq", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), }, }); console.log( `Root IPA created at transaction hash ${response.txHash}, IPA ID: ${response.ipId}` ); ``` ```typescript RegisterIpAssetRequest theme={null} export type RegisterRequest = { nft: MintedNFT | MintNFT; // attach license terms licenseTermsData?: LicenseTermsDataInput[]; // sent royalty tokens out royaltyShares?: RoyaltyShare[]; // add metadata ipMetadata?: { ipMetadataURI: string; ipMetadataHash: Hex; nftMetadataURI: string; nftMetadataHash: Hex; }; deadline?: number | bigint; }; type MintedNFT = { type: "minted"; /** The address of the NFT contract. */ nftContract: Address; tokenId: number | bigint; }; type MintNFT = { type: "mint"; /** * The address of the SPG NFT contract. * You can create one via `client.nftClient.createNFTCollection`. */ spgNftContract: Address; /** * The address to receive the NFT. * Defaults to client's wallet address if not provided. */ recipient?: Address; /** * Set to true to allow minting an NFT with a duplicate metadata hash. * @default true */ allowDuplicates?: boolean; }; type LicenseTermsDataInput = { terms: LicenseTerms; licensingConfig?: LicensingConfig; /** * The max number of license tokens that can be minted from this license term. * * - When not specified, there is no limit on license token minting * - When specified, minting is capped at this value and the TotalLicenseTokenLimitHook * is automatically configured as the licensingConfig.licensingHook */ maxLicenseTokens?: number | bigint; }; type RoyaltyShare = { recipient: Address; /** * The percentage of the total royalty share. For example, a * value of 10 represents 10% of max royalty shares, which is 10,000,000. * @example 10 */ percentage: number | bigint; }; ``` ```typescript RegisterIpAssetResponse theme={null} export type RegisterIpResponse = { txHash?: Hex; // ipId of the newly registered IP Asset ipId: Address; // if license terms were attached licenseTermsIds?: bigint[]; // other fields based on input // ... }; ``` ### registerDerivativeIpAsset Register an IP as a derivative of another IP Asset. This function allows you to use an existing license token to register as derivative, or it will mint one for you. To register an IP as a derivative, it must be an IP Asset itself. So this function allows you to register an existing NFT as an IP Asset (which will be the derivative), or mint a new NFT for you (and register it as a derivative). | Method | Type | | --------------------------- | --------------------------------------------------------------------------------- | | `registerDerivativeIpAsset` | `(request: RegisterDerivativeIpRequest) => Promise` | Parameters: * `request.nft`: You have two options here * `{ type: "minted", nftContract: Address, tokenId: number | bigint }`: Register an existing NFT as an IP Asset. This is typically the harder option because you need to already have an NFT minted. * `{ type: "mint", spgNftContract: Address, recipient?: Address, allowDuplicates?: boolean }`: Mint a new NFT and register as an IP Asset. This is typically the easier option because you don't need to worry about already having an NFT minted. Just create an spgNftContract, or use a default one, to mint for you. * `request.licenseTokenIds`: If you want to use a license token to register as derivative. * `request.derivData`: If you want to mint a license token for you. * `request.derivData.parentIpIds`: The IDs of the parent IPs to link the registered derivative IP. * `request.derivData.licenseTermsIds`: The IDs of the license terms to be used for the linking. * `request.royaltyShares`: If you want to distribute royalty tokens out. * `request.royaltyShares.recipient`: The address of the recipient of the royalty shares. * `request.royaltyShares.percentage`: The percentage of the royalty shares. * `request.maxRts`: The maximum number of royalty tokens that can be distributed to the external royalty policies. Must be between 0 and 100,000,000. **Recommended for simplicity: 100\_000\_000** * `request.ipMetadata`: The metadata of the IP Asset * `request.ipMetadata.ipMetadataURI`: The URI of the metadata for the IP. * `request.ipMetadata.ipMetadataHash`: The hash of the metadata for the IP. * `request.ipMetadata.nftMetadataURI`: The URI of the metadata for the NFT. * `request.ipMetadata.nftMetadataHash`: The hash of the metadata for the IP NFT. * `request.deadline`: The deadline for the signature in milliseconds. **Defaults to 1000**. ```typescript Example theme={null} import { toHex } from "viem"; // an example of an SPG NFT contract address // you can create one via `client.nftClient.createNFTCollection` const spgNftContract = "0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc"; // an example of a parent IP ID const parentIpId = "0x456..."; // an example of a commercial remix license terms ID const commercialRemixLicenseTermsId = 5; const response = await client.ipAsset.registerDerivativeIpAsset({ nft: { type: "mint", spgNftContract }, derivData: { parentIpIds: [parentIpId], licenseTermsIds: [commercialRemixLicenseTermsId], }, ipMetadata: { ipMetadataURI: "https://ipfs.io/ipfs/bafkreiardkgvkejqnnkdqp4pamkx2e5bs4lzus5trrw3hgmoa7dlbb6foe", ipMetadataHash: toHex("test-metadata-hash", { size: 32 }), nftMetadataURI: "https://ipfs.io/ipfs/bafkreicexrvs2fqvwblmgl3gnwiwh76pfycvfs66ck7w4s5omluyhti2kq", nftMetadataHash: toHex("test-nft-metadata-hash", { size: 32 }), }, royaltyShares: [ { recipient: "0x123...", percentage: 10, }, ], }); console.log( `Derivative IPA linked to parent at transaction hash ${response.txHash}` ); ``` ```typescript Request Type theme={null} export type RegisterDerivativeIpRequest = { nft: MintedNFT | MintNFT; /** The IDs of the license tokens to be burned for linking the IP to parent IPs. * Must be provided together with `maxRts`. */ licenseTokenIds?: number[] | bigint[]; /** * The derivative data containing parent IP information and licensing terms. * This will be used to mint a license token for you. * @remarks * This should not be provided if you are using a license token to register as derivative. * Because the license token is already minted. */ derivData?: DerivativeDataInput; /** * Authors of the IP and their shares of the royalty tokens. * * @remarks * Royalty shares can only be specified if `derivData` is also provided. * This ensures that royalty distribution is always associated with derivative IP registration. * The shares define how royalty tokens will be distributed among IP authors. */ royaltyShares?: RoyaltyShare[]; ipMetadata?: { ipMetadataURI: string; ipMetadataHash: Hex; nftMetadataURI: string; nftMetadataHash: Hex; }; /** * The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). * Must be provided together with `licenseTokenIds`. * Just use 100_000_000 for simplicity. */ maxRts?: number; /** * The deadline for the signature in seconds. * @default 1000 */ deadline?: number | bigint; }; type MintedNFT = { type: "minted"; /** The address of the NFT contract. */ nftContract: Address; tokenId: number | bigint; }; type MintNFT = { type: "mint"; /** * The address of the SPG NFT contract. * You can create one via `client.nftClient.createNFTCollection`. */ spgNftContract: Address; /** * The address to receive the NFT. * Defaults to client's wallet address if not provided. */ recipient?: Address; /** * Set to true to allow minting an NFT with a duplicate metadata hash. * @default true */ allowDuplicates?: boolean; }; export type DerivativeDataInput = { parentIpIds: Address[]; /** The IDs of the license terms that the parent IP supports. */ licenseTermsIds: bigint[] | number[]; /** * The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. * @default 0 */ maxMintingFee?: bigint | number; /** * The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). * @default 100_000_000 */ maxRts?: number; /** * The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100_000_000). * @default 100 */ maxRevenueShare?: number; /** * The address of the license template. * @default Defaults to https://docs.datafdn.org/developers/deployed-smart-contracts * PILicenseTemplate address if not provided. */ licenseTemplate?: Address; }; type RoyaltyShare = { recipient: Address; /** * The percentage of the total royalty share. For example, a * value of 10 represents 10% of max royalty shares, which is 10,000,000. * @example 10 */ percentage: number | bigint; }; ``` ```typescript Response Type theme={null} export type RegisterDerivativeIpResponse = { txHash?: Hex; ipId?: Address; }; ``` ### linkDerivative Link an existing derivative IP to a parent IP. | Method | Type | | ---------------- | --------------------------------------------------------------------- | | `linkDerivative` | `(request: LinkDerivativeRequest) => Promise` | Parameters: * `request.childIpId`: The ID of the child IP. * `request.licenseTermIds`: The IDs of the license terms to be used for the linking. * `request.parentIpIds`: The IDs of the parent IPs. ```typescript TypeScript theme={null} const response = await client.ipAsset.linkDerivative({ childIpId: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", parentIpIds: ["0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba"], licenseTermsIds: [5], }); console.log( `Derivative IPA linked to parent at transaction hash ${response.txHash}` ); ``` ```typescript Request Type theme={null} export type LinkDerivativeRequest = { parentIpIds: Address[]; childIpId: Address; /** The IDs of the license terms that the parent IP supports. */ licenseTermsIds: number[] | bigint[]; /** * The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. * @default 0 */ maxMintingFee?: bigint | number; /** * The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). * @default 100_000_000 */ maxRts?: number; /** * The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100_000_000). * @default 100 */ maxRevenueShare?: number; /** * The address of the license template. * Defaults to {@link https://docs.datafdn.org/docs/programmable-ip-license | License Template} address if not provided. */ licenseTemplate?: Address; }; ``` ```typescript Response Type theme={null} export type LinkDerivativeResponse = { txHash?: Hex; }; ``` * `request.childIpId`: The ID of the child IP. * `request.licenseTokenIds`: The IDs of the license tokens to be used for the linking. ```typescript TypeScript theme={null} const response = await client.ipAsset.linkDerivative({ childIpId: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", licenseTokenIds: [1115], }); console.log( `Derivative IPA linked to parent at transaction hash ${response.txHash}` ); ``` ```typescript Request Type theme={null} export type LinkDerivativeRequest = { /** The derivative IP ID. */ childIpId: Address; /** The IDs of the license tokens. */ licenseTokenIds: number[] | bigint[]; /** * The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). * @default 100_000_000 */ maxRts?: number; }; ``` ```typescript Response Type theme={null} export type LinkDerivativeResponse = { txHash?: Hex; }; ``` # License Source: https://docs.datafdn.org/sdk-reference/license LicenseClient allows you to manage license terms and tokens within the DATA Foundation. ## LicenseClient ### Methods * attachLicenseTerms * mintLicenseTokens * registerPILTerms * registerPilTermsAndAttach * registerNonComSocialRemixingPIL * registerCommercialUsePIL * registerCommercialRemixPIL * registerCreativeCommonsAttributionPIL * getLicenseTerms * predictMintingLicenseFee * setLicensingConfig * getLicensingConfig * setMaxLicenseTokens ### attachLicenseTerms Attaches license terms to an IP. | Method | Type | | -------------------- | -------------------------------------------------------------------- | | `attachLicenseTerms` | `(request: AttachLicenseTermsRequest) => AttachLicenseTermsResponse` | Parameters: * `request.ipId`: The address of the IP to which the license terms are attached. * `request.licenseTermsId`: The ID of the license terms. * `request.licenseTemplate`: \[Optional] The address of the license template. ```typescript TypeScript theme={null} const response = await client.license.attachLicenseTerms({ licenseTermsId: "1", ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", }); if (response.success) { console.log( `Attached License Terms to IPA at transaction hash ${response.txHash}.` ); } else { console.log(`License Terms already attached to this IPA.`); } ``` ```typescript Request Type theme={null} export type AttachLicenseTermsRequest = { ipId: Address; licenseTermsId: string | number | bigint; licenseTemplate?: Address; }; ``` ```typescript Response Type theme={null} export type AttachLicenseTermsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### mintLicenseTokens Mints [License Tokens](/concepts/licensing-module/license-token) that give permission to use the IP Asset based on [License Terms](/concepts/licensing-module/license-terms). The license tokens are minted to the `receiver`. Note that a license token can only be minted if the `licenseTermsId` are already attached to the IP Asset, making it a publicly available license. The IP owner can, however, mint a [private license](/concepts/licensing-module/license-token#private-licenses) by minting a license token with a `licenseTermsId` that is not attached to the IP Asset. It might require the caller pay a minting fee, depending on the license terms or configured by the IP owner. The minting fee is paid in the minting fee token specified in the license terms or configured by the IP owner. IP owners can configure the minting fee of their IPs or configure the minting fee module to determine the minting fee. A diagram showing how private licenses are minted. | Method | Type | | ------------------- | --------------------------------------------------------------------------- | | `mintLicenseTokens` | `(request: MintLicenseTokensRequest) => Promise` | Parameters: * `request.licensorIpId`: The licensor IP ID. * `request.licenseTermsId`: The ID of the license terms within the license template. * `request.maxMintingFee`: \[Optional] The maximum minting fee to be paid when minting a license. * `request.maxRevenueShare`: \[Optional] The maximum revenue share to be paid when minting a license. * `request.amount`: \[Optional] The amount of license tokens to mint. * `request.receiver`: \[Optional] The address of the receiver. * `request.licenseTemplate`: \[Optional] The address of the license template. ```typescript TypeScript theme={null} const response = await client.license.mintLicenseTokens({ licenseTermsId: "1", licensorIpId: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", receiver: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", // optional amount: 1, }); console.log( `License Token minted at transaction hash ${response.txHash}, License IDs: ${response.licenseTokenIds}` ); ``` ```typescript Request Type theme={null} export type MintLicenseTokensRequest = { licensorIpId: Address; licenseTermsId: string | number | bigint; licenseTemplate?: Address; maxMintingFee: bigint | string | number; maxRevenueShare: number | string; amount?: number | string | bigint; receiver?: Address; } & WithWipOptions; ``` ```typescript Response Type theme={null} export type MintLicenseTokensResponse = { licenseTokenIds?: bigint[]; receipt?: TransactionReceipt; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### registerPILTerms Registers new license terms and return the ID of the newly registered license terms. | Method | Type | | ------------------ | -------------------------------------------------------------------- | | `registerPILTerms` | `(request: RegisterPILTermsRequest) => Promise` | Parameters: * Expected Parameters: Instead of listing all of the expected parameters here, please see `LicenseTerms` type in [this](https://github.com/thedatafoundation/sdk/blob/main/packages/core-sdk/src/types/resources/license.ts) file. They all come from the [PIL Terms](/concepts/programmable-ip-license/pil-terms). ```typescript TypeScript theme={null} import { LicenseTerms, PILFlavor, WIP_TOKEN_ADDRESS, } from "@story-protocol/core-sdk"; import { zeroAddress, parseEther } from "viem"; // OPTION 1. If you want to specify all the terms const licenseTerms: LicenseTerms = { transferable: false, royaltyPolicy: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts defaultMintingFee: 0n, expiration: 0n, commercialUse: false, commercialAttribution: false, commercializerChecker: zeroAddress, commercializerCheckerData: "0x", commercialRevShare: 10, // 10% commercialRevCeiling: 0n, derivativesAllowed: true, derivativesAttribution: false, derivativesApproval: false, derivativesReciprocal: false, derivativeRevCeiling: 0n, currency: "0x1514000000000000000000000000000000000000", // $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts uri: "", }; const response = await client.license.registerPILTerms({ ...licenseTerms, }); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); // OPTION 2. If you want to use a PIL flavor for convenience const response = await client.license.registerPILTerms( PILFlavor.commercialRemix({ commercialRevShare: 5, defaultMintingFee: parseEther("1"), // 1 $DATA currency: WIP_TOKEN_ADDRESS, }) ); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); ``` ```typescript Request Type theme={null} export type RegisterPILTermsRequest = Omit< LicenseTerms, | "defaultMintingFee" | "expiration" | "commercialRevCeiling" | "derivativeRevCeiling" > & { defaultMintingFee: bigint | string | number; expiration: bigint | string | number; commercialRevCeiling: bigint | string | number; derivativeRevCeiling: bigint | string | number; }; export type LicenseTerms = { /*Indicates whether the license is transferable or not.*/ transferable: boolean; /*The address of the royalty policy contract which required to DATA Foundation in advance.*/ royaltyPolicy: Address; /*The default minting fee to be paid when minting a license.*/ defaultMintingFee: bigint; /*The expiration period of the license.*/ expiration: bigint; /*Indicates whether the work can be used commercially or not.*/ commercialUse: boolean; /*Whether attribution is required when reproducing the work commercially or not.*/ commercialAttribution: boolean; /*Commercializers that are allowed to commercially exploit the work. If zero address, then no restrictions is enforced.*/ commercializerChecker: Address; /*The data to be passed to the commercializer checker contract.*/ commercializerCheckerData: Address; /**Percentage of revenue that must be shared with the licensor. Must be from 0-100.*/ commercialRevShare: number; /*The maximum revenue that can be generated from the commercial use of the work.*/ commercialRevCeiling: bigint; /*Indicates whether the licensee can create derivatives of his work or not.*/ derivativesAllowed: boolean; /*Indicates whether attribution is required for derivatives of the work or not.*/ derivativesAttribution: boolean; /*Indicates whether the licensor must approve derivatives of the work before they can be linked to the licensor IP ID or not.*/ derivativesApproval: boolean; /*Indicates whether the licensee must license derivatives of the work under the same terms or not.*/ derivativesReciprocal: boolean; /*The maximum revenue that can be generated from the derivative use of the work.*/ derivativeRevCeiling: bigint; /*The ERC20 token to be used to pay the minting fee. the token must be registered in story protocol.*/ currency: Address; /*The URI of the license terms, which can be used to fetch the offchain license terms.*/ uri: string; }; ``` ```typescript Response Type theme={null} export type RegisterPILResponse = { licenseTermsId?: bigint; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### registerPilTermsAndAttach Register Programmable IP License Terms (if unregistered) and attach it to IP. | Method | Type | | --------------------------- | ------------------------------------------------------------------------------------------- | | `registerPilTermsAndAttach` | `(request: RegisterPilTermsAndAttachRequest) => Promise` | Parameters: * `request.ipId`: The ID of the IP. * `request.licenseTermsData[]`: The array of license terms to be attached. * `request.licenseTermsData.terms`: See the [LicenseTerms type](https://github.com/thedatafoundation/sdk/blob/main/packages/core-sdk/src/types/resources/license.ts#L26). * `request.licenseTermsData.licensingConfig`: \[Optional] See the [LicensingConfig type](https://github.com/thedatafoundation/sdk/blob/main/packages/core-sdk/src/types/common.ts#L15). If none provided, it will default to the one shown [here](https://github.com/thedatafoundation/sdk/blob/main/packages/core-sdk/src/utils/validateLicenseConfig.ts). * `request.deadline`: \[Optional] The deadline for the signature in milliseconds. **Defaults to 1000**. ```typescript TypeScript theme={null} import { PILFlavor, WIP_TOKEN_ADDRESS } from "@story-protocol/core-sdk"; import { parseEther } from "viem"; const response = await client.license.registerPilTermsAndAttach({ ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", licenseTermsData: [ { terms: PILFlavor.commercialRemix({ commercialRevShare: 5, defaultMintingFee: parseEther("1"), // 1 $DATA currency: WIP_TOKEN_ADDRESS, }), }, ], }); console.log(`License Terms ${response.licenseTermsId} attached to IP Asset.`); ``` ```typescript Request Type theme={null} export type RegisterPilTermsAndAttachRequest = { ipId: Address; licenseTermsData: LicenseTermsData< RegisterPILTermsRequest, LicensingConfig >[]; deadline?: string | number | bigint; }; ``` ```typescript Response Type theme={null} export type RegisterPilTermsAndAttachResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; licenseTermsIds?: bigint[]; }; ``` ### registerNonComSocialRemixingPIL Convenient function to register a PIL non commercial social remix license to the registry. No reason to call this function. Non-Commercial Social Remixing terms are already registered with `licenseTermdId = 1` in our protocol. There's no reason to register them again. | Method | Type | | --------------------------------- | ------------------------------------------------------------------------------------ | | `registerNonComSocialRemixingPIL` | `(request?: RegisterNonComSocialRemixingPILRequest) => Promise` | Parameters: ```typescript TypeScript theme={null} const response = await client.license.registerNonComSocialRemixingPIL({}); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); ``` ```typescript Request Type theme={null} export type RegisterNonComSocialRemixingPILRequest = {}; ``` ```typescript Response Type theme={null} export type RegisterPILResponse = { licenseTermsId?: bigint; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### registerCommercialUsePIL Convenient function to register a PIL commercial use license to the registry. | Method | Type | | -------------------------- | ---------------------------------------------------------------------------- | | `registerCommercialUsePIL` | `(request: RegisterCommercialUsePILRequest) => Promise` | Parameters: * `request.defaultMintingFee`: The fee to be paid when minting a license. * `request.currency`: The ERC20 token to be used to pay the minting fee and the token must be registered on the DATA Foundation's protocol. * `request.royaltyPolicyAddress`: \[Optional] The address of the royalty policy contract, default value is LAP. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const commercialUseParams = { currency: "0x1514000000000000000000000000000000000000", // $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts defaultMintingFee: parseEther("1"), // 1 $WIP royaltyPolicyAddress: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts }; const response = await client.license.registerCommercialUsePIL({ ...commercialUseParams, }); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); ``` ```typescript Request Type theme={null} export type RegisterCommercialUsePILRequest = { defaultMintingFee: string | number | bigint; currency: Address; royaltyPolicyAddress?: Address; }; ``` ```typescript Response Type theme={null} export type RegisterPILResponse = { licenseTermsId?: bigint; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### registerCommercialRemixPIL Convenient function to register a PIL commercial Remix license to the registry. | Method | Type | | ---------------------------- | ------------------------------------------------------------------------------ | | `registerCommercialRemixPIL` | `(request: RegisterCommercialRemixPILRequest) => Promise` | Parameters: * `request.defaultMintingFee`: The fee to be paid when minting a license. * `request.commercialRevShare`: Percentage of revenue that must be shared with the licensor. * `request.currency`: The ERC20 token to be used to pay the minting fee and the token must be registered on the DATA Foundation's protocol. * `request.royaltyPolicyAddress`: \[Optional] The address of the royalty policy contract, default value is LAP. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const commercialRemixParams = { currency: "0x1514000000000000000000000000000000000000", // $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts defaultMintingFee: parseEther("1"), // 1 $WIP royaltyPolicyAddress: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts commercialRevShare: 10, // 10% }; const response = await client.license.registerCommercialRemixPIL({ ...commercialRemixParams, }); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); ``` ```typescript Request Type theme={null} export type RegisterCommercialRemixPILRequest = { defaultMintingFee: string | number | bigint; commercialRevShare: number; currency: Address; royaltyPolicyAddress?: Address; }; ``` ```typescript Response Type theme={null} export type RegisterPILResponse = { licenseTermsId?: bigint; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### registerCreativeCommonsAttributionPIL Convenient function to register a PIL creative commons attribution license to the registry. | Method | Type | | --------------------------------------- | ----------------------------------------------------------------------------------------- | | `registerCreativeCommonsAttributionPIL` | `(request: RegisterCreativeCommonsAttributionPILRequest) => Promise` | Parameters: * `request.currency`: The ERC20 token to be used to pay the minting fee and the token must be registered on the DATA Foundation's protocol. * `request.royaltyPolicyAddress`: \[Optional] The address of the royalty policy contract, default value is LAP. ```typescript TypeScript theme={null} const response = await client.license.registerCreativeCommonsAttributionPIL({ currency: "0x1514000000000000000000000000000000000000", // $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts royaltyPolicyAddress: "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", // RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts }); console.log( `PIL Terms registered at transaction hash ${response.txHash}, License Terms ID: ${response.licenseTermsId}` ); ``` ```typescript Request Type theme={null} export type RegisterCreativeCommonsAttributionPILRequest = { currency: Address; royaltyPolicyAddress?: Address; }; ``` ```typescript Response Type theme={null} export type RegisterPILResponse = { licenseTermsId?: bigint; txHash?: Hex; encodedTxData?: EncodedTxData; }; ``` ### getLicenseTerms Gets License Terms of the given ID. | Method | Type | | :---------------- | :------------------------------------------------------------------------------------------------- | | `getLicenseTerms` | `(selectedLicenseTermsId: string \| number \| bigint) => PiLicenseTemplateGetLicenseTermsResponse` | Parameters: * `selectedLicenseTermsId`: The ID of the license terms. ```typescript Response Type theme={null} export type PiLicenseTemplateGetLicenseTermsResponse = { terms: { transferable: boolean; royaltyPolicy: Address; defaultMintingFee: bigint; expiration: bigint; commercialUse: boolean; commercialAttribution: boolean; commercializerChecker: Address; commercializerCheckerData: Hex; commercialRevShare: number; commercialRevCeiling: bigint; derivativesAllowed: boolean; derivativesAttribution: boolean; derivativesApproval: boolean; derivativesReciprocal: boolean; derivativeRevCeiling: bigint; currency: Address; uri: string; }; }; ``` ### predictMintingLicenseFee Pre-compute the minting license fee for the given IP and license terms. The function can be used to calculate the minting license fee before minting license tokens. | Method | Type | | -------------------------- | ----------------------------------------------------------------------------------------------- | | `predictMintingLicenseFee` | `(request: PredictMintingLicenseFeeRequest) => LicensingModulePredictMintingLicenseFeeResponse` | Parameters: * `request.licensorIpId`: The IP ID of the licensor. * `request.licenseTermsId`: The ID of the license terms. * `request.amount`: The amount of license tokens to mint. * `request.licenseTemplate`: \[Optional] The address of the license template, default value is Programmable IP License. * `request.receiver`: \[Optional] The address of the receiver, default value is your wallet address. ```typescript Response Type theme={null} export type LicensingModulePredictMintingLicenseFeeResponse = { currencyToken: Address; tokenAmount: bigint; }; ``` ### setLicensingConfig Sets the licensing configuration for a specific license terms of an IP. | Method | Type | | -------------------- | -------------------------------------------------------------------- | | `setLicensingConfig` | `(request: SetLicensingConfigRequest) => SetLicensingConfigResponse` | Parameters: * `request.ipId`: The address of the IP for which the configuration is being set. * `request.licenseTermsId`: The ID of the license terms within the license template. * `request.licenseTemplate`: \[Optional] The address of the license template used, If not specified, the configuration applies to all licenses. * `request.licensingConfig`: The licensing configuration for the license. * `request.licensingConfig.isSet`: Whether the configuration is set or not. * `request.licensingConfig.mintingFee`: The minting fee to be paid when minting license tokens. * `request.licensingConfig.hookData`: The data to be used by the licensing hook. * `request.licensingConfig.licensingHook`: The hook contract address for the licensing module, or address(0) if none. * `request.licensingConfig.commercialRevShare`: The commercial revenue share percentage (from 0 to 100). * `request.licensingConfig.disabled`: 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. * `request.licensingConfig.expectMinimumGroupRewardShare`: The minimum percentage of the group's reward share (from 0 to 100). * `request.licensingConfig.expectGroupRewardPool`: 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. ```typescript TypeScript theme={null} import { parseEther, zeroAddress } from "viem"; const response = await client.license.setLicensingConfig({ ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", licenseTermsId: 1, licensingConfig: { isSet: true, mintingFee: parseEther("1"), licensingHook: "0xaBAD364Bfa41230272b08f171E0Ca939bD600478", hookData: "0x", commercialRevShare: 10, disabled: false, expectMinimumGroupRewardShare: 0, expectGroupRewardPool: zeroAddress, }, }); ``` ```typescript Request Type theme={null} export type SetLicensingConfigRequest = GetLicensingConfigRequest & { /** The licensing configuration for the license. */ licensingConfig: LicensingConfigInput; }; export type GetLicensingConfigRequest = { /** The address of the IP for which the configuration is being set. */ ipId: Address; /** The ID of the license terms within the license template. */ licenseTermsId: number | bigint; /** * The address of the license template. * Defaults to {@link https://docs.datafdn.org/docs/programmable-ip-license | PIL} address if not provided. */ licenseTemplate?: Address; }; export type LicensingConfigInput = { /** Whether the licensing configuration is active. If false, the configuration is ignored. */ isSet: boolean; /** The minting fee to be paid when minting license tokens. */ mintingFee: bigint | string | number; /** * The licensingHook is an address to a smart contract that implements the `ILicensingHook` interface. * This contract's `beforeMintLicenseTokens` function is executed before a user mints a License Token, * allowing for custom validation or business logic to be enforced during the minting process. * For detailed documentation on licensing hook, visit {@link https://docs.datafdn.org/concepts/hooks#licensing-hooks} */ licensingHook: Address; /** * The data to be used by the licensing hook. * Set to a zero hash if no data is provided. */ hookData: Hex; /** The commercial revenue share percentage (from 0 to 100%, represented as 100_000_000). */ commercialRevShare: number | string; /** 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. */ disabled: boolean; /** 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. */ expectMinimumGroupRewardShare: number | string; /** 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. */ expectGroupRewardPool: Address; }; ``` ```typescript Response Type theme={null} export type SetLicensingConfigResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### getLicensingConfig Gets the licensing configuration for a specific license terms of an IP. | Method | Type | | -------------------- | --------------------------------------------------------- | | `getLicensingConfig` | `(request: GetLicensingConfigRequest) => LicensingConfig` | Parameters: * `request.ipId`: The address of the IP for which the configuration is being fetched. * `request.licenseTermsId`: The ID of the license terms within the license template. * `request.licenseTemplate`: \[Optional] The address of the license template used. ```typescript TypeScript theme={null} const licensingConfig = await client.license.getLicensingConfig({ ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", licenseTermsId: 1, }); ``` ```typescript Request Type theme={null} export type GetLicensingConfigRequest = { /** The address of the IP for which the configuration is being set. */ ipId: Address; /** The ID of the license terms within the license template. */ licenseTermsId: number | bigint; /** * The address of the license template. * Defaults to {@link https://docs.datafdn.org/docs/programmable-ip-license | PIL} address if not provided. */ licenseTemplate?: Address; }; ``` ```typescript Response Type theme={null} export type LicensingConfig = { /** Whether the licensing configuration is active. If false, the configuration is ignored. */ isSet: boolean; /** The minting fee to be paid when minting license tokens. */ mintingFee: bigint; /** * The licensingHook is an address to a smart contract that implements the `ILicensingHook` interface. * This contract's `beforeMintLicenseTokens` function is executed before a user mints a License Token, * allowing for custom validation or business logic to be enforced during the minting process. * For detailed documentation on licensing hook, visit {@link https://docs.datafdn.org/concepts/hooks#licensing-hooks} */ licensingHook: Address; /** * The data to be used by the licensing hook. * Set to a zero hash if no data is provided. */ hookData: Hex; /** The commercial revenue share percentage (from 0 to 100%, represented as 100_000_000). */ commercialRevShare: number; /** 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. */ disabled: boolean; /** 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. */ expectMinimumGroupRewardShare: number; /** 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. */ expectGroupRewardPool: Address; }; ``` ### setMaxLicenseTokens Set the max license token limit for a specific license. This method automatically configures the licensing hook to use the [TotalLicenseTokenLimitHook](https://github.com/thedatafoundation/protocol-periphery-v1/blob/release/1.3/contracts/hooks/TotalLicenseTokenLimitHook.sol) contract if the current licensing hook is not set to `TotalLicenseTokenLimitHook`, and sets the max license tokens to the specified limit. | Method | Type | | --------------------- | ----------------------------------------------------------------------- | | `setMaxLicenseTokens` | `(request: SetMaxLicenseTokensRequest) => Promise` | Parameters: * `request.ipId`: The address of the IP for which the configuration is being set. * `request.licenseTermsId`: The ID of the license terms within the license template. * `request.maxLicenseTokens`: The total license token limit, 0 means no limit. * `request.licenseTemplate`: \[Optional] The address of the license template used. ```typescript TypeScript theme={null} const response = await client.license.setMaxLicenseTokens({ ipId: "0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", licenseTermsId: 1, maxLicenseTokens: 1000, }); console.log(`Max license tokens set at transaction hash ${response.txHash}`); ``` ```typescript Request Type theme={null} export type SetMaxLicenseTokensRequest = GetLicensingConfigRequest & { /** The total license token limit, 0 means no limit */ maxLicenseTokens: bigint | number; }; export type GetLicensingConfigRequest = { /** The address of the IP for which the configuration is being set. */ ipId: Address; /** The ID of the license terms within the license template. */ licenseTermsId: number | bigint; /** * The address of the license template. * Defaults to {@link https://docs.datafdn.org/docs/programmable-ip-license | PIL} address if not provided. */ licenseTemplate?: Address; }; ``` ```typescript Response Type theme={null} export type TransactionResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` # NFT Client Source: https://docs.datafdn.org/sdk-reference/nftclient Used to mint a new SPG collection for use with the DATA Foundation. ## NftClient ### Methods * createNFTCollection * getMintFeeToken * getMintFee * setTokenURI * getTokenURI ### createNFTCollection Creates a new SPG NFT Collection. | Method | Type | | --------------------- | ------------------------------------------------------------------------------- | | `createNFTCollection` | `(request: CreateNFTCollectionRequest) => Promise` | Parameters: * `request.name`: The name of the collection. * `request.symbol`: The symbol of the collection. * `request.isPublicMinting`: If true, anyone can mint from the collection. If false, only the addresses with the minter role can mint. * `request.mintOpen`: Whether the collection is open for minting on creation. * `request.mintFeeRecipient`: The address to receive mint fees. * `request.contractURI`: The contract URI for the collection. Follows ERC-7572 standard. See [here](https://eips.ethereum.org/EIPS/eip-7572). * `request.baseURI`: \[Optional] 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. * `request.maxSupply`: \[Optional] The maximum supply of the collection. * `request.mintFee`: \[Optional] The cost to mint a token. * `request.mintFeeToken`: \[Optional] The token to mint. * `request.owner`: \[Optional] The owner of the collection. ```typescript TypeScript theme={null} import { zeroAddress } from "viem"; // Create a new SPG NFT collection // // NOTE: Use this code to create a new SPG NFT collection. You can then use the // `newCollection.spgNftContract` address as the `spgNftContract` argument in // functions like `registerIpAsset` in the IPAsset Client. // // You will mostly only have to do this once. Once you get your nft contract address, // you can use it in SPG functions. // const newCollection = await client.nftClient.createNFTCollection({ name: "Test NFT", symbol: "TEST", isPublicMinting: true, mintOpen: true, mintFeeRecipient: zeroAddress, contractURI: "", }); console.log( `New SPG NFT collection created at transaction hash ${newCollection.txHash}` ); console.log(`NFT contract address: ${newCollection.spgNftContract}`); ``` ```typescript Request Type theme={null} export type CreateNFTCollectionRequest = { name: string; symbol: string; isPublicMinting: boolean; mintOpen: boolean; mintFeeRecipient: Address; contractURI: string; baseURI?: string; maxSupply?: number; mintFee?: bigint; mintFeeToken?: Hex; owner?: Hex; }; ``` ```typescript Response Type theme={null} export type CreateNFTCollectionResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; spgNftContract?: Address; // the address of the newly created contract }; ``` ### getMintFeeToken Returns the current mint token of the collection. | Method | Type | | ----------------- | ----------------------------------------------- | | `getMintFeeToken` | `(spgNftContract: Address) => Promise
` | Parameters: * `spgNftContract`: The address of the NFT contract. ```typescript TypeScript theme={null} const mintFeeToken = await client.nftClient.getMintFeeToken("0x01"); ``` ### getMintFee Returns the current mint fee of the collection. | Method | Type | | ------------ | ---------------------------------------------- | | `getMintFee` | `(spgNftContract: Address) => Promise` | Parameters: * `spgNftContract`: The address of the NFT contract. ```typescript TypeScript theme={null} const mintFee = await client.nftClient.getMintFee("0x01"); ``` ### setTokenURI Sets the token URI for a given token. | Method | Type | | ------------- | --------------------------------------------------------------- | | `setTokenURI` | `(request: SetTokenURIRequest) => Promise` | Parameters: * `request.spgNftContract`: The address of the NFT contract. * `request.tokenId`: The ID of the token. * `request.tokenURI`: The URI to set. ```typescript TypeScript theme={null} const response = await client.nftClient.setTokenURI({ spgNftContract: "0x01", tokenId: 1, tokenURI: "https://ipfs.io/ipfs/QmX4zdp8VpzqvtKuEqMo6gfZPdoUx9TeHXCgzKLcFfSUbk", }); ``` ```typescript Request Type theme={null} export type SetTokenURIRequest = { spgNftContract: Address; tokenId: bigint | number; tokenURI: string; }; ``` ```typescript Response Type theme={null} export type TransactionResponse = { txHash: Hex; /** Transaction receipt, only available if waitForTransaction is set to true */ receipt?: TransactionReceipt; }; ``` ### getTokenURI Returns the token URI for a given token. | Method | Type | | ------------- | -------------------------------------------------- | | `getTokenURI` | `(request: GetTokenURIRequest) => Promise` | Parameters: * `request.spgNftContract`: The address of the SPG NFT contract. * `request.tokenId`: The ID of the token. ```typescript TypeScript theme={null} const tokenURI = await client.nftClient.getTokenURI({ spgNftContract: "0x01", tokenId: 1, }); ``` ```typescript Request Type theme={null} export type GetTokenURIRequest = { spgNftContract: Address; tokenId: bigint | number; }; ``` # SDK Reference Overview Source: https://docs.datafdn.org/sdk-reference/overview Overview of all available DATA Foundation TypeScript SDKs The DATA Foundation provides two TypeScript SDKs for building on the protocol: | Package | Description | Install | GitHub | | --------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------- | | **Protocol SDK** | Register IP Assets, manage licensing, royalties, disputes, and more | [npm](https://www.npmjs.com/package/@story-protocol/core-sdk) | [Code](https://github.com/thedatafoundation/sdk/tree/main) | | **CDR SDK** | Threshold encryption, confidential data vaults, and on-chain access control | [npm](https://www.npmjs.com/package/@piplabs/cdr-sdk) | [Code](https://github.com/piplabs/cdr-sdk) | A Python SDK is also available for the Protocol SDK: | Package | Description | PyPi | GitHub | | ------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------- | | **Protocol SDK (Python)** | Python client for IP Assets, licensing, royalties, and more | [PyPi](https://pypi.org/project/story-protocol-python-sdk) | [Code](https://github.com/thedatafoundation/python-sdk) | *** ## Protocol SDK The Protocol SDK (`@story-protocol/core-sdk`) is the primary SDK for interacting with the DATA Foundation's IP management layer. Use it to register IP Assets, attach license terms, mint license tokens, manage royalties, raise disputes, and more. Learn the Protocol SDK through a series of tutorials. Switch to the Python SDK reference. *** ## CDR SDK The CDR SDK (`@piplabs/cdr-sdk`) provides a TypeScript client for the DATA Foundation's Confidential Data Rails system. The current release targets Aeneid and covers both on-chain secret vaults (`uploadCDR` / `accessCDR`) and encrypted-file workflows (`uploadFile` / `downloadFile`). The package is published to npm as `@piplabs/cdr-sdk`. Learn CDR through a series of integration tutorials. Jump to the full CDR API reference. # Permissions Source: https://docs.datafdn.org/sdk-reference/permissions PermissionClient allows you to manage permissions for IP Accounts within the DATA Foundation. ## PermissionClient ### Methods * setPermission * createSetPermissionSignature * setAllPermissions * setBatchPermissions * createBatchPermissionSignature ### setPermission Sets the permission for a specific function call. Each policy is represented as a mapping from an IP account address to a signer address to a recipient\ address to a function selector to a permission level. The permission level can be 0 (ABSTAIN), 1 (ALLOW), or\ 2 (DENY). By default, all policies are set to 0 (ABSTAIN), which means that the permission is not set. The owner of IP Account by default has all permission. | Method | Type | | --------------- | --------------------------------------------------------------------- | | `setPermission` | `(request: SetPermissionsRequest) => Promise` | Parameters: * `request.ipId`: The IP ID that grants the permission for `signer`. * `request.signer`: The address that can call `to` on behalf of the `ipAccount`. * `request.to`: The address that can be called by the `signer` (currently only modules can be `to`) * `request.permission`: The new permission level. * `request.func`: \[Optional] The function selector string of `to` that can be called by the `signer` on behalf of the `ipAccount`. By default, it allows all functions. ```typescript Response Type theme={null} export type SetPermissionsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### createSetPermissionSignature Specific permission overrides wildcard permission with signature. | Method | Type | | ------------------------------ | ----------------------------------------------------------------------------------- | | `createSetPermissionSignature` | `(request: CreateSetPermissionSignatureRequest) => Promise` | Parameters: * `request.ipId`: The IP ID that grants the permission for `signer`. * `request.signer`: The address that can call `to` on behalf of the `ipAccount`. * `request.to`: The address that can be called by the `signer` (currently only modules can be `to`) * `request.permission`: The new permission level. * `request.func`: \[Optional] The function selector string of `to` that can be called by the `signer` on behalf of the `ipAccount`. By default, it allows all functions. * `request.deadline`: \[Optional] The deadline for the signature in milliseconds, default is 1000ms. ```typescript Response Type theme={null} export type SetPermissionsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### setAllPermissions Sets permission to a signer for all functions across all modules. | Method | Type | | ------------------- | ------------------------------------------------------------------------ | | `setAllPermissions` | `(request: SetAllPermissionsRequest) => Promise` | Parameters: * `request.ipId`: The IP ID that grants the permission for `signer`. * `request.signer`: The address of the signer receiving the permissions. * `request.permission`: The new permission. ```typescript Response Type theme={null} export type SetPermissionsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### setBatchPermissions Sets a batch of permissions in a single transaction. | Method | Type | | --------------------- | -------------------------------------------------------------------------- | | `setBatchPermissions` | `(request: SetBatchPermissionsRequest) => Promise` | Parameters: * `request.permissions[]`: An array of `Permission` structure, each representing the permission to be set. * `request.permissions[].ipId`: The IP ID that grants the permission for `signer`. * `request.permissions[].signer`: The address that can call `to` on behalf of the `ipAccount`. * `request.permissions[].to`: The address that can be called by the `signer` (currently only modules can be `to`) * `request.permissions[].permission`: The new permission level. * `request.permissions[].func`: \[Optional] The function selector string of `to` that can be called by the `signer` on behalf of the `ipAccount`. By default, it allows all functions. * `request.deadline`: \[Optional] The deadline for the signature in milliseconds, default is 1000ms. ```typescript Response Type theme={null} export type SetPermissionsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` ### createBatchPermissionSignature Sets a batch of permissions in a single transaction with signature. | Method | Type | | -------------------------------- | ------------------------------------------------------------------------------------- | | `createBatchPermissionSignature` | `(request: CreateBatchPermissionSignatureRequest) => Promise` | Parameters: * `request.ipId`: The IP ID that grants the permission for `signer` * `request.permissions[]` - An array of `Permission` structure, each representing the permission to be set. * `request.permissions[].ipId`: The IP ID that grants the permission for `signer`. * `request.permissions[].signer`: The address that can call `to` on behalf of the `ipAccount`. * `request.permissions[].to`: The address that can be called by the `signer` (currently only modules can be `to`) * `request.permissions[].permission`: The new permission level. * `request.permissions[].func`: \[Optional] The function selector string of `to` that can be called by the `signer` on behalf of the `ipAccount`. By default, it allows all functions. ```typescript Response Type theme={null} export type SetPermissionsResponse = { txHash?: Hex; encodedTxData?: EncodedTxData; success?: boolean; }; ``` # IP Account Source: https://docs.datafdn.org/sdk-reference/python/ipaccount IPAccount allows you to manage IP Account metadata and execute transactions. ## IPAccount ### Methods * set\_ip\_metadata * execute * execute\_with\_sig * transfer\_erc20 ### set\_ip\_metadata Sets the metadataURI for an IP asset. | Method | | ----------------- | | `set_ip_metadata` | Parameters: * `ip_id`: The IP to set the metadata for. * `metadata_uri`: The metadataURI to set for the IP asset. Should be a URL pointing to metadata that fits the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard). * `metadata_hash`: The hash of metadata at metadataURI. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} tx_hash = story_client.IPAccount.set_ip_metadata( ip_id="0x01", metadata_uri="https://ipfs.io/ipfs/bafkreiardkgvkejqnnkdqp4pamkx2e5bs4lzus5trrw3hgmoa7dlbb6foe", # example hash (not accurate) metadata_hash="0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997", ) ``` ```python Request Parameters theme={null} ip_id: str # The IP to set the metadata for metadata_uri: str # The metadataURI to set for the IP asset. Should be a URL pointing to metadata that fits the [IPA Metadata Standard](/concepts/ip-asset/ipa-metadata-standard) metadata_hash: str # The hash of metadata at metadataURI tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str # The transaction hash } ``` ### execute Executes a transaction from the IP Account. | Method | | --------- | | `execute` | Parameters: * `ip_id`: The IP Id to get ip account. * `to`: The recipient of the transaction. * `value`: The amount of Ether to send. * `data`: The data to send along with the transaction. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.IPAccount.execute( ip_id="0x01", to="0x1234567890123456789012345678901234567890", value=1000000000000000000, # 1 ETH data="0x1234567890123456789012345678901234567890", ) ``` ```python Request Parameters theme={null} ip_id: str # The IP to set the metadata for to: str # The recipient of the transaction value: int # The amount of Ether to send data: str # The data to send along with the transaction tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str # The transaction hash } ``` ### execute\_with\_sig Executes a transaction from the IP Account. | Method | | ------------------ | | `execute_with_sig` | Parameters: * `ip_id`: The IP to set the metadata for. * `to`: The recipient of the transaction. * `data`: The data to send along with the transaction. * `signer`: The signer of the transaction. * `deadline`: The deadline of the transaction signature. * `signature`: The signature of the transaction, EIP-712 encoded. * `value`: \[Optional] The amount of Ether to send. **Default: 0** * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.IPAccount.execute_with_sig( ip_id="0x01", to="0x1234567890123456789012345678901234567890", data="0x1234567890123456789012345678901234567890", signer="0x1234567890123456789012345678901234567890", deadline=1000000000000000000, signature="0x1234567890123456789012345678901234567890", value=1000000000000000000, # 1 ETH ) ``` ```python Request Parameters theme={null} ip_id: str # The IP to set the metadata for to: str # The recipient of the transaction data: str # The data to send along with the transaction signer: str # The signer of the transaction deadline: int # The deadline of the transaction signature signature: str # The signature of the transaction, EIP-712 encoded value: int = 0 # Optional: The amount of Ether to send tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str # The transaction hash } ``` ### transfer\_erc20 Transfers an ERC20 token from the IP Account. | Method | | ---------------- | | `transfer_erc20` | Parameters: * `ip_id`: The `ipId` of the account * `tokens`: The token info to transfer * `tokens.address`: The address of the ERC20 token including WIP and standard ERC20. * `tokens.amount`: The amount of tokens to transfer * `tokens.target`: The address of the recipient. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.IPAccount.transferERC20( ip_id="0x01", tokens=[ { "address": "0x1514000000000000000000000000000000000000", # $WIP "target": "0x02", "amount": 1000000 # Equivalent to 0.001 ether } ] ) ``` ```python Request Parameters theme={null} ip_id: str # The IP to set the metadata for tokens: list # The token info to transfer tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str, # The transaction hash } ``` # IP Asset Source: https://docs.datafdn.org/sdk-reference/python/ipasset IPAsset allows you to create, get, and list IP Assets within the DATA Foundation. ## IPAsset ### Methods * register * register\_derivative * register\_derivative\_with\_license\_tokens * mint\_and\_register\_ip\_asset\_with\_pil\_terms ### Navigating Around the IPAssetClient Because there are a lot of functions to interact with the [📜 Licensing Module](/concepts/licensing-module), we have broken them down into a helpful chart so you can identify what you're looking for, and then find the associated docs. | **Function** | **Mint an NFT** | **Register IPA** | **Create License Terms** | **Attach License Terms** | **Mint License Token** | **Register as Derivative** | | ------------------------------------------------------------- | :-------------: | :--------------: | :----------------------: | :----------------------: | :--------------------: | :------------------------: | | register | | ✓ | | | | | | mint\_and\_register\_ip\_asset\_with\_pil\_terms | ✓ | ✓ | ✓ | ✓ | | | | register\_derivative | | | | | | ✓ | | register\_derivative\_with\_license\_tokens | | | | | | ✓ | | register\_pil\_terms | | | ✓ | | | | | attach\_license\_terms | | | | ✓ | | | | mint\_license\_tokens | | | | | ✓ | | * Red: IPAssetClient (this page) * Blue: [LicenseClient](/sdk-reference/python/license) ## register Registers an NFT as IP, creating a corresponding [🧩 IP Asset](/concepts/ip-asset). If the given NFT was already registered, this function will return the existing `ipId`. Note that this function will also set the underlying NFT's `tokenUri` to whatever is passed under `ipMetadata.nftMetadataURI`. | Method | | ---------- | | `register` | Parameters: * `nft_contract`: The address of the NFT. * `token_id`: The token identifier of the NFT. * `ip_metadata`: \[Optional] The desired metadata for the newly minted NFT and newly registered IP. * `ip_metadata['ip_metadata_uri']`: \[Optional] The URI of the metadata for the IP. * `ip_metadata['ip_metadata_hash']`: \[Optional] The hash of the metadata for the IP. * `ip_metadata['nft_metadata_uri']`: \[Optional] The URI of the metadata for the NFT. * `ip_metadata['nft_metadata_hash']`: \[Optional] The hash of the metadata for the IP NFT. * `deadline`: \[Optional] The deadline for the signature in milliseconds. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} metadata = { 'ip_metadata_uri': "test-uri", 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-ip-metadata-hash")), 'nft_metadata_uri': "test-uri", 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) } response = story_client.IPAsset.register( nft_contract="0x041B4F29183317Fd352AE57e331154b73F8a1D73", token_id="12", ip_metadata=metadata ) ``` ```python Request Parameters theme={null} nft_contract: str # The address of the NFT token_id: str # The token identifier of the NFT ip_metadata: dict = None # Optional: Metadata for the NFT and IP deadline: int = None # Optional: The deadline for the signature in milliseconds tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "ip_id": str, # The IP ID of the registered IP "tx_hash": str # The transaction hash } ``` ## register\_derivative Registers a derivative directly with parent IP's license terms, without needing license tokens, and attaches the license terms of the parent IPs to the derivative IP. The license terms must be attached to the parent IP before calling this function. All IPs attached default license terms by default. The derivative IP owner must be the caller or an authorized operator. | Method | | --------------------- | | `register_derivative` | Parameters: * `child_ip_id`: The derivative IP ID. * `parent_ip_ids`: The parent IP IDs. * `license_terms_ids`: The IDs of the license terms that the parent IP supports. * `max_minting_fee`: \[Optional] The maximum minting fee that the caller is willing to pay. If set to 0, then there is no limit. **Default: 0** * `max_revenue_share`: \[Optional] The maximum revenue share percentage agreed upon between a child and parent when a child is registering as derivative. Must be between 0 and 100. **Default: 100** * `max_rts`: \[Optional] The maximum number of royalty tokens that can be distributed to the external royalty policies. Must be between 0 and 100,000,000. **Default: 100\_000\_000** * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.IPAsset.register_derivative( child_ip_id="0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", parent_ip_ids=["0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba"], license_terms_ids=["5"], max_minting_fee=0, # disabled max_rts=100_000_000, # default max_revenue_share=100 # default ) ``` ```python Request Parameters theme={null} child_ip_id: str # The derivative IP ID parent_ip_ids: list # The parent IP IDs license_terms_ids: list # The IDs of the license terms that the parent IP supports max_minting_fee: int = 0 # Optional: The maximum minting fee the caller is willing to pay max_rts: int = 0 # Optional: The maximum number of royalty tokens max_revenue_share: int = 0 # Optional: The maximum revenue share percentage license_template: str = None # Optional: The address of the license template tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str # The transaction hash } ``` ## register\_derivative\_with\_license\_tokens Registers a derivative with license tokens. The derivative IP is registered with license tokens minted from the parent IP's license terms. The license terms of the parent IPs issued with license tokens are attached to the derivative IP. The caller must be the derivative IP owner or an authorized operator. | Method | | ----------------------------------------- | | `register_derivative_with_license_tokens` | Parameters: * `child_ip_id`: The derivative IP ID. * `license_token_ids`: The IDs of the license tokens. * `max_rts`: The maximum number of royalty tokens that can be distributed to the external royalty policies. Must be between 0 and 100,000,000. **Recommended for simplicity: 100\_000\_000** * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.IPAsset.register_derivative_with_license_tokens( child_ip_id="0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", license_token_ids=["5"], max_rts=100_000_000 # default ) ``` ```python Request Parameters theme={null} child_ip_id: str # The derivative IP ID license_token_ids: list # The IDs of the license tokens max_rts: int = 0 # Optional: The maximum number of royalty tokens tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str # The transaction hash } ``` ## mint\_and\_register\_ip\_asset\_with\_pil\_terms Mint an NFT from a collection, register it as an IP, attach metadata to the IP, and attach License Terms to the IP all in one function. Note that this function will also set the underlying NFT's `tokenUri` to whatever is passed under `ipMetadata.nftMetadataURI`. | Method | | ------------------------------------------- | | `mint_and_register_ip_asset_with_pil_terms` | Parameters: * `spg_nft_contract`: The address of the NFT collection. * `terms`: The array of license terms to be attached. ⚠️ **This function will fail if you pass in an empty array.** * `terms[].terms`: The license terms data. See the Python example below for the structure. * `terms[].licensing_config`: \[Optional] The licensing configuration. See the Python example below for the structure. * `allow_duplicates`: \[Optional] Set to true to allow minting IPs with the same NFT metadata. **Default: True** * `ip_metadata`: \[Optional] The desired metadata for the newly minted NFT and newly registered IP. * `ip_metadata['ip_metadata_uri']`: \[Optional] The URI of the metadata for the IP. * `ip_metadata['ip_metadata_hash']`: \[Optional] The hash of the metadata for the IP. * `ip_metadata['nft_metadata_uri']`: \[Optional] The URI of the metadata for the NFT. * `ip_metadata['nft_metadata_hash']`: \[Optional] The hash of the metadata for the IP NFT. * `recipient`: \[Optional] The address of the recipient of the minted NFT. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} commercial_remix_terms = { "transferable": True, "royalty_policy": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", # RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts "default_minting_fee": 0, "expiration": 0, "commercial_use": True, "commercial_attribution": True, "commercializer_checker": "0x0000000000000000000000000000000000000000", "commercializer_checker_data": "0x0000000000000000000000000000000000000000", "commercial_rev_share": 50, "commercial_rev_ceiling": 0, "derivatives_allowed": True, "derivatives_attribution": True, "derivatives_approval": False, "derivatives_reciprocal": True, "derivative_rev_ceiling": 0, "currency": "0x1514000000000000000000000000000000000000", # $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts "uri": "", } licensing_config = { "is_set": False, "minting_fee": 0, "licensing_hook": "0x0000000000000000000000000000000000000000", "hook_data": "0x0000000000000000000000000000000000000000", "commercial_rev_share": 0, "disabled": False, "expect_minimum_group_reward_share": 0, "expect_group_reward_pool": "0x0000000000000000000000000000000000000000", } metadata = { 'ip_metadata_uri': "test-uri", 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-ip-metadata-hash")), 'nft_metadata_uri': "test-uri", 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) } response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract="0xfE265a91dBe911db06999019228a678b86C04959", terms=[{ "terms": commercial_remix_terms, "licensing_config": licensing_config }], allow_duplicates=True, ip_metadata=metadata ) ``` ```python Request Parameters theme={null} spg_nft_contract: str # The address of the NFT collection terms: list # The array of license terms to be attached ip_metadata: dict = None # Optional: Metadata for the NFT and IP recipient: str = None # Optional: The address of the recipient of the minted NFT allow_duplicates: bool = False # Optional: Allow minting IPs with the same NFT metadata tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "ip_id": str, # The IP ID of the registered IP "token_id": int, # The token ID of the minted NFT "tx_hash": str, # The transaction hash "license_terms_ids": list # The IDs of the registered license terms } ``` # License Source: https://docs.datafdn.org/sdk-reference/python/license License allows you to manage license terms and tokens within the DATA Foundation. ## License ### Methods * attach\_license\_terms * mint\_license\_tokens * register\_pil\_terms * register\_non\_com\_social\_remixing\_pil * register\_commercial\_use\_pil * register\_commercial\_remix\_pil ### attach\_license\_terms Attaches license terms to an IP. | Method | | ---------------------- | | `attach_license_terms` | Parameters: * `ip_id`: The address of the IP to which the license terms are attached. * `license_template`: The address of the license template. * `license_terms_id`: The ID of the license terms. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.License.attach_license_terms( ip_id="0x4c1f8c1035a8cE379dd4ed666758Fb29696CF721", license_template="0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", # insert PILicenseTemplate from https://docs.datafdn.org/docs/deployed-smart-contracts license_terms_id="1" ) ``` ```python Request Parameters theme={null} ip_id: str # The address of the IP to which the license terms are attached license_template: str # The address of the license template license_terms_id: int # The ID of the license terms tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "tx_hash": str } ``` ### mint\_license\_tokens Mints license tokens for the license terms attached to an IP. The license tokens are minted to the receiver. The license terms must be attached to the IP before calling this function. IP owners can mint license tokens for their IPs for arbitrary license terms without attaching the license terms to IP. It might require the caller pay the minting fee, depending on the license terms or configured by the iP owner. The minting fee is paid in the minting fee token specified in the license terms or configured by the IP owner. IP owners can configure the minting fee of their IPs or configure the minting fee module to determine the minting fee. | Method | | --------------------- | | `mint_license_tokens` | Parameters: * `licensor_ip_id`: The licensor IP ID. * `license_template`: The address of the license template. * `license_terms_id`: The ID of the license terms within the license template. * `amount`: The amount of license tokens to mint. * `receiver`: The address of the receiver. * `max_minting_fee`: \[Optional] The maximum minting fee to pay. * `max_revenue_share`: \[Optional] The maximum revenue share percentage. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = client.License.mint_license_tokens( licensor_ip_id="0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", license_template="0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", # insert PILicenseTemplate from https://docs.datafdn.org/docs/deployed-smart-contracts license_terms_id="1", amount=1, receiver="0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", # optional max_minting_fee=0, # disabled max_revenue_share=100 # default ) ``` ```python Request Parameters theme={null} licensor_ip_id: str # The licensor IP ID license_template: str # The address of the license template license_terms_id: int # The ID of the license terms amount: int # The amount of license tokens to mint receiver: str # The address of the receiver max_minting_fee: int = 0 # Optional: The maximum minting fee to pay max_revenue_share: int = 0 # Optional: The maximum revenue share percentage tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "license_token_ids": list, # List of license token IDs "tx_hash": str, # The transaction hash } ``` ### register\_pil\_terms Registers new license terms and return the ID of the newly registered license terms. | Method | | -------------------- | | `register_pil_terms` | Parameters: * See the Python code example below for all the parameters. They all come from the [PIL Terms](/concepts/programmable-ip-license/pil-terms). * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.License.register_pil_terms( transferable=False, royalty_policy="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", # RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts default_minting_fee=0, expiration=0, commercial_use=False, commercial_attribution=False, commercializer_checker="0x0000000000000000000000000000000000000000", commercializer_checker_data="0x", commercial_rev_share=10, # 10% commercial_rev_ceiling=0, derivatives_allowed=True, derivatives_attribution=False, derivatives_approval=False, derivatives_reciprocal=False, derivative_rev_ceiling=0, currency="0x1514000000000000000000000000000000000000", # $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts uri="", ) ``` ```python Request Parameters theme={null} transferable: bool # Indicates whether the license is transferable or not royalty_policy: str # The address of the royalty policy contract default_minting_fee: int # The default minting fee to be paid when minting a license expiration: int # The expiration period of the license commercial_use: bool # Indicates whether the work can be used commercially or not commercial_attribution: bool # Whether attribution is required when reproducing the work commercially commercializer_checker: str # Commercializers that are allowed to commercially exploit the work commercializer_checker_data: str # The data to be passed to the commercializer checker contract commercial_rev_share: int # Percentage of revenue that must be shared with the licensor (0-100) commercial_rev_ceiling: int # The maximum revenue that can be generated from commercial use derivatives_allowed: bool # Indicates whether the licensee can create derivatives of the work derivatives_attribution: bool # Whether attribution is required for derivatives of the work derivatives_approval: bool # Whether the licensor must approve derivatives before they can be linked derivatives_reciprocal: bool # Whether derivatives must be licensed under the same terms derivative_rev_ceiling: int # The maximum revenue that can be generated from derivative use currency: str # The ERC20 token to be used to pay the minting fee uri: str # The URI of the license terms tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "license_terms_id": int, "tx_hash": str } ``` ### register\_non\_com\_social\_remixing\_pil Convenient function to register a PIL non commercial social remix license to the registry. No reason to call this function. Non-Commercial Social Remixing terms are already registered with `licenseTermdId = 1` in our protocol. There's no reason to register them again. | Method | | -------------------------------------- | | `register_non_com_social_remixing_pil` | Parameters: * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.License.register_non_com_social_remixing_pil() ``` ```python Request Parameters theme={null} tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "license_terms_id": int, # The ID of the registered license terms "tx_hash": str, # The transaction hash } ``` ### register\_commercial\_use\_pil Convenient function to register a PIL commercial use license to the registry. | Method | | ----------------------------- | | `register_commercial_use_pil` | Parameters: * `default_minting_fee`: The fee to be paid when minting a license. * `currency`: The ERC20 token to be used to pay the minting fee and the token must be registered on the DATA Foundation's protocol. * `royalty_policy`: \[Optional] The address of the royalty policy contract, default value is LAP. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.License.register_commercial_use_pil( currency='0x1514000000000000000000000000000000000000', # $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts default_minting_fee=10, # 10 of the currency (using the above currency, 10 $WIP), royalty_policy="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", # RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts ) ``` ```python Request Parameters theme={null} default_minting_fee: int # The fee to be paid when minting a license currency: str # The ERC20 token to be used to pay the minting fee royalty_policy: str = None # Optional: The address of the royalty policy contract tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "license_terms_id": int, # The ID of the registered license terms "tx_hash": str, # The transaction hash } ``` ### register\_commercial\_remix\_pil Convenient function to register a PIL commercial Remix license to the registry. | Method | | ------------------------------- | | `register_commercial_remix_pil` | Parameters: * `default_minting_fee`: The fee to be paid when minting a license. * `commercial_rev_share`: Percentage of revenue that must be shared with the licensor. * `currency`: The ERC20 token to be used to pay the minting fee and the token must be registered on the DATA Foundation's protocol. * `royalty_policy`: The address of the royalty policy contract, default value is LAP. * `tx_options`: \[Optional] Transaction options dictionary. ```python Python theme={null} response = story_client.License.register_commercial_remix_pil( currency='0x1514000000000000000000000000000000000000', # $WIP address from https://docs.datafdn.org/docs/deployed-smart-contracts default_minting_fee=10, # 10 of the currency (using the above currency, 10 $WIP) royalty_policy="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", # RoyaltyPolicyLAP address from https://docs.datafdn.org/docs/deployed-smart-contracts commercial_rev_share=10 # 10% ) ``` ```python Request Parameters theme={null} default_minting_fee: int # The fee to be paid when minting a license currency: str # The ERC20 token to be used to pay the minting fee commercial_rev_share: int # Percentage of revenue that must be shared with the licensor royalty_policy: str # The address of the royalty policy contract tx_options: dict = None # Optional: Transaction options ``` ```python Response theme={null} { "license_terms_id": int, # The ID of the registered license terms "tx_hash": str, # The transaction hash } ``` # SDK Reference Overview Source: https://docs.datafdn.org/sdk-reference/python/overview A detailed description of every function in our Python SDK This section provides a detailed description of every function in our Python SDK. | Package | Compatibility | Package | GitHub | | | --------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------ | | TypeScript | Full | [npm](https://www.npmjs.com/package/@story-protocol/core-sdk) | [Code](https://github.com/thedatafoundation/sdk/tree/main) | [SWITCH](/sdk-reference) | | Python | Full | [PyPi](https://pypi.org/project/story-protocol-python-sdk) | [Code](https://github.com/thedatafoundation/python-sdk) | | *** # WIP Client Source: https://docs.datafdn.org/sdk-reference/wipclient Used to handle the wrapping/unwrapping of WIP (Wrapped IP) tokens within the DATA Foundation. ## WipClient ### Methods * deposit * withdraw * approve * balanceOf * transfer * transferFrom ### deposit Wraps the selected amount of IP to WIP. The WIP will be deposited to the wallet that transferred the IP. | Method | Type | | --------- | --------------------------- | | `deposit` | `(request: DepositRequest)` | Parameters: * `request.amount`: The amount to deposit. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const response = await client.wipClient.deposit({ amount: parseEther("10"), // 10 DATA tokens }); ``` ```typescript Request Type theme={null} export type DepositRequest = { amount: TokenAmountInput; }; ``` ### withdraw Unwraps the selected amount of WIP to IP. | Method | Type | | ---------- | ---------------------------- | | `withdraw` | `(request: WithdrawRequest)` | Parameters: * `request.amount`: The amount to withdraw. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const response = await client.wipClient.withdraw({ amount: parseEther("5"), // 5 WIP tokens }); ``` ```typescript Request Type theme={null} export type WithdrawRequest = { amount: TokenAmountInput; }; ``` ### approve Approve a spender to use the wallet's WIP balance. | Method | Type | | --------- | --------------------------- | | `approve` | `(request: ApproveRequest)` | Parameters: * `request.amount`: The amount of WIP tokens to approve. * `request.spender`: The address that will use the WIP tokens ```typescript TypeScript theme={null} import { parseEther } from "viem"; const response = await client.wipClient.approve({ spender: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", amount: parseEther("20"), // 20 WIP tokens }); ``` ```typescript Request Type theme={null} export type ApproveRequest = { spender: Address; amount: TokenAmountInput; }; ``` ### balanceOf Returns the balance of WIP for an address. | Method | Type | | ----------- | ------------------------------------ | | `balanceOf` | `(addr: Address) => Promise` | Parameters: * `addr`: The address you want to check the baalnce for. ### transfer Transfers `amount` of WIP to a recipient `to`. | Method | Type | | ---------- | ---------------------------- | | `transfer` | `(request: TransferRequest)` | Parameters: * `request.to`: Who you're transferring to. * `request.amount`: The amount to transfer. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const response = await client.wipClient.transfer({ to: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", amount: parseEther("3"), // 3 WIP tokens }); ``` ```typescript Request Type theme={null} export type TransferRequest = { to: Address; amount: TokenAmountInput; }; ``` ### transferFrom Transfers `amount` of WIP from `from` to a recipient `to`. | Method | Type | | -------------- | -------------------------------- | | `transferFrom` | `(request: TransferFromRequest)` | Parameters: * `request.to`: Who you're transferring to. * `request.amount`: The amount to transfer. * `request.from`: The address to transfer from. ```typescript TypeScript theme={null} import { parseEther } from "viem"; const response = await client.wipClient.transferFrom({ to: "0xC92EC2f4c86458AFee7DD9EB5d8c57920BfCD0Ba", amount: parseEther("2"), // 2 WIP tokens from: "0x6B86B39F03558A8a4E9252d73F2bDeBfBedf5b68", }); ``` ```typescript Request Type theme={null} export type TransferFromRequest = { to: Address; amount: TokenAmountInput; from: Address; }; ``` # Trace Integration Guide Source: https://docs.datafdn.org/trace/integration How any data provider registers records, submits metadata updates, and reads public audit views through the Trace API. # Trace Provider Data Audit — Integration & API Reference The single source of truth for the Trace data audit API: provider onboarding, auth, the Trace Schema, write and read endpoints, search, stats, scoped groups, limits, and validation. The API and Trace Schema are provider-agnostic — every provider integrates through the same endpoints, headers, and schema. Examples use **Kled**, a live provider; substitute your own provider identity wherever Kled appears. ## Environments | Environment | Base URL | Auth | | -------------- | --------------------------------------- | ----------------------------------------------------- | | **Staging** | `https://staging-api.storyprotocol.net` | Writes gated by staging API key. Reads are public. | | **Production** | `https://api.dataapis.io` | Writes gated by production API key. Reads are public. | Staging is where new provider integrations are onboarded and tested. Production is deployed but may not yet have provider data — check with the DATA Foundation team before sending production traffic. ## Getting access Write access is gated. Before integrating, contact the DATA Foundation team to be onboarded: we whitelist your provider identity, issue staging and production API keys, and assign the `X-Provider` value your write requests must use. Until that is done, write requests are rejected. Read, search, and scoped-group endpoints are public audit views and do not require a key. ## Auth and provider scope Every write request must include: ```text theme={null} X-API-Key: X-Provider: kled X-Batch-Id: ``` * The API key must belong to your provider and match `X-Provider`; use the key for the environment you're targeting. * For backlog ingestion, add `X-Ingestion-Source: backlog`. For live ingestion, omit the header — explicit `live` is not accepted. * Read APIs are keyed by the global `data_id`; `provider` is returned as a field and doubles as an optional filter. Read, search, and scoped-group endpoints are **public audit views**. Do not put enterprise-only or sensitive fields into the public Trace payload. If a provider-only or partner-only read tier is ever needed, that is a separate API/product decision. ## Ingestion flow The provider client POSTs to a webhook batch endpoint. Accepted items are enqueued and processed asynchronously by the DATA Foundation. Records become available through the read and search endpoints. A `records:batch` response always includes per-item statuses: * `202 Accepted` — at least one item was accepted for asynchronous processing and no item conflicted. * `200 OK` — every item was a duplicate, so no new async work was enqueued. * `409 Conflict` — at least one item conflicted; any `accepted` items in the same response were still enqueued. Inspect item statuses, and retry transient request failures with the same payload and the same `X-Batch-Id`. ## Record lifecycle Each record's history is an append-only event log that Trace renders as a timeline. Each step maps to exactly one value; a step appears on the timeline when its value is present. | # | Step | Value | | - | ----------------- | ---------------------------------------------------------------------------------- | | 1 | Captured | `timestamps.captured_at` | | 2 | Uploaded | `timestamps.uploaded_at` | | 3 | Attested | `attestation.signed_at_utc` | | 4 | Registered | `ingested_at` of the `DataRegistered` event (`seq: 0`) — DATA Foundation-generated | | 5 | Anchored on-chain | `tx_hash` (empty string until the DATA Foundation broadcasts) | | 6 | Updated | Root-level `occurred_at` of each `MetadataUpdated` event (`seq` 1–100) | | 7 | Payment credited | `timestamps.payment_credited_at` — stored today, surfaced on the timeline soon | Steps 1–3 come from the registration payload's metadata; steps 4–7 are audit events. Send each milestone as its own metadata update — one milestone, one timeline event. The root `occurred_at` on each batch item is required, provider-supplied, and dates that specific event — registration time on the `DataRegistered` event, update time on each `MetadataUpdated` event. Trace surfaces the **Registered** lifecycle step with its own `ingested_at` — the moment the DATA Foundation accepted the record, returned on per-record reads. `timestamps.originated_at` inside the attested payload carries the record's origin moment: set it equal to the registration's root `occurred_at`, and keep it fixed on later updates (each update's root `occurred_at` dates the update itself). Records registered before this field was renamed carry the same value under the legacy name `timestamps.occurred_at` — readers should accept both. The only values the DATA Foundation adds are `ingested_at` and `tx_hash`. ## Trace Schema v1.0 Populate the standardized Trace Schema fields directly and preserve your full original public payload under `provider_payload`. The normalized fields are the portable contract the frontend and audit flows use; nothing provider-specific is lost. * Use `schema_version: trace-v1.0` in `initial_metadata_json` and `metadata_json`. The value is not validated — a missing or unparseable `schema_version` defaults to `trace-v1.0`, and whatever value is sent is folded into the event hash. * The DATA Foundation canonicalizes metadata JSON before computing internal event hashes, so object key order does not affect idempotency or conflict detection. * Do not send a transaction hash in write payloads. The DATA Foundation owns `tx_hash` and returns it on read responses for registration, metadata update, and search result rows — as an empty string until the DATA Foundation fills it after broadcast. ### Canonical content hash (required) Initial registrations and metadata updates must include one canonical content hash. Accepted fields are `asset.hash`, `content_hash`, `file.content_sha256`, or `file.hashes.sha256`; all normalize to `sha256:<64-lowercase-hex>`. If more than one alias is present, they must represent the same hash. ### Recommended top-level shape ```json theme={null} { "schema_version": "trace-v1.0", "file": { "content_sha256": "sha256:<64-hex>", "mime_type": "video/mp4", "media_category": "video", "size_bytes": 123456, "hashes": { "phash64": "facebeef01234567", "dhash64": "1b9072d44a8be3c1", "ahash64": "ff7e3c1a90d5e8c2", "keyframe_phashes": [ "0011223344556677", "8899aabbccddeeff" ] }, "behavior": { "captured_at_utc": "2026-05-13", "uploaded_at_utc": "2026-05-13", "capture_to_upload_seconds": 421, "capture_to_upload_bucket": "5-60min", "upload_session_size": 3, "upload_session_kind": "gallery_pick", "captured_via": "ios_native_camera", "uploaded_via": "ios_app", "client_version": "ios-1.42.0" } }, "file_specific": { "base": { "motion": { "compass_heading": 247.3, "compass_heading_reference": "true_north", "speed_bucket": "stationary" } }, "video": {}, "image": {}, "document": {} }, "asset": { "collection_id": "kled-collection-2026-05", "customer_id": "kled-customer-001", "task_id": "kled-task-0042" }, "contributor": { "anon_id": "kled-public-user-id", "kyc_status": "verified", "kyc_country": "US", "geo_region": "US", "tax_status": "submitted", "account_verification_status": "verified", "consent": { "tos_version": "2026-05-20", "tos_hash": "sha256:<64-hex-policy-hash>", "tos_uri": "https://kled.ai/terms/2026-05-20", "privacy_policy_version": "2026-05-20", "privacy_policy_hash": "sha256:<64-hex-policy-hash>", "privacy_policy_uri": "https://kled.ai/privacy/2026-05-20" } }, "app": { "platform_name": "kled.ai", "legal_entity": "Nitrility Inc. (Delaware, USA)" }, "timestamps": { "originated_at": "2026-05-13T00:00:00Z", "uploaded_at": "2026-05-13T00:00:00Z", "captured_at": "2026-05-12T23:59:00Z", "payment_credited_at": "2026-05-20T12:00:00Z" }, "attestation": { "payload_hash": "sha256:", "signature": "optional-signature", "key_id": "optional-key-id", "key_url": "https://kled.ai/.well-known/verification-keys.json", "signed_at_utc": "2026-05-13T00:00:02Z" }, "provider_payload": { "...": "full provider public payload" } } ``` ### Searchable fields Current exact-match searchable fields: | Field | Aliases | | ------------------------- | ---------------------------------------------------------------------------------- | | `provider` | — | | `source_record_id` | `media_id_public` (when present in metadata) | | `file.content_sha256` | `asset_hash`, `asset.hash`, `content_hash`, `content_sha256`, `file.hashes.sha256` | | `file.mime_type` | `mime_type`, `mimetype`, `file.mimetype` | | `file.media_category` | `media_category` | | `contributor.anon_id` | — | | `collection_id` | `asset.collection_id` | | `customer_id` | `asset.customer_id` | | `task_id` | `asset.task_id` | | `contributor.kyc_status` | — | | `contributor.kyc_country` | — | | `contributor.geo_region` | — | | `tos_hash` | `contributor.consent.tos_hash` | | `privacy_policy_hash` | `contributor.consent.privacy_policy_hash` | The only searchable hash is the canonical content SHA-256 — perceptual hashes (`phash64`, `dhash64`, `ahash64`, `keyframe_phashes`) and `md5` are stored but not indexed. The same goes for `app.platform_name`, `app.legal_entity`, `file.behavior.*`, `file_specific.base.motion.*`, and `timestamps.payment_credited_at`: send them when useful — stored fields can be indexed later without resending old data. Use `/stats` for distributions and `provider` as an optional query scope. ### Field guidance * `source_record_id` — provider-owned stable public media ID (for example `kmf_...`). The service trims surrounding whitespace, preserves case, rejects control characters, and accepts up to 512 bytes. * `contributor.anon_id` — provider-owned public anonymized contributor ID. * `asset.collection_id`, `asset.customer_id`, `asset.task_id` — optional provider-assigned grouping IDs: a collection or batch of records, the customer or campaign they were produced for, and the task they belong to. Each is exact-match searchable, and matching is case-sensitive like `source_record_id`. Send them at the top level (as in the shape above) or inside `provider_payload` — both are read. Omit any that don't apply. * `contributor.kyc_status` — recommended values: `verified`, `pending`, `failed`, `unverified`. * `contributor.kyc_country` — ISO 3166-1 alpha-2 country code from KYC, if available. Country only; no address or GPS-derived country. * `contributor.tax_status` — recommended values: `submitted`, `not_submitted`, `not_applicable`, `unknown`. For a boolean like `tax_form_on_file`, map `true` to `submitted` and `false` to `not_submitted`. * `contributor.account_verification_status` — recommended values: `verified`, `pending`, `failed`, `unverified`. * `contributor.consent.tos_*` and `contributor.consent.privacy_policy_*` — the exact policy version, hash, and URI the contributor accepted for this record. Provider-level *active* policies are set separately through `PUT /webhook/v1/data-audit/provider-policy`, never inside record payloads. * `attestation` — signature is optional on staging. For production verification, send `payload_hash`, `signature`, `key_id`, `key_url`, and `signed_at_utc`. * `app.legal_entity` — legal counterparty information alongside the app/platform name. * `file.behavior` — non-PII capture/upload behavior signals. * `file_specific.base.motion` — shared motion signals that apply across media types. * `timestamps.payment_credited_at` — optional UTC timestamp for when the provider finalized and credited the contributor's payment. Omit when nothing was credited (or the payout was forfeited); if payment lands after registration, send it as a full-state metadata update. Stored-only. | Version | Value | Purpose | | --------------------------- | ------------------------ | ------------------------------------------------------------------------------------- | | Trace schema | `trace-v1.0` | Normalized metadata shape expected in `initial_metadata_json` and `metadata_json`. | | Event hash | `event-hash-v1` | Version of the internal audit event hash envelope. | | Event hash canonicalization | `json-canonical-v1` | JSON is normalized before hashing so object key order does not affect the event hash. | | Read model schema | `read-model-v1` | Version embedded in read-model partition keys. | | Stats shard schema | `shard-v2` | Version embedded in sharded stats counter partition keys. | | Search index schema | `postgres-read-model-v1` | Version stamped on search index rows returned by `/search`. | Stored on durable and index rows. Future changes should dual-write/dual-read versions during transition rather than mutating history. ## Write API ### Set active provider policies Use this endpoint when the provider publishes a new Terms of Service or Privacy Policy. Trace stores the active version, hash, and URI so the frontend can link to the policy and stats can compare records against it. Policy rows are audit-only — not indexed or aggregated. Record payloads still carry `contributor.consent.*`; those record-level references are what `/stats`, scoped-group summaries, and policy-hash search use. ```http theme={null} PUT /webhook/v1/data-audit/provider-policy Content-Type: application/json X-API-Key: X-Provider: kled ``` ```json theme={null} { "tos": { "version": "2026-06-01", "hash": "sha256:<64-hex-policy-hash>", "uri": "https://kled.ai/terms/2026-06-01", "effective_at": "2026-06-01T00:00:00Z" }, "privacy_policy": { "version": "2026-06-01", "hash": "sha256:<64-hex-policy-hash>", "uri": "https://kled.ai/privacy/2026-06-01", "effective_at": "2026-06-01T00:00:00Z" } } ``` Both the `tos` and `privacy_policy` objects are required, each with `version`, `hash`, and `uri`; `effective_at` is optional. The hash accepts the same forms as asset hashes (`sha256:<64-hex>`, bare hex, `0x<64-hex>`, `0x1220<64-hex>`) and is stored canonically as `sha256:<64-lowercase-hex>`. The request body is capped at `1 MiB`. Read current and historical policy documents: ```http theme={null} GET /api/v1/data-audit/providers/kled/policy GET /api/v1/data-audit/providers/kled/policies/tos/sha256:<64-hex-policy-hash> GET /api/v1/data-audit/providers/kled/policies/privacy_policy/sha256:<64-hex-policy-hash> ``` ### Register records Use this endpoint for initial backlog and live registration batches. The provider sends its stable `source_record_id`; the DATA Foundation generates the `data_id` and returns the mapping. ```http theme={null} POST /webhook/v1/data-audit/records:batch Content-Type: application/json X-API-Key: X-Provider: kled X-Batch-Id: kled-records-000001 X-Ingestion-Source: backlog ``` The request body is a JSON array. `initial_metadata_json` follows the [recommended top-level shape](#recommended-top-level-shape) above (abbreviated here): ```json theme={null} [ { "source_record_id": "kmf_8a9c2e7d4b1f0e23", "initial_metadata_root": "sha256:", "initial_metadata_json": { "schema_version": "trace-v1.0", "file": { "content_sha256": "sha256:<64-hex>", "mime_type": "video/mp4", "media_category": "video", "size_bytes": 123456 }, "file_specific": { "video": { "duration_ms": 120000, "width": 1920, "height": 1080 } }, "contributor": { "anon_id": "kup_123", "kyc_status": "verified", "kyc_country": "US", "geo_region": "US", "tax_status": "submitted", "account_verification_status": "verified", "consent": { "tos_version": "2026-05-20", "tos_hash": "sha256:<64-hex-policy-hash>", "tos_uri": "https://kled.ai/terms/2026-05-20", "privacy_policy_version": "2026-05-20", "privacy_policy_hash": "sha256:<64-hex-policy-hash>", "privacy_policy_uri": "https://kled.ai/privacy/2026-05-20" } }, "app": { "platform_name": "kled.ai", "legal_entity": "Nitrility Inc. (Delaware, USA)" }, "timestamps": { "originated_at": "2026-05-13T00:00:00Z", "uploaded_at": "2026-05-13T00:00:00Z" }, "attestation": { "payload_hash": "sha256:", "signature": "optional-on-staging", "key_id": "kled-verify-2026-q1", "key_url": "https://kled.ai/.well-known/verification-keys.json", "signed_at_utc": "2026-05-13T00:00:02Z" }, "provider_payload": { "media_id_public": "kmf_8a9c2e7d4b1f0e23" } }, "occurred_at": "2026-05-13T00:00:00Z" } ] ``` `initial_metadata_root` should be the provider's deterministic non-zero hash of the canonical Trace Schema v1.0 metadata JSON. The DATA Foundation stores this value as submitted; hash verification against `initial_metadata_json` is not enforced yet. Successful response (`202` when new items were enqueued, `200` when every item was a duplicate, `409` when any item conflicted): ```json theme={null} { "request_id": "story-request-uuid", "provider": "kled", "batch_id": "kled-records-000001", "format": "json", "kind": "records", "records": 1, "accepted": 1, "duplicates": 0, "conflicts": 0, "messages": 1, "items": [ { "source_record_id": "kmf_8a9c2e7d4b1f0e23", "data_id": "story-generated-uuid", "status": "accepted" } ] } ``` The returned `data_id` is the canonical ID for future metadata updates and reads. Re-sending the same `X-Provider` + `source_record_id` generates the same `data_id`. Per-item statuses: an item already persisted with the exact same payload returns `duplicate`; the same `source_record_id` with *different* initial metadata returns `conflict`. Neither is (re-)enqueued, and other valid items in the batch are unaffected. An overlapping retry while the first request is still queued may return `accepted` — downstream ingestion stays idempotent. If a caller already has DATA Foundation-assigned UUIDs, the lower-level `POST /webhook/v1/data-audit/data-ids:batch` endpoint exists. It requires `data_id` on every record and is not the recommended provider path. ### Submit metadata updates Use this endpoint for later corrections or mutable metadata changes. `seq` must be `1`–`100` per `data_id` (a hard cap of 100 updates per record, all environments). Changes that belong to the same provider-side revision can share one update and one `seq`. `metadata_json` must include a canonical content hash and be the **full latest Trace metadata state** — not a diff or patch. If only KYC changed, still include the unchanged file, app, consent, and provider payload fields, so each event verifies against `metadata_root` and the latest state can be rebuilt without merge rules. ```http theme={null} POST /webhook/v1/data-audit/metadata-updates:batch Content-Type: application/json X-API-Key: X-Provider: kled X-Batch-Id: kled-metadata-000001 X-Ingestion-Source: backlog ``` Request body is a JSON array: ```json theme={null} [ { "data_id": "11111111-1111-4111-8111-111111111111", "seq": 1, "prev_metadata_root": "sha256:", "metadata_root": "sha256:", "metadata_json": { "schema_version": "trace-v1.0", "asset": { "hash": "sha256:<64-hex>" }, "contributor": { "anon_id": "kup_123", "kyc_status": "unverified", "consent": { "tos_version": "2026-06-01", "tos_hash": "sha256:<64-hex-policy-hash>", "tos_uri": "https://kled.ai/terms/2026-06-01", "privacy_policy_version": "2026-05-20", "privacy_policy_hash": "sha256:<64-hex-policy-hash>", "privacy_policy_uri": "https://kled.ai/privacy/2026-05-20" } }, "app": { "platform_name": "kled.ai" }, "provider_payload": { "media_id_public": "kmf_8a9c2e7d4b1f0e23", "reason": "kyc_status_changed" } }, "occurred_at": "2026-05-13T00:00:01Z" } ] ``` `metadata_root` should be the provider's deterministic non-zero hash of the canonical full updated Trace Schema v1.0 metadata JSON. `prev_metadata_root` must also be a non-zero root. The DATA Foundation stores these values as submitted; hash verification against `metadata_json` is not enforced yet. The normal integration path is `application/json` on `/records:batch` — that route accepts JSON only. For backlog tooling, these lower-level route variants also exist; data ID file routes require `data_id` in every record. ```text theme={null} POST /webhook/v1/data-audit/data-ids:batch-ndjson POST /webhook/v1/data-audit/metadata-updates:batch-ndjson POST /webhook/v1/data-audit/data-ids:batch-csv POST /webhook/v1/data-audit/metadata-updates:batch-csv POST /webhook/v1/data-audit/data-ids:batch-txt POST /webhook/v1/data-audit/metadata-updates:batch-txt ``` * **NDJSON** requires `Content-Type: application/x-ndjson` and `Content-Encoding: gzip`. Each decompressed line is one JSON record with the same fields as the JSON endpoints. * **CSV** requires `Content-Type: text/csv`. * Data ID columns: `data_id,source_record_id,initial_metadata_root,initial_metadata_json,occurred_at` * Metadata update columns: `data_id,seq,prev_metadata_root,metadata_root,metadata_json,occurred_at` * `initial_metadata_json` and `metadata_json` must be valid JSON in a quoted CSV field and include one canonical content hash. * **TXT** requires `Content-Type: text/plain` and accepts line-delimited JSON, a JSON array, or header-delimited comma/tab text using the same CSV columns. ## Read API Read endpoints are public audit views. `provider` is optional and acts as a narrowing filter. Read model: * `GET /data-ids/{data_id}` returns the registration profile plus the latest raw metadata event — the exact payload of the highest stored sequence, not necessarily a diff. The response also carries a top-level `ingested_at` — the DATA Foundation-generated timestamp of the registration (seq 0) ingestion — omitted when unavailable. * `GET /data-ids/{data_id}/metadatas` returns the full append-only metadata history, including registration at `seq: 0` and later metadata updates. * Search, asset receipt lookup, and scoped-group summaries use the DATA Foundation's normalized latest-state projection derived from those events. This projection powers fields such as MIME type, media category, KYC status, TOS/privacy versions, lifecycle status, and `tx_hash`. ### Get trace by data ID ```http theme={null} GET /api/v1/data-audit/data-ids/11111111-1111-4111-8111-111111111111 ``` ```json theme={null} { "data_id": "11111111-1111-4111-8111-111111111111", "provider": "kled", "ingested_at": "2026-05-13T00:00:01.234567Z", "profile": { "data_id": "11111111-1111-4111-8111-111111111111", "provider": "kled", "tx_hash": "" }, "latest_metadata": { "data_id": "11111111-1111-4111-8111-111111111111", "seq": 0, "event_type": "DataRegistered", "tx_hash": "" } } ``` ### List metadata history ```http theme={null} GET /api/v1/data-audit/data-ids/11111111-1111-4111-8111-111111111111/metadatas ``` Each metadata row includes `tx_hash`, initially as an empty string: ```json theme={null} { "data_id": "11111111-1111-4111-8111-111111111111", "metadatas": [ { "seq": 0, "event_type": "DataRegistered", "tx_hash": "" }, { "seq": 1, "event_type": "MetadataUpdated", "tx_hash": "" } ] } ``` ### Search by indexed field ```http theme={null} GET /api/v1/data-audit/search?field=source_record_id&value=kmf_8a9c2e7d4b1f0e23 ``` Search is exact-match on the [searchable fields](#searchable-fields). Matching is case-sensitive for `source_record_id`, `contributor.anon_id`, `collection_id`, `customer_id`, and `task_id`; other fields match case-insensitively, and content-hash values are canonicalized before matching. Each match includes `tx_hash` (initially an empty string), and responses include `next_cursor` for pagination. `limit` defaults to `100` and is capped at `1000` — larger values are silently clamped, not rejected. Typical flow: use `/search` to locate records by an exact field value, then `/data-ids/{data_id}` or `/data-ids/{data_id}/metadatas` for the canonical event payload used in audit verification. Use `/stats` for distribution counts, and `/recent` or `/feed` when the UI needs the newest records. More examples: ```text theme={null} GET /api/v1/data-audit/search?field=asset_hash&value=sha256:<64-hex> GET /api/v1/data-audit/search?field=file.content_sha256&value=<64-hex-or-sha256-prefixed-hex> GET /api/v1/data-audit/search?field=customer_id&value= GET /api/v1/data-audit/search?field=task_id&value= GET /api/v1/data-audit/search?field=collection_id&value= GET /api/v1/data-audit/search?field=contributor.consent.tos_hash&value=sha256:<64-hex-policy-hash> GET /api/v1/data-audit/search?field=contributor.consent.privacy_policy_hash&value=sha256:<64-hex-policy-hash> GET /api/v1/data-audit/search?field=collection_id&value=&provider=kled&limit=100&cursor= ``` ### Provider totals (`/stats`) ```http theme={null} GET /api/v1/data-audit/stats GET /api/v1/data-audit/stats?provider=kled ``` ```json theme={null} { "total_records": 1000324, "total_contributors": 902111, "provider": "kled", "provider_records": 100234, "provider_contributors": 87234, "kyc_status": { "verified": 91234, "pending": 9000 }, "account_verification": { "verified": 90000, "pending": 10234 }, "tax_status": { "complete": 80000, "submitted": 20234 }, "tos_versions": { "2026-05": 100234 }, "privacy_policy_versions": { "2026-05": 100234 }, "media_category_coverage": { "video": 80000, "image": 20234 }, "mime_distribution": { "video/mp4": 80000, "image/jpeg": 20234 }, "geo_distribution": { "us": 70234, "ca": 30000 }, "total_size_bytes": 1234567890, "size_record_count": 100234, "average_size_bytes": 12316, "active_tos_version": "2026-05", "active_tos_hash": "sha256:<64-hex-policy-hash>", "active_tos_uri": "https://kled.ai/terms/2026-05", "active_privacy_policy_version": "2026-05", "active_privacy_policy_hash": "sha256:<64-hex-policy-hash>", "active_privacy_policy_uri": "https://kled.ai/privacy/2026-05" } ``` Record totals count each registered record once; contributor totals count distinct `provider + contributor_anon_id` values. Metadata updates change neither, but distributions and size totals follow the latest projection, so a full-state update can move a record between buckets. `average_size_bytes` is `total_size_bytes / size_record_count`, counting records with a positive size. `shard_count` is reserved (currently `0`). The `provider_*` fields appear only with `?provider=`, and `active_*` only when that provider has an active policy. App/platform fields are not stats scopes. ### Recent ingestion feed ```http theme={null} GET /api/v1/data-audit/feed?limit=50 GET /api/v1/data-audit/feed?provider=kled&limit=50 GET /api/v1/data-audit/feed?provider=kled&limit=50&cursor= ``` The feed returns recent audit events — both registrations and metadata updates — newest first by ingestion time. Rows include `event_type`, `seq`, `data_id`, `source_record_id`, `asset_hash`, `occurred_at`, and `ingested_at`. Use `next_cursor` to fetch older rows. ### Recent registered records ```http theme={null} GET /api/v1/data-audit/recent?limit=50 GET /api/v1/data-audit/recent?provider=kled&limit=50 GET /api/v1/data-audit/recent?provider=kled&limit=50&cursor= ``` Returns registered receipt rows only, newest first by ingestion time. Use `next_cursor` to fetch older rows. ### Asset receipts by content hash ```http theme={null} GET /api/v1/data-audit/assets/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` Accepted hash forms are `sha256:<64-hex>`, plain `<64-hex>`, `0x<64-hex>`, and `0x1220<64-hex>`. The response returns the canonical `sha256:<64-lowercase-hex>` asset hash and all matched receipt rows. ## Scoped groups Scoped groups let the Trace frontend or a partner reviewer create a public snapshot over a review set. For provider review workflows, labs should submit the provider's `source_record_id` values; Trace computes the deterministic `data_id`, verifies every record exists, and only creates the group if the full set is valid. (Groups can also be created from content hashes via `hashes` / `hashes_text` — the two input kinds are mutually exclusive.) Creation is asynchronous: poll `GET /scoped-groups/{group_id}` until `status` is `complete`. ### Create a group ```http theme={null} POST /api/v1/data-audit/scoped-groups Content-Type: application/json Idempotency-Key: kled-review-000001 ``` ```json From source record IDs theme={null} { "title": "Kled review set", "description": "Kled source record IDs supplied to a lab", "provider": "kled", "source_record_ids": ["kmf_1", "kmf_2"] } ``` ```json From pasted text theme={null} { "title": "Kled pasted review set", "provider": "kled", "source_record_ids_text": "kmf_1\nkmf_2\nkmf_3" } ``` ```json From an upload theme={null} { "title": "Kled uploaded review set", "description": "CSV uploaded through presigned S3", "provider": "kled", "upload_id": "up_.csv" } ``` Source-record groups are all-or-nothing: | Condition | Response | | ---------------------------------------------- | -------- | | Duplicate `provider + source_record_id` values | `400` | | Missing records | `404` | | Profile/source mismatches | `409` | | Records registered without an asset projection | `422` | ### Uploads for larger review sets For larger source-record CSV/TXT inputs, request a presigned upload URL first: ```http theme={null} POST /api/v1/data-audit/scoped-groups/uploads Content-Type: application/json ``` ```json theme={null} { "format": "csv" } ``` Supported formats are `csv` and `txt`. Upload the file bytes to the returned `upload_url` with the returned `Content-Type` header, then create the group with the returned `upload_id`. ```csv One-column CSV (requires top-level provider) theme={null} source_record_id kmf_1 kmf_2 ``` ```csv Mixed-provider CSV (no top-level provider) theme={null} provider,source_record_id kled,kmf_1 oto,oto_1 ``` ```text TXT (requires top-level provider) theme={null} kmf_1 kmf_2 ``` Omit `provider` in the create request only when the uploaded CSV has a `provider` column. ### Read a group ```http theme={null} GET /api/v1/data-audit/scoped-groups/{group_id} GET /api/v1/data-audit/scoped-groups/{group_id}/items?limit=100 GET /api/v1/data-audit/scoped-groups/{group_id}/items?limit=100&cursor= GET /api/v1/data-audit/scoped-groups/{group_id}/export.csv ``` `GET /scoped-groups/{group_id}` returns the group status and, once complete, aggregate metrics in `summary`. `profile.status` is one of `pending`, `processing`, `complete`, or `failed`; `summary` is only present once the group is `complete`. ```json theme={null} { "profile": { "group_id": "sg_...", "title": "Kled review set", "description": "Kled source record IDs supplied to a lab", "manifest_kind": "source_record", "status": "complete", "submitted_items": 2, "unique_items": 2, "computed_at": "2026-06-04T15:30:00Z" }, "summary": { "records_in_set": 2, "submitted_items": 2, "unique_items": 2, "matched_items": 2, "missing_items": 0, "matched_receipts": 2, "kyc_verified_percent": 50, "distinct_media_categories": ["image", "video"], "distinct_tos_versions": ["2026-05"], "total_size_bytes": 7340032, "average_size_bytes": 3670016, "source_distribution": { "kled": 2 }, "media_category_coverage": { "image": 1, "video": 1 }, "mime_distribution": { "image/jpeg": 1, "video/mp4": 1 }, "tos_versions": { "2026-05": 2 }, "privacy_policy_versions": { "2026-05": 2 }, "kyc_status": { "verified": 1, "unverified": 1 }, "geo_distribution": { "us": 2 }, "lifecycle_status": { "registered": 2 }, "metadata_presence": { "custom.camera": 1, "tos_acknowledgment": 2 } } } ``` `/items` returns one row per submitted `source_record_id` with the resolved `data_id`, `asset_hash`, and receipt summary. It is cursor-paginated and returns `next_cursor` when more rows exist. Both `/items` and `/export.csv` require the group to be `complete` and return `409` before that. For source-record groups, the CSV export columns are `input_type,provider,source_record_id,data_id,asset_hash,status,mime_type,media_category,kyc_status,tos_version,privacy_policy_version,tx_hash`; hash-manifest groups start with `hash,status,data_id,...` instead. ## Limits and retry behavior | Limit | Value | | ---------------------------------------- | ----------- | | Max webhook batch payload (decompressed) | `25 MiB` | | Max provider-policy request body | `1 MiB` | | Max SQS message chunk | `240 KiB` | | Max SQS batch payload | `256 KiB` | | Max serialized record size | `350 KiB` | | Max metadata updates per data ID | `100` | | Max inline scoped-group body | `5 MiB` | | Max inline scoped-group hashes | `10,000` | | Max inline scoped-group source records | `10,000` | | Max source\_record\_id length | `512 bytes` | Retry guidance: * Retry `502`, `503`, `504`, network timeouts, and `429` with exponential backoff and jitter. (Only the public scoped-group create/upload endpoints emit `429` — 60 requests/minute per IP; webhook batch endpoints never do.) * Do not retry validation/auth `4xx` until the request is fixed. * Keep `data_id`, request body, and `X-Batch-Id` stable across retries. * Use `X-Ingestion-Source: backlog` only for backlog work; omit it for live work. * The write path is idempotent for the same `data_id`, event key, and event hash. * If the same `data_id` and event key are retried with different metadata, the record is treated as a conflict and rejected. ## Validation rules | Rule | Behavior | | -------------------------------------------------------------------------- | ------------------------------------- | | Missing `X-Provider` on write endpoints | Request is rejected. | | Invalid provider name | Request is rejected. | | Provider outside the configured allowlist | Write request is rejected. | | API key not authorized for the `X-Provider` value | Write request is rejected with `403`. | | Missing `X-Batch-Id` on write endpoints | Request is rejected. | | `X-Ingestion-Source` present with any value other than `backlog` | Request is rejected. | | Missing `source_record_id` on `/records:batch` | Request is rejected. | | Duplicate `source_record_id` inside one `/records:batch` request | Request is rejected. | | Existing `/records:batch` item with the same initial registration payload | Item returns `status: "duplicate"`. | | Existing `/records:batch` item with different initial registration payload | Item returns `status: "conflict"`. | | Non-UUID `data_id` on lower-level data ID and metadata update routes | Request is rejected. | | Missing `occurred_at` | Request is rejected. | | Invalid `occurred_at` timestamp | Request is rejected. | | Metadata `seq` outside `1` through `100` | Request is rejected. | | Missing, malformed, or zero required metadata root fields | Request is rejected. | ## Delivery semantics * Delivery is at least once, so duplicate submissions may occur. * Duplicate submissions of the same event are treated idempotently. * Same `data_id`, same metadata sequence, and different event content is treated as a conflict. * Metadata updates may arrive before the initial data ID registration. * Audit data is durable and does not expire. * Event hashes are computed from canonicalized metadata JSON plus the event/hash/schema version fields. # Trace Overview Source: https://docs.datafdn.org/trace/overview Verifiable, provider-normalized provenance for data registered on the DATA Foundation. Trace is currently available on staging. Build and test integrations against the staging API; treat schemas and endpoints as stable-but-evolving until the production release. ## What is Trace? **Trace** is the DATA Foundation's data provenance and audit layer. Data providers send normalized metadata about the content they handle (content hashes, perceptual hashes, contributor consent, KYC signals, and capture/upload behavior) to the DATA Foundation, which assigns each record a global `data_id`, stores an append-only metadata history, and exposes public audit views over the whole dataset. The result is a portable, queryable, verifiable record of **where data came from and under what terms it was contributed**, across every provider that integrates, not just one. ```text theme={null} Provider client -> story-api webhook batch endpoint (write: register + metadata updates) -> asynchronous DATA Foundation processing (assigns data_id, hashes events) -> story-api read/search/stats (public audit views) ``` ## Where Trace Fits Proves the origin, consent, and lineage of data with a provider-normalized schema and public audit views. Keeps the underlying data encrypted, with threshold decryption gated by on-chain access control. Defines who owns the data and the terms under which it can be used. Together they let a provider register data that is **provable** (Trace), **confidential** (CDR), and **governed by clear usage rights** (IP & Licensing). ## The Trace Schema Every provider maps its own payload onto a single shared shape, the **Trace Schema** (`schema_version: trace-v1.0`), and includes its full original payload under `provider_payload` so no provider-specific detail is lost. The normalized fields are the portable contract that the Trace frontend, audit flows, and other providers all rely on. The schema covers: * **`file`**: content hash (`content_sha256`), MIME type, media category, perceptual hashes (`phash64`, `dhash64`, `ahash64`, keyframe hashes), and non-PII capture/upload behavior signals. * **`user` / `contributor`**: KYC status and country, tax status, account verification status, and the exact Terms of Service / Privacy Policy versions, hashes, and URIs the contributor accepted. * **`app`**: platform name and legal entity behind the integration. * **`timestamps`**: when the content was captured, uploaded, and occurred. * **`attestation`**: payload hash and an optional provider signature for independent verification. * **`provider_payload`**: the provider's full original public payload. ## How Providers Integrate Integration is a REST flow keyed by an API key and provider scope, no SDK required. The complete write, read, search, and scoped-group API, the Trace Schema, and limits/retry behavior. Reach out to get a provider scope and staging API key. A provider: 1. Receives a provider scope (e.g. `X-Provider: kled`) and an API key. 2. Registers records in batches via the webhook endpoint, sending normalized Trace fields plus its full original payload. 3. Submits metadata updates over time as KYC, consent, or other mutable fields change: each update is the full latest state, independently verifiable. 4. Sets its active Terms of Service and Privacy Policy through the provider policy endpoint. Reads, search, stats, feeds, and scoped-group summaries are **public audit views** keyed by the global `data_id`, with `provider` available as an optional filter. # Blockscout API Source: https://docs.datafdn.org/api-reference/blockscout-api Get gas price, average block time, market cap, token price, and more. Datanetscan has a public API endpoint that returns gas price, average block time, market cap, token price (coin gecko), and several other stats: `https://www.datanetscan.io/api/v2/stats` Here is an example response ⤵️ ```json theme={null} { "average_block_time": 2364, "coin_image": "https://coin-images.coingecko.com/coins/images/54035/small/Transparent_bg.png?1738075331", "coin_price": "4.83", "coin_price_change_percentage": null, "gas_price_updated_at": "2025-03-10T14:47:27.175157Z", "gas_prices": { "slow": 0.1, "average": 0.57, "fast": 1.05 }, "gas_prices_update_in": 11735, "gas_used_today": "147032238744", "market_cap": "1209228486.984", "network_utilization_percentage": 10.8968948333333, "secondary_coin_image": null, "secondary_coin_price": null, "static_gas_price": null, "total_addresses": "686024", "total_blocks": "1765700", "total_gas_used": "0", "total_transactions": "5606580", "transactions_today": "221320", "tvl": null } ``` # Introduction Source: https://docs.datafdn.org/api-reference/consensus-client/introduction Example section for showcasing API endpoints In order to use the Consensus Client API, you must run your own node. See the [Node Setup Guide](/network/operating-a-node/node-setup-mainnet). We have included the API Reference here so you know what to expect in the response. # GetAuthParams Source: https://docs.datafdn.org/api-reference/cosmos-originauth/getauthparams /api-reference/consensus-client/consensus-client-api.json get /auth/params # GetBalancesByAddressDenom Source: https://docs.datafdn.org/api-reference/cosmos-originbank/getbalancesbyaddressdenom /api-reference/consensus-client/consensus-client-api.json get /bank/balances/{address}/by_denom # GetBankParams Source: https://docs.datafdn.org/api-reference/cosmos-originbank/getbankparams /api-reference/consensus-client/consensus-client-api.json get /bank/params # GetSpendableBalancesByAddressDenom Source: https://docs.datafdn.org/api-reference/cosmos-originbank/getspendablebalancesbyaddressdenom /api-reference/consensus-client/consensus-client-api.json get /bank/spendable_balances/{address}/by_denom # GetSupplyByDenom Source: https://docs.datafdn.org/api-reference/cosmos-originbank/getsupplybydenom /api-reference/consensus-client/consensus-client-api.json get /bank/supply/by_denom # GetDelegatorRewardsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getdelegatorrewardsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/delegators/{delegator_address}/rewards # GetDelegatorRewardsByDelegatorAddressValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getdelegatorrewardsbydelegatoraddressvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/delegators/{delegator_address}/rewards/{validator_address} # GetDistributionParams Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getdistributionparams /api-reference/consensus-client/consensus-client-api.json get /distribution/params # GetDistributionValidatorByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getdistributionvalidatorbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/validators/{validator_address} # GetDistributionValidatorsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getdistributionvalidatorsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/delegators/{delegator_address}/validators # GetValidatorCommissionByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getvalidatorcommissionbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/validators/{validator_address}/commission # GetValidatorOutstandingRewardsByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getvalidatoroutstandingrewardsbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/validators/{validator_address}/outstanding_rewards # GetValidatorSlashesByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-origindistribution/getvalidatorslashesbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /distribution/validators/{validator_address}/slashes # GetMintParams Source: https://docs.datafdn.org/api-reference/cosmos-originmint/getmintparams /api-reference/consensus-client/consensus-client-api.json get /mint/params # GetDelegationByValidatorAddressDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getdelegationbyvalidatoraddressdelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations/{delegator_addr} # GetDelegationsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getdelegationsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/delegations/{delegator_addr} # GetDelegatorUnbondingDelegation Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getdelegatorunbondingdelegation /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation # GetHistoricalInfoByHeight Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/gethistoricalinfobyheight /api-reference/consensus-client/consensus-client-api.json get /staking/historical_info/{height} # GetRedelegationsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getredelegationsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/redelegations # GetStakingParams Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getstakingparams /api-reference/consensus-client/consensus-client-api.json get /staking/params # GetStakingPool Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getstakingpool /api-reference/consensus-client/consensus-client-api.json get /staking/pool # GetUnbondingDelegationsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getunbondingdelegationsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/unbonding_delegations # GetValidatorByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidatorbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr} # GetValidatorDelegationsByValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidatordelegationsbyvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegations # GetValidators Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidators /api-reference/consensus-client/consensus-client-api.json get /staking/validators # GetValidatorsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidatorsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/validators # GetValidatorsByDelegatorAddressValidatorAddress Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidatorsbydelegatoraddressvalidatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/delegators/{delegator_addr}/validators/{validator_addr} # GetValidatorUnbondingDelegations Source: https://docs.datafdn.org/api-reference/cosmos-originstaking/getvalidatorunbondingdelegations /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/unbonding_delegations # Get License Tokens Source: https://docs.datafdn.org/api-reference/protocol-v4/get-license-tokens https://api.dataapis.io/api/v4/openapi.json post /licenses/tokens Retrieve license tokens with optional filtering. If no filters are provided, returns all license tokens. Supports filtering by owner wallet address and/or licensor IP ID. Results are paginated and can be ordered by 'blockNumber' (default: descending). # List Collections Source: https://docs.datafdn.org/api-reference/protocol-v4/list-collections https://api.dataapis.io/api/v4/openapi.json post /collections Retrieve a list of collections with pagination and filtering options. Collections can be ordered by updatedAt, assetCount, or licensesCount (asc/desc). Collections are automatically enriched with metadata. The 'where' field is optional and should only be provided when filtering by specific collection addresses or asset counts. This endpoint can also be used to fetch a single collection by passing its address in the collectionAddresses filter. Collections that don't exist in Alchemy or encounter errors will return with empty metadata instead of failing the entire request. # List IP Asset Edges Source: https://docs.datafdn.org/api-reference/protocol-v4/list-ip-asset-edges https://api.dataapis.io/api/v4/openapi.json post /assets/edges Retrieve a list of edges (derivative registered events) that represent relationships between IP assets. These edges show parent-child relationships formed through licensing. # List IP Assets Source: https://docs.datafdn.org/api-reference/protocol-v4/list-ip-assets https://api.dataapis.io/api/v4/openapi.json post /assets Retrieve a list of IP assets with pagination and filtering options. The 'where' field is optional and should only be provided when filtering by specific IP IDs, owner address, or token contract address. This endpoint can also be used to fetch a single asset by passing its ID in the ipIds filter. # Introduction Source: https://docs.datafdn.org/api-reference/protocol/introduction Example section for showcasing API endpoints Welcome to the DATA Foundation API Reference! See below for details. You can use the following public API key: ```http Headers theme={null} // mainnet X-API-Key: MhBsxkU1z9fG6TofE59KqiiWV-YlYE8Q4awlLQehF3U // aeneid testnet X-API-Key: KOTbaGUSWQ6cUJWhiJYiOjPgB0kTRu1eCFFvQL0IWls ``` | Environment | Endpoint | Live Docs | OpenAPI JSON | | -------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Mainnet | `https://api.dataapis.io/api/v4` | [Go](https://api.dataapis.io/api/v4/docs) | [Go](https://api.dataapis.io/api/v4/openapi.json) | | Aeneid Testnet | `https://staging-api.storyprotocol.net/api/v4` | [Go](https://staging-api.storyprotocol.net/api/v4/docs) | [Go](https://staging-api.storyprotocol.net/api/v4/openapi.json) | ## Rate Limit The above public API key has a requests/second of 300. If you'd like an API key with a higher limit, please join our Builder Discord and describe your project needs in the discussion channel. # Search IP Assets Source: https://docs.datafdn.org/api-reference/protocol/search https://api.dataapis.io/api/v4/openapi.json POST /search Perform vector search for IP assets based on query text and optional media type filter. This endpoint uses AI-powered search to find relevant assets by semantic similarity. Search is done by vectorizing the title and description of the IP metadata using the [IPA metadata standard](https://docs.datafdn.org/concepts/ip-asset/ipa-metadata-standard). # List IP Transactions Source: https://docs.datafdn.org/api-reference/protocol/transactions https://api.dataapis.io/api/v4/openapi.json POST /transactions Retrieve a list of IP transactions with pagination and filtering options. The ‘where’ field is optional and should only be provided when filtering by specific transaction hashes, event types, or block ranges. This endpoint can also be used to fetch specific transactions by passing their hashes in the txHashes filter. This endpoint allows you to filter based on event type. Here are the possible string values for the `where.eventTypes[]` field: * "IPRegistered": When an IP asset is registered * "LicenseTermsAttached": When license terms are attached to an IP asset * "DerivativeRegistered": When a derivative IP asset is registered * "DisputeRaised": When a dispute is raised against an IP asset * "DisputeResolved": When a dispute is resolved * "DisputeCancelled": When a dispute is cancelled * "DisputeJudgementSet": When a judgement is set for a dispute * "RoyaltyPaid": When royalty payments are made # GetPeriodDelegationByDelegatorAddressAndID Source: https://docs.datafdn.org/api-reference/story-extendstaking/getperioddelegationbydelegatoraddressandid /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegators/{delegator_addr}/period_delegations/{period_delegation_id} # GetPeriodDelegationsByDelegatorAddress Source: https://docs.datafdn.org/api-reference/story-extendstaking/getperioddelegationsbydelegatoraddress /api-reference/consensus-client/consensus-client-api.json get /staking/validators/{validator_addr}/delegators/{delegator_addr}/period_delegations # DATA Foundation Disclaimer Source: https://docs.datafdn.org/foundation/disclaimer The Foundation may directly or indirectly purchase and sell DATA tokens ($DATA) at any time and from time to time in its sole discretion for any reason, including, without limitation, in order to support market stability, the Foundation's operations and long-term ecosystem growth, and it has previously engaged in such purchase and sale transactions. Such purchases and sales may be made through open market transactions, OTC trades, block trades, in privately negotiated transactions or by other means. The Foundation may also, from time to time, enter into programmatic sales and/or purchase plans with respect to the sale and/or purchase of $DATA. The volume, price, timing and manner of any sales and purchases and other parameters will be determined by the Foundation in its sole discretion. These transactions may involve the Foundation selling $DATA at a price (or at an average price) that is greater than the price (or the average price) at which the Foundation purchases $DATA in close proximity to one another or over time. This notice does not obligate the Foundation to sell or purchase any specific amount of \$DATA or within any specific time period and, if any such purchases or sales are commenced, may be modified, suspended or discontinued at any time without notice at the discretion of the Foundation. The Foundation will enter into such transactions on a proprietary basis for its own benefit and account and, for the avoidance of doubt, will not enter into any such transactions as agent or otherwise on behalf of, or for the benefit of, any other person. The Foundation may directly or indirectly purchase and sell shares of common stock or other securities of Heritage Distilling Holding Company, Inc. d/b/a IP Strategy (“IP Strategy”) at any time and from time to time in its sole discretion for any reason. Such purchases and sales may be made through open market transactions, Nasdaq trades, block trades, in privately negotiated transactions or by other means. The Foundation may also, from time to time, enter into programmatic sales and/or purchase plans with respect to the sale and/or purchase of IP Strategy securities. The volume, price, timing and manner of any sales and purchases and other parameters will be determined by the Foundation in its sole discretion. These transactions may involve the Foundation selling IP Strategy securities at a price (or at an average price) that is greater than the price (or the average price) at which the Foundation purchases IP Strategy securities in close proximity to one another or over time. This notice does not obligate the Foundation to sell or purchase any specific amount of IP Strategy securities or within any specific time period and, if any such purchases or sales are commenced, may be modified, suspended or discontinued at any time without notice at the discretion of the Foundation. The Foundation will enter into such transactions on a proprietary basis for its own benefit and account and, for the avoidance of doubt, will not enter into any such transactions as agent or otherwise on behalf of, or for the benefit of, any other person. # Governance Source: https://docs.datafdn.org/foundation/governance Learn about DATA Foundation's governance. As the steward of the DATA Foundation ecosystem, the DATA Foundation works in close alignment with \$DATA Tokenholders and the broader ecosystem. The DATA Foundation supports the DATA Foundation DAO by providing operational support, executing tokenholder governance decisions, and overseeing strategic development and growth of the overall ecosystem. This relationship is designed to empower decentralized governance while preserving efficiency and stability throughout the DATA Foundation ecosystem. ## DATA Foundation DAO's Constitution Read the entire DATA Foundation DAO constitution. ## DATA Foundation’s Role in Governance Provide **strategic grants** to align with innovation via partner projects including, but not limited to, infrastructure providers, application developers, artists, creators, brand partnerships, creative studios, and strategic growth partners. **Promote network security** by creating a security council and appointing members to serve on this council. Developing the ecosystem and protocol by **implementing proposals** of the DATA Foundation DAO that are approved in accordance with the process outlined in the DATA Foundation DAO Constitution and engaging parties to build apps. This may include funding research, public education, and establishing grant programs. Organizing **educational initiatives and hosting events** to increase awareness of and promote the DATA Network, DATA Foundation and ecosystem. Advocating for and **supporting increased autonomy and decentralization** of the DATA Foundation DAO. **Treasury management** and oversight to foster long-term ecosystem growth and support the Foundation’s ongoing mission. # MiCA White Paper Source: https://docs.datafdn.org/foundation/mica