SDK
crossbell.js
Guides
Note

Note

A character can create notes.

A note is typically an article published on the blockchain.

Create a Note

Assume that we have a character named Alice with character ID 42. Alice wants create a note as a blog post:

contract.postNote(
  42, // character ID
  "https://example.com/note.json", // note metadata URL
);

Please refer to postNote for more details.

Note Metadata

The https://example.com/note.json above is the URL of the note metadata. The note is a JSON file with the following structure:

{
  "title": "Hello World",
  "content": "This is a note."
}

It can be hosted on any web server. It can be ipfs:// URL. For example, here is another more complex note metadata: ipfs://bafkreidhjuuo2vibtevn66v2q6bjq652sfox6csocg6v2adwhywifcnzcy (opens in a new tab).

You can create a note with metadata content object directly:

contract.postNote(
  42, // character ID
  {
    title: "Hello World",
    content: "This is a note."
  }
);

In this case, the SDK will automatically upload the metadata to IPFS and create a ipfs:// URL for you.

Please refer to Note Metadata for more details.

Link and Unlink a Note

You can use link to link a note to another note.

For example, a character 42 likes the note 5 written by another character 43:

// character ID 42 likes note ID 43-5 (the 5th note of character 43)
const { data } = await contract.linkNote(42, 43, 5, 'like')
console.log(data) // linklist ID
 
// character ID 42 unlikes note ID 43-5
await contract.unlinkNote(42, 43, 5, 'like')

Please refer to Link Topics - Like and Unlike a Note for more details.

Note Link/Mint Module

πŸ’‘

Before attaching a link or a module to a note, please refer to the Concepts - Mint/Link Module section to learn more about what a module is.

Please refer to the linkModule options API and mintModule options API for more details.

Mint a Note

After creating a note, everyone (wallets) can mint it.

For example, to mint a note with characterId 42 and noteId 5 to the address 0x1234567890123456789012345678901234567890:

const { data } = await contract.mintNote(
  42, // characterId
  5, // noteId
  '0x1234567890123456789012345678901234567890', // toAddress
)

Now, the address 0x1234567890123456789012345678901234567890 owns the note NFT with contract address 0xabc123def456abc123def456abc123def456abc1 and token ID 6 (meaning that the note NFT is stored at the contract of address 0xabc123def456abc123def456abc123def456abc1, and the minter is the 6th to mint).

This minted note NFT is of ERC721 type. It can be transferred to another address, or burned.

πŸ’‘

It deserves to be noted that minting might fail if the note is attached with a mint module that limits who can mint it. The app should handle this case.

Please refer to mintNote for more details.

Query Notes

You can query the notes from the indexer.

For example, to query the notes of character 42:

const { list } = await indexer.getNotes({
  characterId: 42,
});

Topics

Reply to a Note

Reply

A character can reply to a note. This is done by using the contract.postNoteForNote method:

// character ID 42 replies to note ID 43-5 (the 5th note of character 43)
const result = contract.postNoteForNote(
  42, // character ID
  { content: "Thank you!" }, // note metadata
  43, // target character ID
  5, // target note ID
);
console.log(result) // The note ID of the reply, e.g. 1

To query all the replies of the note 43-5:

const { list } = await indexer.getNotes({
  toCharacterId: 43,
  toNoteId: 5,
});
console.log(list) // The list of notes that reply to note 43-5, which includes the note 42-1

Reply to a Reply (Thread)

Of course, the character 43 can also reply to the reply:

// character ID 43 replies to note ID 42-6 (the 6th note of character 42)
contract.postNoteForNote(
  43, // character ID
  { content: "You're welcome!" }, // note metadata
  42, // target character ID
  1, // target note ID
);

This becomes a tree structure of notes:

43-5
└── 42-1
    └── 43-6

This is commonly called a "note thread". In real-world applications, the thread might be more complex:

43-5
β”œβ”€β”€ 42-1
β”‚   β”œβ”€β”€ 43-6
β”‚   └── 42-2
β”‚       └── 43-7
└── 42-3
    └── 43-8

We can query the whole thread recursively:

async function getNoteThread(
  characterId: number,
  noteId: number,
  depth: number = 0,
): Promise<Note[]> {
  const { list } = await indexer.getNotes({
    toCharacterId: characterId,
    toNoteId: noteId,
  });
 
  if (depth === 0) {
    return list;
  }
 
  const replies = await Promise.all(
    list.map((note) =>
      getNoteThread(note.characterId, note.noteId, depth - 1),
    ),
  );
 
  return list.concat(...replies);
}

Or using the includeNestedNotes parameter to fetch them in one query (recommended):

const { list } = await indexer.getNotes({
  toCharacterId: 43,
  toNoteId: 5,
  includeNestedNotes: true,
  nestedNotesDepth: 3, // 3 levels of replies, max is 3 for performance
  nestedNotesLimit: 10, // 10 replies per level
});

Note Variant: "Achievement"

Publish an Achievement Note

A character can post a note as an achievement for others to mint.

This is done by setting the "variant" field in the note metadata to "achievement". For example:

