Endless
  • 🚀README
  • Discovery
    • 🚀Endless Web3 Genesis Cloud
    • 💎Business Model
    • 🎯Vision
    • ✈️Roadmap
    • 🪙Economics
    • 👤Team
      • Yu Xiong
      • Amit Kumar Jaiswal
      • Ned
      • 0xfun
      • Scott Trowbridge
      • Neeraj Sharma LLB
      • Amjad Suleman
      • Binu Paul
      • Eduard Romulus GOEAN
    • ❤️Developer Community
  • Endless Chain
    • Tech Docs
      • Account Address Format
      • Endless Account
      • Endless Coin(EDS)
      • Sponsored Transaction
      • On-Chain Multisig
      • Randomness
      • Safety Transaction
      • Token Locking & Distribution
    • Start
      • Learn about Endless
        • Accounts
        • Resources
        • Events
        • Transactions and States
        • Gas and Storage Fees
        • Computing Transaction Gas
        • Blocks
        • Staking
          • Delegated Staking
        • Governance
        • Endless Blockchain Deep Dive
          • Validator Nodes Overview
          • Fullnodes Overview
          • Node Networks and Synchronization
        • Move - A Web3 Language and Runtime
      • Explore Endless
      • Latest Endless Releases
      • Networks
    • Build
      • Tutorials
        • Your First Transaction
        • Your First Fungible Asset
        • Your First NFT
        • Your First Move Module
        • Your First Multisig
      • Learn the Move Language
        • The Move Book
          • Getting Started
            • Introduction
            • Modules and Scripts
          • Primitive Types
            • Move Tutorial
            • Integers
            • Bool
            • Address
            • Vector
            • Signer
            • References
            • Tuples and Unit
          • Basic Concepts
            • Local Variables and Scope
            • Equality
            • Abort and Assert
            • Conditionals
            • While, For, and Loop
            • Functions
            • Structs and Resources
            • Constants
            • Generics
            • Abilities
            • Uses and Aliases
            • Friends
            • Packages
            • Package Upgrades
            • Unit Tests
          • Global Storage
            • Global Storage - Structure
            • Global Storage - Operators
          • Reference
            • Libraries
            • Move Coding Conventions
        • Advanced Move Guides
          • Objects
            • Creating Objects
            • Configuring objects
            • Using objects
          • Move Scripts
            • Writing Move Scripts
            • Compiling Move Scripts
            • Running Move Scripts
            • Move Scripts Tutorial
          • Resource Accounts
          • Modules on Endless
          • Cryptography
          • Gas Profiling
          • Security
      • Endless Standards
        • Object
        • Endless Fungible Asset Standard
        • Endless Digital Asset Standard
        • Endless Wallet Standard
      • Endless APIs
        • Fullnode Rest API
        • Indexer Restful API
          • Indexer Installation
        • GRPC Transaction Stream
          • Running Locally
          • Custom Processors
            • End-to-End Tutorial
            • Parsing Transactions
          • Self-Hosted Transaction Stream Service
      • Endless SDKs
        • TypeScript SDK
          • Account
          • SDK Configuration
          • Fetch data from chain
          • Transaction Builder
          • HTTP Client
          • Move Types
          • Testing
          • Typescript
        • Rust SDK
        • Go SDK
      • Endless CLI
        • Install the Endless CLI
          • Install On Mac
          • Install On Alibaba Cloud
          • Install On Linux
          • Install On Windows
        • CLI Configuration
        • Use Endless CLI
          • Working With Move Contracts
            • Arguments in JSON Tutorial
          • Trying Things On-Chain
            • Look Up On-Chain Account Info
            • Create Test Accounts
          • Running A Local Network
            • Running a Public Network
          • Managing a Network Node
      • Integrate with Endless
        • Endless Token Overview
        • Application Integration Guide
      • Endless VSCode extension
      • Advanced Builder Guides
        • Develop Locally
          • Running a Local Network
          • Run a Localnet with Validator
    • Nodes
      • Learn about Nodes
      • Run a Validator and VFN
        • Node Requirements
        • Deploy Nodes
          • Using Docker
          • Using AWS
          • Using Azure
          • Using GCP
        • Connect Nodes
          • Connect to a Network
        • Verify Nodes
          • Node Health
          • Validator Leaderboard
      • Run a Public Fullnode
        • PFN Requirements
        • Deploy a PFN
          • Using Pre-compiled Binary
          • Using Docker
          • Using GCP 🚧 (under_construction)
        • Verify a PFN
        • Modify a PFN
          • Upgrade your PFN
          • Generate a PFN Identity
          • Customize PFN Networks
      • Bootstrap a Node
        • Bootstrap from a Snapshot
        • Bootstrap from a Backup
      • Configure a Node
        • State Synchronization
        • Data Pruning
        • Telemetry
        • Locating Node Files
          • Files For Mainnet
          • Files For Testnet
          • Files For Devnet
      • Monitor a Node
        • Node Inspection Service
        • Important Node Metrics
        • Node Health Checker
    • Reference
      • Endless Error Codes
      • Move Reference Documentation
      • Endless Glossary
    • FAQs
  • Endless Bridge
    • Intro to Endless Bridge
    • How to use bridge
    • Liquidity Management
    • Faucet
    • Developer Integration
      • Contract Integration
        • Message Contract
        • Execute Contract
      • Server-Side Integration
        • Message Sender
        • Example of Message Listener Service (Rust)
        • Example of Token Cross-Chain (JS)
  • Endless Wallet
    • User Guide
    • Basic Tutorial
    • FAQs
    • MultiAccount
    • SDK
      • Functions
      • Events
  • GameFi
    • Intro
    • GameFi & Endless
  • Endless Modules
    • Stacks
    • Storage
    • Module List
  • Endless Ecosystem
    • Intro
    • Show Cases
    • App Demo
  • Whitepaper
  • Endless SCAN
    • User Guide
  • MULTI-SIGNATURE
    • Multi-Signature User Guide
  • Regulations
    • Privacy Policy
    • Terms of Service
    • Funding Terms - Disclaimer
