# 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.
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.
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.**
## 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.
## Private Licenses
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.
### 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.
# 📜 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).
## 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:
## 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
### 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
### 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.
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.
```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
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.
## 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:
## 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:
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.
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 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!
***
## 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