It's the easiest and fastest way to build on top of the Ternoa Chain. Based on Javascript and the Polkadot.js API, it offers developers a seamless experience in the Domain of Web 3.
You can start by familiarizing yourself with the architecture and playing around with the ternoa.js test-dapp to get a good grasp of the Ternoa SDK.
Lastly, don’t forget to have a good time, that's like the most important thing. Cheers 🍻
Table of Contents
Ternoa is a Decentralised, Open source, NFT-centric Layer 1 blockchain that is multi-chain by design and aims to provide a technical stack to build scalable and secure NFTs with native support for advanced features.
NFTs native to our chain can be minted using High-level programming languages and doesn't require smart contract functionality.
With native support for Secret NFTs, Delegating and Lending, Transaction Batching and much more, you might want to give it a try.
Our ecosystem of NFT-based dApps keeps growing day after day. Our SDK relies upon the most popular high-level languages, allowing us to tap into the world’s largest pool of existing developers, thereby minimizing the transition period.
As Ternoa-js is an open-source SDK, feel free to interact with the tools and libraries, log issues, create pull requests or leave feedback. We welcome and greatly appreciate your contribution.
If you’re interested in contributing to the Ternoa SDK, we recommend you inspect our contribution guidelines.
If you want to learn how to use the Ternoa SDK, the test-dapp is the perfect place to familiarize yourself with our architecture and contribute to its development.
If you encounter any errors along the way, technical or otherwise. Let us know and we'll deal with it swiftly. It'll help us further improve the overall experience for our users.
- Open a discussion of type
General
in the discussions section if you encounter any unexpected behaviour. - Open a Bug report using the bug template if the bug persists.
- If you can, suggest a fix in a pull request to resolve that issue.
Make sure to document the error properly, keeping in mind that the better you describe it, the easier it is to deal with.
Pre-requisites: Node.js
Install the latest stable version of the ternoa-js library by running this command:
npm install ternoa-js
This package provides TypeScript types, but you will need TypeScript version 4.2 or higher to use them properly.
You can test out our upcoming features in our Beta @beta
or Release candidate @rc
versions. These versions aren't stable and might contain some technical errors. @beta versions are for internal and public testing only whereas @rc releases tend to be the closest to its production version.
You can check out our version list
over @ npm. Installing a specific version is as easy as replacing the 1.2.0-rc0 with your desired version.
# for version 1.2.0-rc0
npm i [email protected]
What are chain events ?
Events are objects containing decoded values (data) provided by the chain in the result of any transaction executed using the submitTxBlocking
function. At least one of these two ExtrinsicSuccessEvent
or ExtrinsicFailedEvent
events are provided for any transaction depending on its execution. While submitTxBlocking
provides the SDK handlers with the list of main On Chain events BlockchainEvents
(alongside the Block information), we also allow you to filter this list to get the ones you need.
An example to filter only the events list of a balance transfer transaction:
const balanceTransfertEvents = BlockchainEvents.events.findEvents(BalancesTransferEvent)
Note: BlockchainEvents is the result of submitTxBlocking
function: it now contains both block information (block hash, header (...)) and the events list. It can be stored in a constant for example.
To get a better understanding of OnChain Events, we already discussed the option to get the extrinsic events list. In case, you don't need to handle manual signing of transactions, each Ternoa extrinsic features comes with two functions to execute a transaction and an easy one to directly get the required events list. See the example below:
The balancesTransferTx
function creates an unsigned unsubmitted transaction hash and the balancesTransfer
function signs and submits the transaction to provide the events list.
This explains two things:
- Each Helper is composed of two functions
- a
xxxxxxxx
version that signs and submits the transaction, then returns the dedicated event. - a
xxxxxxxxTx
version to create an unsigned and unsubmitted transaction hash.
- We already cover the most common Ternoa pallets with ready to use pallets. However, all extrinsics can be triggered using a combination of generic blockchain helpers like
createTxHex
,signTxHex
,submitTxHex
, etc (check the Handlers Architecture in the next section for more info)
In order to make the returned event data useful, we provide both the native and a friendly ready to use format design:
- a string as an AccountId32 correspond to a classic user valid address.
- a string as u128 is a BN value as string natively used under the hood by the chain.
- a rounded data (ex: amoutRounded) is the "human" version of a data, (usually a BN) that can be directly used.
- some events from the utility pallet do not return any data.
The events below are the Events handled in the Ternoa SDK sorted by categories
Ternoa SDK handles the main features proposed by the Ternoa chain. It allows you to run every transaction from the chain pallet, make a query like asking for some constant's storage. We also provide a bunch of helpers and utility functions to assist you with development.
The main handlers are as follows:
- blockchain: Handles the Core blockchain functions and the API brain initializes the API, execute transactions, query datas, batch transactions and much more.
- account: the functions that allows you to generate a new seed and a keyring.
- assets: the functions to deal with fungible assets that are meant for use within a dApp.
- balance: the Balance pallet with its extrinsics, query and storage.
- nft: the NFT pallet with its extrinsics, query and storage.
- marketplace: the Marketplace pallet with its extrinsics, query and storage.
- events: the events list returned, alongside of block information, when
submitTxBlocking
function is triggered.
For those who are familiar with Polkadot, you will quickly recognize the design structure of our features. If you aren't aware, no worries, the basic principles are easy enough to grasp. Depending on the pallet or handler category, you'll retrive:
- Constants: to request the chain runtime constants.
- Storage: to query the chain state.
- Extrinsics: to execute transactions.
- Utilities: to provide some additional useful functions you can directly import in your project.
As it makes sense for us to provide you the easiest tools to build on the Ternoa chain, we also try to simplify the response format of our functions as much as we can.
Depending on if you go with the easy way or choose the complex route (for added customization), we suggest you to choose the right function: Events and features data will be provided directly on some functions while only transaction hash hex will be returned on others.
Before you can start calling any functions, you'll need to initialize an API Instance using the initializeApi
function located in ternoa-js/src/blockchain.
import { initializeApi } from "ternoa-js"
async function main() {
// Construct
await initializeApi()
// Do something
console.log("Api Connected")
}
You can set the chain endpoint by passing an endpoint parameter to the initializeApi
function whereas the default chain endpoint is: DEFAULT_CHAIN_ENDPOINT
= "wss://alphanet.ternoa.com"
.
{
// Customizing API Endpoint
await initializeApi("wss://mainnet.ternoa.io")
// Do something
console.log("Api Connected to mainnet")
}
Pro Tip: Ternoa SDK provides a very useful getRawApi()
function to interact with the API. If the API is connected, it'll be returned instantaneously.
{
// Assuming that API has been initialized before
const api = await getRawApi()
// example: get last block
const signedBlock = await api.rpc.chain.getBlock()
}
You can create an account using the API itself. You'll need to call the generateSeed
and getKeyringFromSeed
functions.
All functions are organized by a specific theme, for example, the function generateSeed which allows us to create a new account and getKeyringFromSeed which allows us to retrieve its address, can conveniently be found in ternoa-js/src/account.
import { generateSeed, getKeyringFromSeed } from "ternoa-js/account"
;(async () => {
const seed = await generateSeed()
const keyring = await getKeyringFromSeed(seed)
const address = keyring.address
console.log("Your fresh public address is: ", address)
})().catch((e) => {
console.log(e)
})
Creating a basic NFT using the features provided by the SDK is easy enough. This short snippet of code allows you to Create, Submit and Sign an NFT. Essentially minting your NFT with the click of a button.
The function createNft
requires a few parameters like: offchainData
aka metadata, the royalty
percentage, its collectionId
if you want it to belong to an existing collection, a Boolean value isSoulBound
to determine its status, keyring
parameter to sign and submit the transaction and a callback parameter waitUntill
, to define when we get the results of that transaction execution.
// The easiest way to Create your first NFT
import { initializeApi } from "ternoa-js"
import { createNft } from "ternoa-js/nft"
const createMyFirstNFT = async () => {
try {
// We initialize the API instance
await initializeApi()
... //we asume your keyring is already created and provided with CAPS to support transactions fees.
// Here we create, sign and submit the NFT transaction with your keyring
const newNFTEvent = await createNft("My first NFT", 10, undefined, false, keyring, WaitUntil.BlockInclusion)
// Do something with the NFTCreatedEvent response
console.log(newNFTEvent);
...
} catch (e) {
console.log(e)
}
}
That being said, You can opt for the manual route which although being complex, offers more versatility.
The Simple way automated the 3 steps (Create -> Sign => Send) associated with creating an NFT making it much easier to use while not allowing any room for customisation and optimisation.
The manual way provides much more versatility but is significantly more complex. Let's say for example you want to batch transactions together instead of executing them one by one (covered in example section). It'll be useful if you want to simplify the process of creating a large amount of NFTs and minimize repetitive tasks like sending and signing each transaction.
First of all, instead of using the createNft()
function, you will use createNftTx()
. And instead of creating, signing and submiting the transaction and getting the returned events, it will just create an unsigned and unsubmitted Create-NFT Transaction Hash which will be valid for the next 5 minutes.
// Imports
import { initializeApi } from "ternoa-js"
import { createNftTx } from "ternoa-js/nft"
const create createNFTManually = async () => {
try {
// STEP 1: Here we create the transaction and get the transaction hash
const nftTxHash = await createNftTx("My first NFT", 10, undefined, false)
// Do something with the transaction hash
console.log(nftTxHash);
} catch (e) {
console.log(e)
}
}
Now we have the txHash
, we can move to the signing step. But before going into detail, it's good to know that "signing" can be directly embed in the submit function. It means that depending on the submit function you are using (see the last step below), signing your tx hash before submit might not be necessary. In case you sign manually the tx hash, you will receive a hex value of the signed transaction ready to be sent. The signTxHex()
function expect a keyring that will sign the transaction and the transaction hash to be signed.
const create createNFTManually = async () => {
try {
// STEP 2: Here we sign the transaction hash. nftTxHash is the name of the tx hash from the function we created before.
const signTxHash = await signTxHex(keyring, nftTxHash)
// Do something with the hex value.
console.log(signTxHash);
} catch (e) {
console.log(e)
}
}
Submiting a transaction is generally the last step of the transaction execussion process. Ternoa-js provides up to 3 ways to submit a transaction. From the raw and native way to the fully handled and easiest way. Depending on the response format you are expecting and the specific case you want to handle (simple submit, batching some transaction (...)) you will need to go with one of the following functions:
submitTxHex()
- The most versatile and customizable way to submit yourtxHash
. You can manage the callback function yourself. It requires the hex value to be submited but also an optional callback. This function requires the transaction hash to be signed beforehand.
const create createNFTManually = async () => {
try {
// STEP 3.1: Here we submit the transaction hex value.
// Here no callback function is used but the second parameter can be a callback function that help you to handle the result.
// Once again, we use here the tx hex signedTxHash, from the previous step.
const submitTxHash = await submitTxHex(signTxHash)
// Do something with the final tx hash.
console.log(submitTxHash);
} catch (e) {
console.log(e)
}
}
submitTxBlocking()
- The most most convenient way to get Events and Data: This function will sign the transaction for you if you pass a keyring (one less thing to worry about) and it's blocking the execution flow until the transaction is either in a block or in a finalized block. Since submitting needs to work will all kinds of transactions, the result is an object that contains the block information (the block hash, the block header and block extrinsics) and all the events that have happened (instead of only specific ones).
Note: Here you do not need to necessarily pass a signed txHash. If you pass the Keyring as a parameter and an unsigned tx hash the signing process will be done here for you.
const create createNFTManually = async () => {
try {
...
// STEP 3.2: Here we submit the transaction hex value signed before.
// We could have used the unsigned tx Hash and pass a keyring as a third parameter instead.
// Once again, we use here the tx hex signTxHash, from the previous step.
const submitTxHash = await submitTxBlocking(signTxHash, WaitUntil.BlockInclusion)
// We destructure the result of submitTxHash:
const { events, blockInfo } = submitTxHash
// Do something with the events recieved. Example: find the NFTCreatedEvent to access the NFT datas.
console.log(events.findEvents(NFTCreatedEvent));
...
} catch (e) {
console.log(e)
}
}
submitTxNonBlocking()
: This one works as thesubmitTxBlocking
but in a non blocking way. Returns a group objects that are used to track the progress of the transaction execution: The first returned object is a conditional variable which can yield the information if the operation is finished. The second returned object is an array of events which gets populated automatically once the operation is finished. The third returned object contains the block information as the block hash, the block header and block extrinsics.
const create createNFTManually = async () => {
try {
...
// STEP 3.3: It get the same parameters as the submitTxBlocking
// Here we submit the transaction hex value signed before.
// We could have used the unsigned tx Hash and pass a keyring as a third parameter instead.
// Once again, we use here the tx hex signTxHash, from the previous step 2.
const submitTxHash = await submitTxNonBlocking(signTxHash, WaitUntil.BlockInclusion)
// Do something with the events recieved.
console.log(submitTxHash);
...
} catch (e) {
console.log(e)
}
}
Check out our Wiki page. We are constantly adding new pages and guides there.
The auto generated typescript documentation is available @Dev.Ternoa and any additional resources can be found over @Docs.Ternoa,
Discover our end-to-end test Dapp here to test out the Ternoa SDK. Here's the E2E-test-dapp repository -> repo
Here's the Ternoa.js SDK Github repository -> repo
To build a new version of the library, run:
npm run build
To generate the documentation website, run:
npm run docs
You can locate the static website in the /docs directory.
The test suites rely upon testing pairs in the testing pairs file. In order to prepare these pairs, test-setup.ts and test-teardown.ts are supplied and drained of $CAPS
respectively. Environmental variables BLOCKCHAIN_ENDPOINT
(e.g. BLOCKCHAIN_ENDPOINT=wss://alphanet.ternoa.com
), SEED_TEST_FUNDS
and SEED_TEST_FUNDS_PUBLIC_KEY
have to be defined in order to run these test suites.
To initiate the test suites, run:
npm run test
This project uses Industry standard ESLint and Typescript rules to ensure good coding practices and readability.
We’ve set up linters and formatters to help catch errors and enhance the overall experience:
- Prettier – ensures that code is formatted in a readable way.
- ESLint — checks code for antipatterns as well as formatting.
If you use Visual Studio Code editor, we suggest you to install ESLint and Prettier extensions.
# Downloads the package lists and "updates" them.S
sudo apt update -y
# Installing podman.
sudo apt install podman
# Building the image using podman and Docker file.
podman build -t tsdk .
# Checking if everything is OK.
podman images | grep tsdk
# Run the tsk image.
podman run tsdk
We’ll illustrate some useful podman commands and their use cases next. It’s important to note that we’ve omitted most of the flags to make them concise. Make sure to build the image as per the steps defined in the "Build and Run with Podman" section.
In case no command arguments are specified, It’ll try to build the starter-project
by default. To override this, simply add bash
at the end of the command. It'll look something like: podman run tsdk bash
.
A container which completed its execution or is no longer in use due to the user disconnecting will not be removed by default, it’ll switch to the exit state.
Use the flag --rm
to make sure it’s deleted and later removed.
# The --rm flag removes the container after usage.
podman run --rm tsdk
# Check if any container is running or stopped.
podman ps -a
The container uses a local copy of the repo in order to compile and run examples. This means that if changes are made inside the container, they’ll fail to propagate and will be lost. To mitigate this issue, the virtual container volume /workdir
needs to be mapped to a directory on the host machine that contains the Ternoa.js repo. After the mapping is done, any changes made in the mapped directory will be reflected in the container.
This way, you can develop dApps without having to install all the required dependencies. For the workflow check out the "Create A Development Environment" section.
# Flag -v tells the host machine to map the physical "./." path with the virtual container one "/workdir". If no command arguments are given this will try to compile and run the starter-project project.
podman run -v ./.:/workdir tsdk
The pre defined operation of the container when executed is to compile and and initiate the starter-project
. To execute a different operation, additional commands can be passed at the end. For example: padding bash
will run the bash shell session instead of the default operation.
# If no command arguments are given this will try to compile and run the starter-project. By passing "bash" we make sure that we run a bash shell session once the container starts.
podman run -it tsdk bash
# Flag "-d" runs the container in detached mode.
podman run -d tsdk bash
# Access its shell.
podman exec -itl bash
The dockerfile is made in a way that it can be used to develop new applications with it.
Example of a typical workflow:
- The host installs git, clones the repo and install a code editor like VS Code.
- runs the container in interactive mode with
/workdir
pointing to a work directory on the host machine (can be your own project or ternoa-js). - writes code via a code editor and uses the terminal to run the
tsc
andnode
commands. - With that setup all the changes are done locally on the host machine while the container is only used to compile and run the app.
# Flag "--name" is used to name the container.
podman run -it --name my_sdk_env -v ./.:/workdir tsdk bash
# Do some activity and the exit the container
[root@d4ad8ec11655:/workdir] nano -V
[root@d4ad8ec11655:/workdir] apt install nano
[root@d4ad8ec11655:/workdir] exit
# Return to the same container
podman start my_sdk_env
podman exec -it my_sdk_env /bin/bash
[root@d4ad8ec11655:/workdir] nano -V
Ternoa.js uses the Apache 2.0 License.