Powered by GitBook
On this page
  • Step 1: Pick an SDK
  • Step 2: Run the example
  • Step 3: Understand the output
  • Step 4: The SDK in depth
  • Step 4.1: Initializing the clients
  • Step 4.2: Creating local accounts
  • Step 4.3: Creating blockchain accounts
  • Step 4.4: Creating a collection
  • Step 4.5: Creating a token
  • Step 4.6: Reading token and collection metadata
  • Step 4.7: Reading an object's owner
  • Step 4.8: Transfer the object back and forth
  • Supporting documentation
Export as PDF
  1. Endless Chain
  2. Build
  3. Tutorials

Your First NFT

PreviousYour First Fungible AssetNextYour First Move Module

Last updated 2 months ago

This tutorial describes how to create and transfer non-fungible assets on the Endless blockchain. The Endless no-code implementation for non-fungible digital assets can be found in the Move module.

Step 1: Pick an SDK

Install your preferred SDK from the below list:

  • TypeScript SDK

Step 2: Run the example

Clone the endless-ts-sdk repo and build it:

git clone https://github.com/endless-labs/endless-ts-sdk.git
cd endless-ts-sdk
pnpm install
pnpm build

Navigate to the Typescript ESM examples directory:

cd examples/typescript-esm

Install the necessary dependencies and build it:

pnpm install
pnpm build

Run the Typescript example:

pnpm run simple_digital_asset

Clone the endless-core repo:

git clone https://github.com/endless-labs/endless.git

Navigate to the Python SDK directory:

cd endless-core/ecosystem/python/sdk

Install the necessary dependencies:

curl -sSL https://install.python-poetry.org | python3
poetry install
poetry run python -m examples.simple_endless_token

Step 3: Understand the output

The following output should appear after executing the simple_digital_asset example, though some values will be different:

=== Addresses ===

Alice's address is: 0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162

=== Create the collection ===

Alice's collection: {
    "collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
    "collection_name": "Example Collection",
    "creator_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
    "current_supply": 0,
    "description": "Example description.",
    "last_transaction_timestamp": "2023-11-29T21:26:03.204874",
    "last_transaction_version": 8001101,
    "max_supply": 18446744073709552000,
    "mutable_description": true,
    "mutable_uri": true,
    "table_handle_v1": null,
    "token_standard": "v2",
    "total_minted_v2": 0,
    "uri": "endless.dev"
}