{
  "variant": "achievement",
  "title": "Sponsor NFT",
  "content": "This is a sponsor NFT for my fans.",
  "attachments": [
    {
      "mime_type": "image/png",
      "address": "ipfs://example/sponsor_nft.png"
    }
  ],
  "attributes": [
    {
      "trait_type": "Tier",
      "value": "Gold",
      "display_type": "string"
    }
  ]
}
contract.postNote(
  42, // character ID
  "https://example.com/my-sponsor-nft.json", // note metadata URL
);

This note is not technically special; it just has a "variant" field to indicate that it is an achievement NFT, and should be displayed as such in Apps.

Query Achievement Notes

Query Achievement Notes Published by a Character

You can query with the variant field in indexer.getNotes to get the list of achievement notes published by a character

const { list } = await indexer.getNotes({
  characterId: 42,
  variant: "achievement",
});
Query Achievement Notes Minted by an Address

You can query with the variant field in indexer.getMintedNotesOfAddress to get the list of achievement notes minted by an address

const { list } = await indexer.getMintedNotesOfAddress(
  "0x1234567890123456789012345678901234567890",
  { variant: "achievement" },
);

Attach Mint Module

Just as other notes, an achievement note can be attached with a mint module.

One useful example is to limit the group of people who can mint the achievement note. In this case, ApproveMintModule can be used.

contract.postNote(
  42, // character ID
  "https://example.com/my-sponsor-nft.json", // note metadata URL
  {
    mintModule: {
      address: "0x328610484ba1fAAE0fCDEe44990D199cD84c8608",
      data: [
        ['0x1234567890123456789012345678901234567890', '0xabc123def456abc123def456abc123def456abc1'],
        1,
      ]
    },
  }
);

The above code will attach a ApproveMintModule to the achievement note, which means that only these two addresses can mint the achievement note, and each address can mint it only once (1).

Restrict Minting

Approve Mint Module

Attach Approve Mint Module

As we have seen in the previous section, you can attach the mint module ApproveMintModule to a note to restrict who can mint it.

contract.postNote(
  42, // character ID
  "https://example.com/my-sponsor-nft.json", // note metadata URL
  {
    mintModule: {
      address: "0x328610484ba1fAAE0fCDEe44990D199cD84c8608",
      data: [
        ['0x1234567890123456789012345678901234567890', '0xabc123def456abc123def456abc123def456abc1'],
        1,
      ]
    },
  }
);

In this example, only addresses 0x1234567890123456789012345678901234567890 and 0xabc123def456abc123def456abc123def456abc1 can mint the note, and each address can mint it only once (1). This is called initData.

Query Approve Mint Module

How do we recover the mint module information from a note? Here is an example.

For example, the above code posted a note 42-5 (Character ID: 42, Note Id: 5), you can query the mint module attached with indexer.getMintModules:

const { list } = await indexer.getMintModules({
  toCharacterId: 42,
  toNoteId: 5,
});
const mintModule = list[0]; // there should be only one mint module for the specified note
console.log(mintModule);

The result will look like this:

{
  "targetItemType": "Note",
  "linkValue": "42-5",
  "contractAddress": "0x328610484ba1faae0fcdee44990d199cd84c8608",
  "initData": "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000001234567890123456789012345678901234567890000000000000000000000000abc123def456abc123def456abc123def456abc1",
  "returnData": "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000001234567890123456789012345678901234567890000000000000000000000000abc123def456abc123def456abc123def456abc1",
  "toCharacterId": 45,
  "toNoteId": 188,
  "operator": "0xc560eb6fd0c2eb80df50e5e06715295ae1205049",
  "createdAt": "2023-03-30T16:26:10.000Z",
  "updatedAt": "2023-03-30T16:26:10.000Z",
  "deletedAt": null,
  "transactionHash": "0x5ea8d21d8c6edd5ed5f37a00ac5fc31b536d8befec12d592b9e4bda8e8445a15",
  "blockNumber": 29834475,
  "logIndex": 1,
  "updatedTransactionHash": "0x5ea8d21d8c6edd5ed5f37a00ac5fc31b536d8befec12d592b9e4bda8e8445a15",
  "updatedBlockNumber": 29834475,
  "updatedLogIndex": 1
}

initData is the encoded init data string we passed in the previous section. We can decode it with contract.decodeModuleInitData:

const decodedInitData = await contract.decodeModuleInitData(
  '0x328610484ba1faae0fcdee44990d199cd84c8608',
  '0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000001234567890123456789012345678901234567890000000000000000000000000abc123def456abc123def456abc123def456abc1',
);
console.log(decodedInitData);

The result will look like this:

[
  [
    '0x1234567890123456789012345678901234567890',
    '0xAbc123def456aBc123def456aBc123DEf456ABc1'
  ],
  BigNumber { _hex: '0x01', _isBigNumber: true }
]

which is the same as the initData we passed in the previous section. Please note that the addresses are in formatted mixed-case (aka. a Checksum Address), and the number 1 is a BigNumber. You can convert it to a number with toNumber().