=== Alice Mints the digital asset ===

Alice's digital assets balance: 1
Alice's digital asset: {
    "token_standard": "v2",
    "token_properties_mutated_v1": null,
    "token_data_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
    "table_type_v1": null,
    "storage_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
    "property_version_v1": 0,
    "owner_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
    "last_transaction_version": 8001117,
    "last_transaction_timestamp": "2023-11-29T21:26:04.521624",
    "is_soulbound_v2": false,
    "is_fungible_v2": false,
    "amount": 1,
    "current_token_data": {
        "collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
        "description": "Example asset description.",
        "is_fungible_v2": false,
        "largest_property_version_v1": null,
        "last_transaction_timestamp": "2023-11-29T21:26:04.521624",
        "last_transaction_version": 8001117,
        "maximum": null,
        "supply": 0,
        "token_data_id": "0x9f4460e29a66b4e41cef1671767dc8a5e8c52a2291e36f84b8596e0d1205fd8c",
        "token_name": "Example Asset",
        "token_properties": {},
        "token_standard": "v2",
        "token_uri": "endless.dev/asset",
        "current_collection": {
            "collection_id": "0x23ece6c35415f5c5a720dc4de2820cabece0a6f1768095db479f657ad2c05753",
            "collection_name": "Example Collection",
            "creator_address": "0x770dbeb6101056eac5a19de9a73ad72fac512e0de909e7bcb13a9d9241d1d162",
            "current_supply": 1,
            "description": "Example description.",
            "last_transaction_timestamp": "2023-11-29T21:26:04.521624",
            "last_transaction_version": 8001117,
            "max_supply": 18446744073709552000,
            "mutable_description": true,
            "mutable_uri": true,
            "table_handle_v1": null,
            "token_standard": "v2",
            "total_minted_v2": 1,
            "uri": "endless.dev"
        }
    }
}

=== Transfer the digital asset to Bob ===

Alice's digital assets balance: 0
Bob's digital assets balance: 1

This example demonstrates:

* Initializing the Endless client. * The creation of two accounts: Alice and Bob. * The funding and creation of Alice and Bob's accounts. * The creation of a collection and a token using Alice's account. * Alice sending a token to Bob.

The following output should appear after executing the simple_endless_token example, though some values will be different:

=== Addresses ===
Alice: 0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89
Bob: 0xfbca055c91d12989dc6a2c1a5e41ae7ba69a35454b04c69f03094bbccd5210b4

=== Initial Coin Balances ===
Alice: 100000000
Bob: 100000000

=== Creating Collection and Token ===

Collection data: {
    "address": "0x38f5310a8f6f3baef9a54daea8a356d807438d3cfe1880df563fb116731b671c",
    "creator": "0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89",
    "name": "Alice's",
    "description": "Alice's simple collection",
    "uri": "https://endless.dev"
}

Token owner: Alice
Token data: {
    "address": "0x57710a3887eaa7062f96967ebf966a83818017b8f3a8a613a09894d8465e7624",
    "owner": "0x391f8b07439768674023fb87ae5740e90fb8508600486d8ee9cc411b4365fe89",
    "collection": "0x38f5310a8f6f3baef9a54daea8a356d807438d3cfe1880df563fb116731b671c",
    "description": "Alice's simple token",
    "name": "Alice's first token",
    "uri": "https://endless.dev/img/nyan.jpeg",
    "index": "1"
}

=== Transferring the token to Bob ===
Token owner: Bob

=== Transferring the token back to Alice ===
Token owner: Alice

This example demonstrates:

* Initializing the REST and faucet clients. * The creation of two accounts: Alice and Bob. * The funding and creation of Alice and Bob's accounts. * The creation of a collection and a token using Alice's account. * Alice sending a token to Bob. * Bob sending the token back to Alice.


Step 4: The SDK in depth


Step 4.1: Initializing the clients

In the first step, the simple_digital_asset example initializes the Endless client:

const ENDLESS_NETWORK: Network =
  NetworkToNetworkName[process.env.ENDLESS_NETWORK] || Network.DEVNET;
const config = new EndlessConfig({ network: ENDLESS_NETWORK });
const endless = new Endless(config);

By default, the Endless client points to Endless devnet services. However, it can be configured with the network input argument

In the first step, the example initializes both the API and faucet clients.

  • The API client interacts with the REST API.

  • The faucet client interacts with the devnet Faucet service for creating and funding accounts.

:!: static/sdks/python/examples/simple_endless_token.py section_1a

Using the API client we can create a TokenClient that we use for common token operations such as creating collections and tokens, transferring them, claiming them, and so on.

:!: static/sdks/python/examples/simple_endless_token.py section_1b
:!: static/sdks/python/examples/common.py section_1

By default, the URLs for both the services point to Endless devnet services. However, they can be configured with the following environment variables:

  • ENDLESS_NODE_URL

  • ENDLESS_FAUCET_URL


Step 4.2: Creating local accounts

The next step is to create two accounts locally. Accounts consist of a public address and the public/private key pair used to authenticate ownership of the account. This step demonstrates how to generate an Account and store its key pair and address in a variable.

const alice = Account.generate();
const bob = Account.generate();
:!: static/sdks/python/examples/simple_endless_token.py section_2

Note that this only generates the local keypair. After generating the keypair and public address, the account still does not exist on-chain.


Step 4.3: Creating blockchain accounts

In order to actually instantiate the Account on-chain, it must be explicitly created somehow. On the devnet network, you can request free coins with the Faucet API to use for testing purposes. This example leverages the faucet to fund and inadvertently create Alice and Bob's accounts:

await endless.fundAccount({
  accountAddress: alice.accountAddress,
  amount: 100_000_000,
});
await endless.faucet.fundAccount({
  accountAddress: bob.accountAddress,
  amount: 100_000_000,
});
:!: static/sdks/python/examples/simple_endless_token.py section_3

Step 4.4: Creating a collection

Now begins the process of creating the digital, non-fungible assets. First, as the creator, you must create a collection that groups the assets. A collection can contain zero, one, or many distinct fungible or non-fungible assets within it. The collection is simply a container, intended only to group assets for a creator.

Your application will call createCollectionTransaction and then signAndSubmitTransaction to chain:

const createCollectionTransaction = await endless.createCollectionTransaction({
  creator: alice,
  description: collectionDescription,
  name: collectionName,
  uri: collectionURI,
});

const committedTxn = await endless.signAndSubmitTransaction({
  signer: alice,
  transaction: createCollectionTransaction,
});

This is the function signature of createCollectionTransaction. It returns a SingleSignerTransaction that can be simulated or submitted to chain:

export async function createCollectionTransaction(
  args: {
    creator: Account;
    description: string;
    name: string;
    uri: string;
    options?: InputGenerateTransactionOptions;
  } & CreateCollectionOptions,
): Promise<SingleSignerTransaction>;

Your application will call create_collection:

:!: static/sdks/python/examples/simple_endless_token.py section_4

This is the function signature of create_collection. It returns a transaction hash:

:!: static/sdks/python/endless_sdk/endless_token_client.py create_collection

Step 4.5: Creating a token

To create a token, the creator must specify an associated collection. A token must be associated with a collection, and that collection must have remaining tokens that can be minted. There are many attributes associated with a token, but the helper API exposes only the minimal amount required to create static content.

Your application will call mintTokenTransaction:

const mintTokenTransaction = await endless.mintTokenTransaction({
  creator: alice,
  collection: collectionName,
  description: tokenDescription,
  name: tokenName,
  uri: tokenURI,
});

const committedTxn = await endless.signAndSubmitTransaction({
  signer: alice,
  transaction: mintTokenTransaction,
});

This is the function signature of mintTokenTransaction. It returns a SingleSignerTransaction that can be simulated or submitted to chain:

async mintTokenTransaction(args: {
    creator: Account;
    collection: string;
    description: string;
    name: string;
    uri: string;
    options?: InputGenerateTransactionOptions;
  }): Promise<SingleSignerTransaction>

Your application will call mint_token:

:!: static/sdks/python/examples/simple_endless_token.py section_5

This is the function signature of mint_token. It returns a transaction hash:

:!: static/sdks/python/endless_sdk/endless_token_client.py mint_token

Step 4.6: Reading token and collection metadata

Both the collection and token assets are Objects on-chain with unique addresses. Their metadata is stored at the object address. The SDKs provide convenience wrappers around querying this data:

To read a collection's metadata:

const alicesCollection = await endless.getCollectionData({
  creatorAddress: alice.accountAddress,
  collectionName,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(`Alice's collection: ${JSON.stringify(alicesCollection, null, 4)}`);

To read an owned token's metadata:

const alicesDigitalAsset = await endless.getOwnedDigitalAssets({
  ownerAddress: alice.accountAddress,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
  `Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);

To read a collection's metadata:

:!: static/sdks/python/examples/simple_endless_token.py section_6

To read a token's metadata:

:!: static/sdks/python/examples/simple_endless_token.py get_token_data

Step 4.7: Reading an object's owner

Each object created from the endless_token.move contract is a distinct asset. The assets owned by a user are stored separately from the user's account. To check if a user owns an object, check the object's owner:

const alicesDigitalAsset = await endless.getOwnedDigitalAssets({
  ownerAddress: alice.accountAddress,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
  `Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
export async function getOwnedDigitalAssets(args: {
  endlessConfig: EndlessConfig;
  ownerAddress: AccountAddressInput;
  options?: PaginationArgs & OrderByArg<GetTokenActivityResponse[0]>;
}): Promise<GetOwnedTokensResponse> {
  const { endlessConfig, ownerAddress, options } = args;

  const whereCondition: CurrentTokenOwnershipsV2BoolExp = {
    owner_address: { _eq: AccountAddress.from(ownerAddress).toStringLong() },
    amount: { _gt: 0 },
  };

  const graphqlQuery = {
    query: GetCurrentTokenOwnership,
    variables: {
      where_condition: whereCondition,
      offset: options?.offset,
      limit: options?.limit,
      order_by: options?.orderBy,
    },
  };

  const data = await queryIndexer<GetCurrentTokenOwnershipQuery>({
    endlessConfig,
    query: graphqlQuery,
    originMethod: "getOwnedDigitalAssets",
  });

  return data.current_token_ownerships_v2;
}
:!: static/sdks/python/examples/simple_endless_token.py section_7
:!: static/sdks/python/examples/simple_endless_token.py owners

Step 4.8: Transfer the object back and forth

Each object created from the endless_token.move contract is a distinct asset. The assets owned by a user are stored separately from the user's account. To check if a user owns an object, check the object's owner:

const alicesDigitalAsset = await endless.getOwnedDigitalAssets({
  ownerAddress: alice.accountAddress,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});

console.log(
  `Alice's digital asset: ${JSON.stringify(alicesDigitalAsset[0], null, 4)}`,
);
const transferTransaction = await endless.transferDigitalAsset({
  sender: alice,
  digitalAssetAddress: alicesDigitalAsset[0].token_data_id,
  recipient: bob.accountAddress,
});
const committedTxn = await endless.signAndSubmitTransaction({
  signer: alice,
  transaction: transferTransaction,
});
const pendingTxn = await endless.waitForTransaction({
  transactionHash: committedTxn.hash,
});
const alicesDigitalAssetsAfter = await endless.getOwnedDigitalAssets({
  ownerAddress: alice.accountAddress,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(
  `Alices's digital assets balance: ${alicesDigitalAssetsAfter.length}`,
);

const bobDigitalAssetsAfter = await endless.getOwnedDigitalAssets({
  ownerAddress: bob.accountAddress,
  minimumLedgerVersion: BigInt(pendingTxn.version),
});
console.log(`Bob's digital assets balance: ${bobDigitalAssetsAfter.length}`);
:!: static/sdks/python/examples/simple_endless_token.py section_8
:!: static/sdks/python/endless_sdk/endless_token_client.py transfer_token
:!: static/sdks/python/examples/simple_endless_token.py section_9
:!: static/sdks/python/examples/simple_endless_token.py section_10
:!: static/sdks/python/examples/simple_endless_token.py section_11

Supporting documentation

Run the Python example:

See the full code See for the complete code as you follow the below steps.

See the full code See for the complete code as you follow the below steps.

initializes these values as follows:

endless_token.move
simple_digital_asset
simple_endless_token
simple_digital_asset
simple_endless_token
common.py
Account basics
TypeScript SDK
Golang SDK
Rust SDK
REST API specification